derailed/k9s · warning
(%s) access denied for user on resource %q:%s in namespace %
Error message
(%s) access denied for user on resource %q:%s in namespace %q
What it means
Returned by APIClient.CanI (internal/client/client.go:209) when a SelfSubjectAccessReview submitted to the API server comes back with Status.Allowed=false for one of the requested verbs. This is k9s' RBAC gate: before rendering or executing an action it asks the server whether the current user may perform verb v on resource gvr/name in namespace ns. Both the denial and successful grants are cached for cacheExpiry, so the result is sticky until the cache entry expires.
Source
Thrown at internal/client/client.go:209
slogs.GVR, gvr,
slogs.Namespace, ns,
slogs.ResName, name,
slogs.Verb, verbs,
)
if resp != nil {
clog.Debug("[CAN] response",
slogs.AuthStatus, resp.Status.Allowed,
slogs.AuthReason, resp.Status.Reason,
)
}
if err != nil {
clog.Warn("Auth request failed", slogs.Error, err)
a.cache.Add(key, false, cacheExpiry)
return auth, err
}
if !resp.Status.Allowed {
a.cache.Add(key, false, cacheExpiry)
return auth, fmt.Errorf("(%s) access denied for user on resource %q:%s in namespace %q", v, name, gvr, ns)
}
}
auth = true
a.cache.Add(key, true, cacheExpiry)
return
}
// CurrentNamespaceName return namespace name set via either cli arg or cluster config.
func (a *APIClient) CurrentNamespaceName() (string, error) {
return a.config.CurrentNamespaceName()
}
// ServerVersion returns the current server version info.
func (a *APIClient) ServerVersion() (*version.Info, error) {
if v, ok := a.cache.Get(serverVersion); ok {
if vi, ok := v.(*version.Info); ok {
return vi, nilView on GitHub (pinned to 2d3ccc6ba2)
Solutions
- Verify the denial outside k9s: kubectl auth can-i <verb> <resource> -n <namespace> (same SelfSubjectAccessReview path)
- If access is intended, grant it: create a Role/ClusterRole plus RoleBinding covering the verb, resource and namespace shown in the message
- Check the active context and impersonation flags (--context, --as, --as-group) — you may be authenticated as a different identity than you think
- If running restricted by design, treat err!=nil || auth==false from CanI as a soft denial in your code instead of a hard failure
Example fix
// before: assuming CanI only fails on transport errors
ok, _ := client.CanI(ns, gvr, name, []string{"delete"})
if ok {
deletePod()
}
// after: denial surfaces as an error with auth=false; handle both
ok, err := client.CanI(ns, gvr, name, []string{"delete"})
if err != nil && !isAccessDenied(err) {
return err // real transport/timeout failure, retry or report
}
if !ok {
log.Printf("insufficient permissions: %v", err)
return nil // degrade gracefully, hide the action
}
deletePod() Defensive patterns
Strategy: try-catch
Validate before calling
// Precheck outside k9s before relying on a privileged action:
// equivalent of `kubectl auth can-i delete pods -n ns`
func canIDelete(dial kubernetes.Interface, ns, name string) (bool, error) {
sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: ns, Verb: "delete",
Group: "", Resource: "pods", Name: name,
},
},
}
resp, err := dial.AuthorizationV1().SelfSubjectAccessReviews().
Create(context.TODO(), sar, metav1.CreateOptions{})
if err != nil {
return false, err
}
return resp.Status.Allowed, nil
} Type guard
func isAccessDenied(err error) bool {
return err != nil && strings.Contains(err.Error(), "access denied for user on resource")
} Try / catch
ok, err := client.CanI(ns, gvr, name, verbs)
switch {
case err != nil && isAccessDenied(err):
// RBAC denial: hide/disable the action, log at info, do not retry
case err != nil:
// transport failure (timeout, API error): surface or retry with backoff
default:
if !ok {
// cached or explicit denial without error: degrade gracefully
}
// allowed: proceed
} Prevention
- Run kubectl auth can-i --list -n <ns> once per environment to know your effective permissions before building flows on them
- Treat CanI's error return as a denial signal, not a crash: always check both auth and err
- Remember results are cached for a short expiry — after changing RBAC, wait or restart before re-testing
- Keep launch scripts' --as/--as-group impersonation flags in sync with the RBAC you actually granted
When it happens
Trigger: Calling CanI(ns, gvr, name, verbs) where the authenticated user (or the impersonated identity from --as/--as-group flags) holds no Role/ClusterRoleBinding granting that verb, e.g. verbs=["delete"] on a pod gvr. Also triggered when the active kubeconfig context points at a different, restricted identity than expected, or when checking helm GVRs (mapped to secrets) without secrets permissions.
Common situations: Browsing a cluster with a read-only service account; switching to a context with restricted permissions and pressing a destructive key; new team members missing RoleBindings; RBAC tightened after k9s started (stale cached 'true' masks it until expiry, then this error appears).
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- user is not authorized to list node metrics
- user is not authorized to list pods metrics
- user is not authorized to list pod metrics
- user is not authorized to run jobs
- user is not authorized to (un)suspend cronjobs
AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15).
Data as JSON: /api/errors/19faef3dfa22a35c.
Report an issue: GitHub.