Troubleshooting RBAC Forbidden Errors
Three Forbidden errors from one ServiceAccount, differing by verb, by resource and by namespace. Each names every field you need, and they map one-to-one onto the lines of a Role you have not written yet.
Troubleshooting Guide 102 of 103 Intermediate
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 25 min
- Reviewed21 August 2026
Written against the versions above. The message format is stable across versions and is the primary diagnostic. Read it rather than guessing.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- The RBAC guide, for Roles, bindings and the additive model.
- Admin rights, since
--asis impersonation and needs privilege. - A scratch namespace.
-
A deliberately narrow ServiceAccount
Four commands produce an identity that can do exactly one thing:
getandlistPods int3.auth can-iconfirms both sides before anything is tested for real:yesfor listing Pods,nofor listing Deployments. That is the right habit, because it answers the question without needing to run the failing command and interpret its output.--as=system:serviceaccount:t3:limitedis impersonation. Note the format, which is fixed:system:serviceaccount:. That string is the username the API server sees, and it is what will appear in every error message and audit log entry for this identity.: Impersonation itself is a privileged operation and requires the
impersonateverb, so this works as admin and would not as an ordinary user. It is the single most useful RBAC debugging tool available, because it lets you reproduce someone else's permission failure from your own terminal without their credentials.bash Example session kubectl create serviceaccount limited -n t3serviceaccount/limited createdkubectl create role pod-lister --verb=get,list --resource=pods -n t3role.rbac.authorization.k8s.io/pod-lister createdkubectl create rolebinding limited-lists -n t3 --role=pod-lister --serviceaccount=t3:limitedrolebinding.rbac.authorization.k8s.io/limited-lists createdkubectl auth can-i list pods -n t3 --as=system:serviceaccount:t3:limitedyeskubectl auth can-i list deployments -n t3 --as=system:serviceaccount:t3:limitednokubectl get pods -n t3 --as=system:serviceaccount:t3:limited --no-headers | wc -l4Expected result
yes,no, and four Pods actually listed under impersonation. The permitted operation genuinely works, which matters: this is not a mock.Success conditionThe ServiceAccount can list Pods and not Deployments.
-
Three failures, three different fields
Now the errors, and the point of the guide. Read them side by side:
deployments.apps is forbidden: User "system:serviceaccount:t3:limited" cannot list resource "deployments" in API group "apps" in the namespace "t3" pods "client" is forbidden: User "system:serviceaccount:t3:limited" cannot delete resource "pods" in API group "" in the namespace "t3" pods is forbidden: User "system:serviceaccount:t3:limited" cannot list resource "pods" in API group "" in the namespace "default"Same identity, three different reasons, and each message names all five fields that decide an RBAC verdict:
- User - the identity. If this is not who you expected, the problem is the Pod's
serviceAccountName, not RBAC. - verb -
list,delete. The Role grantsgetandlistonly, sodeletefails. - resource -
deployments,pods. - API group -
appsfor Deployments,""for Pods. That empty pair of quotes is the core group, and it is the field people get wrong most often. - namespace -
t3versusdefault. The third error is the same verb on the same resource, failing purely because a RoleBinding's reach stops at its own namespace.
Those map directly onto a Role. To permit the second error you add
deletetoverbs; the third needs a whole new RoleBinding indefault.So the message is not a generic refusal. It is a specification of the rule you are missing, and reading it carefully is faster than any amount of experimenting.
One detail in the second error worth noticing:
pods "client" is forbiddennames the object, becausedeletetargets a specific resource. The list errors have no name because listing has no single target. When a message names an object,resourceNamesin a Role can restrict access to exactly that one, which is occasionally what you want.bash Example session kubectl get deployments -n t3 --as=system:serviceaccount:t3:limited 2>&1 | tail -2Error from server (Forbidden): deployments.apps is forbidden: User "system:serviceaccount:t3:limited" cannot list resource "deployments" in API group "apps" in the namespace "t3"kubectl delete pod client -n t3 --as=system:serviceaccount:t3:limited 2>&1 | tail -2Error from server (Forbidden): pods "client" is forbidden: User "system:serviceaccount:t3:limited" cannot delete resource "pods" in API group "" in the namespace "t3"kubectl get pods -n default --as=system:serviceaccount:t3:limited 2>&1 | tail -2Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:t3:limited" cannot list resource "pods" in API group "" in the namespace "default"Expected resultThree
Forbiddenerrors differing in exactly one field each. Note the third is a failed delete that did not happen: impersonating a restricted identity is a safe way to test destructive permissions, since the operation is refused.Success conditionYou can name which field differs in each of the three messages.
- User - the identity. If this is not who you expected, the problem is the Pod's
-
What the identity can do, in full
auth can-i --listinverts the question. Rather than testing one operation, it enumerates everything the identity is permitted, which is what you want when inheriting a cluster or auditing an account.Filtered to the interesting line:
pods [] [] [get list]The two empty brackets are
Non-Resource URLsandResource Names, both unrestricted within what is granted.[get list]is the verb set, matching the Role exactly.Three practical notes on using this command.
Filter it. The unfiltered output includes permissions every authenticated identity has (
selfsubjectaccessreviews, so it can runauth can-iabout itself) plus anything operator-installed ClusterRoles grant. Reading it raw and being surprised is normal; grep for the resource you care about.It is namespace-scoped.
--list -n t3shows permissions int3. The same identity may have different permissions elsewhere, and cluster-scoped grants appear separately.It answers about the authoriser's decision, not about your Roles. So it accounts for every binding that applies, including ones in other namespaces and ClusterRoleBindings you did not know about. That makes it authoritative in a way that reading Role YAML is not.
The workflow this all adds up to, when someone reports a permission problem:
- Read the Forbidden message and note the five fields.
- Confirm with
auth can-ithat you can reproduce it.-n --as= auth can-i --list --as=to see what they do have.- Add the missing rule, and re-test with step 2 before telling them to try again.
Step 2 is the one people skip, and it is the one that catches the case where the identity in the message is not the identity you assumed.
bash Example session kubectl auth can-i --list -n t3 --as=system:serviceaccount:t3:limited | grep -E 'Resources|^pods'Resources Non-Resource URLs Resource Names Verbspods [] [] [get list]Expected resultOne line matching the Role that was created. The filter is deliberate: the unfiltered list on this cluster also contains Calico CRD permissions from an operator-installed ClusterRole, which are not part of what was granted here.
Success condition
auth can-i --listshowspods [get list]and nothing more for that resource.
Troubleshooting
A Forbidden error and you do not know what to add.
Why: The message is being skimmed. It contains the complete answer.
Fix:Map its five fields onto a rule: the verb goes in
verbs, the resource inresources, the API group inapiGroups, and the namespace determines where the Role and RoleBinding live.API group ""meansapiGroups: [""], the core group. Then confirm withauth can-ibefore deploying anything.The user named in the error is not who you expected.
Why: The Pod is using a different ServiceAccount from the one you granted, usually
defaultbecauseserviceAccountNamewas omitted.Fix:Check it:
kubectl get pod <name> -o jsonpath='{.spec.serviceAccountName}'. An omitted field meansdefault, and every namespace has one with no permissions. This is the most common RBAC problem that is not an RBAC problem.A Role that looks correct grants nothing.
Why: Wrong
apiGroups, or no RoleBinding at all.Fix:Both are silent failures.
kubectl get rolebinding -n <ns>first, since a Role alone does nothing. Then confirm the group:kubectl api-resources | grep <resource>shows it. Deployments areapps, Jobs arebatch, Ingress isnetworking.k8s.io, and Pods, Services and Secrets are"".kubectl logsorkubectl execis forbidden whileget podsworks.Why: Those are subresources and are named separately.
Fix:Add them explicitly:
resources: ["pods", "pods/log", "pods/exec"]. The verb forexeciscreate, notget, which is a second thing to get right. Be awarepods/execis effectively code execution in any container in scope, so it does not belong in a read-only role.--asfails with a Forbidden error about impersonation.Why:
--asneeds theimpersonateverb, which is privileged because impersonating anyone is equivalent to being them.Fix:Run it as an admin identity. If you cannot, test from inside a Pod with the ServiceAccount's own token against
https://kubernetes.default.svc, which needs no special permission because the Pod is asking about itself. The RBAC guide shows that method.You added a Role to restrict an account and it still has too much access.
Why: RBAC is purely additive. There are no deny rules, so adding a narrow Role cannot subtract anything.
Fix:Find and remove the binding that grants the excess:
kubectl get clusterrolebindings -o wide | grep <subject>, and the same for RoleBindings in each namespace. Watch for bindings to the groupssystem:authenticatedorsystem:unauthenticated, which apply to identities nobody explicitly granted.A 401 rather than a 403.
Why: 401 is authentication and RBAC never got involved. An expired credential, or no credential presented at all.
Fix:Different fault entirely: a 403 means the API server knows who you are and refused; a 401 means it does not know who you are. From a Pod, check the token exists:
kubectl exec <pod> -- ls /var/run/secrets/kubernetes.io/serviceaccount/. Projected tokens are refreshed in place, so an application that read the file once at startup will eventually get 401s.