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"
| Field | Value | Meaning |
|---|---|---|
| Identity | system:serviceaccount:default:api | ServiceAccount api in namespace default |
| Verb | list | Not get, not watch. These are separate |
| Resource | pods | Plural, lowercase, as in the API |
| API group | "" | The core group. apps, batch etc. are others |
| Namespace | production | The 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:
| Resource | apiGroup |
|---|---|
| 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
- Read the identity, verb, resource, apiGroup and namespace out of the error.
kubectl auth can-i <verb> <resource> -n <ns> --as <identity>to confirm.kubectl auth can-i --list --as <identity>to see the effective set.- Find bindings for the subject. None → that is the cause.
- RoleBinding must be in the target namespace; the subject names its own.
get,list,watchare distinct verbs. So arecreate,patch,update.kubectl logsneedspods/logas a separate resource.- Check the Pod's
serviceAccountName. Unset meansdefault, 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.