KubernetesKubernetes

Error from server (Forbidden): User cannot list resource

RBAC denied the request. How to read the message, confirm with kubectl auth can-i, and grant exactly what is missing without handing out cluster-admin.

medium fix6 min read

the kubernetes error
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:api" cannot list resource "pods" in API group "" in the namespace "production"

Error from server (Forbidden): deployments.apps is forbidden: User "dev@acme.com" cannot create resource "deployments" in API group "apps" at the cluster scope

error: failed to create clusterrolebinding: clusterroles.rbac.authorization.k8s.io "admin" is forbidden: attempt to grant extra privileges

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Ask the API server directly whether the identity is allowed

    kubectl auth can-i list pods --namespace production --as system:serviceaccount:default:api

    This runs the same authorization check the real request runs, so a "no" here confirms RBAC rather than a bug elsewhere. --as requires impersonation rights, which cluster-admin has.

  2. 2

    List everything that identity can do

    kubectl auth can-i --list --namespace production --as system:serviceaccount:default:api

    The output is the full effective permission set. Compare it against the verb and resource named in the error, remembering that get, list and watch are three separate verbs.

  3. 3

    Find the bindings that apply to it

    kubectl get rolebindings,clusterrolebindings -A -o json | jq -r '.items[] | select(.subjects[]?.name=="api") | "\(.kind) \(.metadata.namespace // "-") \(.metadata.name) -> \(.roleRef.kind)/\(.roleRef.name)"'

    No output means no binding exists for that subject, which is the most common cause. A binding that exists but is a RoleBinding in the wrong namespace is the second.

All 10 sections

RBAC denied the request. The message is unusually informative, so read every field before changing anything:

pods is forbidden: User "system:serviceaccount:default:api"
cannot list resource "pods" in API group "" in the namespace "production"
FieldValueMeaning
Identitysystem:serviceaccount:default:apiServiceAccount api in namespace default
VerblistNot get, not watch. These are separate
ResourcepodsPlural, lowercase, as in the API
API group""The core group. apps, batch etc. are others
NamespaceproductionThe request's namespace, not the account's

Note the identity is in default and the request is against production. That cross-namespace shape is the most common cause of these.

Confirm it

kubectl auth can-i list pods \
  --namespace production \
  --as system:serviceaccount:default:api
no

This runs the real authorization check, so it is authoritative. --as needs impersonation rights, which cluster-admin has.

Then see everything the identity can do:

kubectl auth can-i --list \
  --namespace production \
  --as system:serviceaccount:default:api
Resources    Non-Resource URLs   Resource Names   Verbs
pods         []                  []               [get watch]

get and watch but not list. That is the whole bug, and it is easy to miss because the three feel like one thing. kubectl get pods (plural, no name) issues a list; kubectl get pod my-pod issues a get. A Role with get alone lets you fetch a named Pod and not enumerate them.

Find the bindings

kubectl get rolebindings,clusterrolebindings -A -o json \
  | jq -r '.items[]
      | select(.subjects[]?.name=="api")
      | "\(.kind) \(.metadata.namespace // "-") \(.metadata.name) -> \(.roleRef.kind)/\(.roleRef.name)"'
RoleBinding default api-reader -> Role/pod-reader

Bound in default, and the request was against production. A RoleBinding only grants within its own namespace.

The four rules that explain most failures

1. A RoleBinding is namespaced. It grants permission only in the namespace it lives in, regardless of which namespace the subject's ServiceAccount is in. To grant access in production, the RoleBinding must be in production.

2. A ClusterRole is only cluster-wide when bound by a ClusterRoleBinding. The same ClusterRole referenced from a RoleBinding grants its rules within that one namespace. This is genuinely useful: define pod-reader once as a ClusterRole, then bind it per namespace.

3. Verbs are exact. get, list and watch are three permissions. So are create, update, patch and delete. kubectl apply needs get, patch and create, which is why apply can fail while create alone works.

4. Subresources are separate resources. pods/log, pods/exec and pods/portforward each need their own rule. A Role with full access to pods still cannot run kubectl logs.

Grant what is missing

Same-namespace ServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: pod-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: production
  name: api-pod-reader
subjects:
  - kind: ServiceAccount
    name: api
    namespace: default        # the account lives elsewhere
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

The RoleBinding is in production and the subject names its own namespace. Both parts matter.

Note that roleRef is immutable. Changing which Role a binding points at means deleting and recreating it.

Getting the apiGroup right

This is the other frequent mistake. The group in the error message is the group your rule needs:

ResourceapiGroup
pods, services, configmaps, secrets, nodes""
deployments, statefulsets, daemonsets, replicasets"apps"
jobs, cronjobs"batch"
ingresses, networkpolicies"networking.k8s.io"
roles, rolebindings"rbac.authorization.k8s.io"

kubectl api-resources prints the group for everything, which beats guessing.

The Pod is not using the ServiceAccount you think

A Pod with no serviceAccountName uses default, which has essentially no permissions.

kubectl get pod my-pod -o jsonpath='{.spec.serviceAccountName}'
spec:
  serviceAccountName: api

An existing Pod cannot be changed; the field is immutable, so the Deployment has to be updated and the Pod recreated.

"attempt to grant extra privileges"

clusterroles.rbac.authorization.k8s.io "admin" is forbidden:
attempt to grant extra privileges

This is privilege escalation prevention. You cannot grant permissions you do not hold yourself. It stops a namespace admin writing themselves a cluster-admin binding.

Two legitimate ways past it: have someone with the permissions create the binding, or hold the escalate verb on clusterroles, which should be rare and deliberate.

Before reaching for cluster-admin

kubectl auth can-i --list on your own identity shows what you actually have, and comparing that against what the workload needs usually produces a small, specific Role. Binding cluster-admin to a ServiceAccount because the error was confusing is how clusters end up with a dozen unbounded identities that nobody dares remove.

A checklist

  1. Read the identity, verb, resource, apiGroup and namespace out of the error.
  2. kubectl auth can-i <verb> <resource> -n <ns> --as <identity> to confirm.
  3. kubectl auth can-i --list --as <identity> to see the effective set.
  4. Find bindings for the subject. None → that is the cause.
  5. RoleBinding must be in the target namespace; the subject names its own.
  6. get, list, watch are distinct verbs. So are create, patch, update.
  7. kubectl logs needs pods/log as a separate resource.
  8. Check the Pod's serviceAccountName. Unset means default, which can do nothing.

Frequently Asked Questions

Why can I get a Pod but not list Pods?

Because get and list are separate RBAC verbs. kubectl get pod my-pod issues a get against a named resource; kubectl get pods issues a list against the collection. A Role granting only get therefore allows fetching a Pod whose name you already know while refusing to enumerate them. Almost every read-only Role wants all three of get, list and watch, the last because controllers and kubectl get -w open watches, and a missing watch produces failures that only appear under those specific commands.

What is the difference between a Role and a ClusterRole?

A Role is namespaced and can only contain rules for namespaced resources; a ClusterRole is cluster-scoped and can also cover cluster-scoped resources such as nodes and persistent volumes. The part people miss is that the binding decides the scope of the grant, not the role. A ClusterRole referenced from a RoleBinding grants its rules only inside that binding's namespace, which is the standard way to define a permission set once and hand it out per namespace.

Why does my ServiceAccount have no permissions even though I created a RoleBinding?

Most often the RoleBinding is in the wrong namespace. A RoleBinding grants access only within the namespace it lives in, so a binding in default does nothing for requests against production, even when the ServiceAccount itself lives in default. The binding must be created in the target namespace, with the subject naming the account's own namespace. The second common cause is that the Pod is not using that ServiceAccount at all, because serviceAccountName was never set and it silently fell back to default.

What does "attempt to grant extra privileges" mean?

Kubernetes prevents privilege escalation: you cannot create a binding that grants permissions you do not hold yourself. Without it, anyone able to create RoleBindings in their own namespace could bind themselves cluster-admin. The legitimate ways round it are to have someone who holds the permissions create the binding, or to hold the escalate verb on clusterroles, which should be granted rarely and deliberately. Seeing this error usually means the permission set you are trying to hand out is wider than your own.

How do I find out exactly what permissions a workload needs?

Start from the errors rather than guessing. Run it with a minimal Role, collect each Forbidden message, and add precisely the verb, resource and apiGroup it names. kubectl auth can-i --list --as system:serviceaccount:<ns>:<name> shows the effective set after each change. It is slower than binding cluster-admin and it produces a Role you can actually justify later, which matters because over-broad ServiceAccount bindings are among the most common findings in a cluster security review.

Reference and practice

Learn the underlying concept

Other Kubernetes errors