A workload's ServiceAccount is getting HTTP 403 Forbidden from the Kubernetes API server. How do you determine exactly which permission is missing and which binding should grant it?
answer
- 403 text names user, verb, resource, apiGroup, namespace
- identity form: system:serviceaccount:<ns>:<name>
- kubectl auth can-i --list --as ... = effective set
- no reverse index — enumerate bindings and grep subjects
- wrong SA usually = missing serviceAccountName
basics
~20 sRead the 403 message — it names the user, verb, resource and namespace. Reproduce it with kubectl auth can-i <verb> <resource> -n <ns> --as system:serviceaccount:<ns>:<sa>, and list the whole effective set with kubectl auth can-i --list --as that identity. Then add the narrowest rule, usually a RoleBinding in that namespace.
solid answer
~60 s1. **Read the error.** The API server's 403 is explicit: `User "system:serviceaccount:orders:worker" cannot list resource "secrets" in API group "" in the namespace "orders"`. That is the verb, resource, apiGroup and namespace you need — no guessing. 2. **Reproduce with impersonation.** `kubectl auth can-i list secrets -n orders --as system:serviceaccount:orders:worker`. ServiceAccount identities always take the form `system:serviceaccount:<namespace>:<name>`. Impersonating requires the `impersonate` verb, which cluster admins have. 3. **See the whole picture.** `kubectl auth can-i --list -n orders --as system:serviceaccount:orders:worker` prints every effective rule, which usually reveals that the SA has a different role than you assumed, or none because the Deployment omitted `serviceAccountName` and got `default`. 4. **Find the bindings.** Grep RoleBindings and ClusterRoleBindings for that subject; `kubectl get rolebindings,clusterrolebindings -A -o json | jq` over `.subjects` is the reliable way. 5. **Fix narrowly.** Add the missing verb/resource — including subresources like `pods/log` — to an existing role, or create a RoleBinding in that namespace. Prefer a Role over a ClusterRole; use `resourceNames` for Secrets. Watch for identity errors, not permission errors: the anonymous or wrong SA usually means a missing `serviceAccountName`.
code
bash · 19 lines# 1. what identity and what action?
kubectl logs deploy/worker -n orders | grep -i forbidden
# 2. reproduce the decision
kubectl auth can-i list secrets -n orders \
--as system:serviceaccount:orders:worker
# 3. see everything that identity can do here
kubectl auth can-i --list -n orders \
--as system:serviceaccount:orders:worker
# 4. which bindings mention it?
kubectl get rolebindings,clusterrolebindings -A -o json \
| jq -r '.items[] | select(.subjects[]?.name=="worker")
| "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name) -> \(.roleRef.name)"'
# 5. confirm the pod is even using the SA you think
kubectl get pod -n orders -l app=worker \
-o jsonpath='{.items[0].spec.serviceAccountName}{"\n"}'go deeper
Read the 403, know the ServiceAccount identity format, and run kubectl auth can-i to confirm before changing anything.
Add the effective-permissions listing, enumerating bindings by subject, and the common causes: wrong SA, missing subresource, wrong apiGroup, get versus list.
Fix narrowly and verify by re-running the effective-set check, and treat surplus permissions found along the way as a finding, since RBAC unions with no deny.
Talk about making this rare: permissions generated from what the controller calls, periodic effective-permission audits, alerting on new ClusterRoleBindings, and a policy that emergency grants carry an owner and expiry.
## The error already contains the answer Kubernetes 403 messages are unusually good. A typical one: ``` Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:orders:worker" cannot list resource "secrets" in API group "" in the namespace "orders" ``` Four facts fall out: the **identity** (`system:serviceaccount:<ns>:<name>` for a workload; a username or `system:anonymous` for a human or a broken auth path), the **verb**, the **resource with its API group**, and the **namespace** (absent for cluster-scoped resources). Any RBAC debugging that starts by guessing has skipped reading this line. If the identity is wrong — `system:anonymous`, or `system:serviceaccount:orders:default` when you expected `worker` — the problem is authentication or the pod spec, not RBAC. The usual cause is a Deployment that never set `serviceAccountName`, so the pod got the namespace's `default` ServiceAccount, which has essentially no permissions. ## Reproducing with impersonation `--as` makes the API server evaluate the request as another subject: ``` kubectl auth can-i list secrets -n orders \ --as system:serviceaccount:orders:worker ``` It returns `yes` or `no` without performing the action. Add `--as-group` to test group-based bindings. Impersonation itself requires the `impersonate` verb on `users`/`groups`/`serviceaccounts`, which `cluster-admin` has; that is also why `impersonate` is a permission to hand out very sparingly, since holding it means being able to act as anyone. The higher-value command is the listing form: ``` kubectl auth can-i --list -n orders \ --as system:serviceaccount:orders:worker ``` This prints the subject's entire effective rule set in that namespace — every resource, verb and resourceName. Comparing it against what the workload actually calls is the fastest way to spot both the missing grant and any surplus you should remove. Since Kubernetes 1.27 there is also `kubectl auth whoami`, which reports the identity the current kubeconfig authenticates as — useful when the question is "who am I, actually?" rather than "what may I do?". ## Locating the bindings There is no built-in reverse index from subject to bindings, so you enumerate: ``` kubectl get rolebindings,clusterrolebindings -A -o json \ | jq -r '.items[] | select(.subjects[]?.name=="worker") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name) -> \(.roleRef.kind)/\(.roleRef.name)"' ``` Then `kubectl describe clusterrole <name>` or `describe role` to read the rules. Remember permissions **union** across all bindings and there is no deny, so the presence of one narrow binding does not exclude a broader one elsewhere. When something has *more* access than expected, it is because a second binding exists — commonly a ClusterRoleBinding to `edit` or an aggregated ClusterRole that grew a new label. ## Common causes, ranked 1. **Wrong ServiceAccount** — `serviceAccountName` missing from the pod template; the pod runs as `default`. 2. **Missing subresource** — `pods` granted, `pods/log` or `pods/exec` not. They are separate resource strings. 3. **Wrong apiGroup** — `deployments` under `""` instead of `apps`; a CRD's group misspelled. 4. **get without list** — `kubectl get <resource>` with no name is a `list`; controllers and informers additionally need `watch`. 5. **resourceNames plus list** — narrowing by name cannot filter a collection, so the list is denied outright. 6. **Binding in the wrong namespace** — a RoleBinding grants only in its own namespace; the SA may live elsewhere. A RoleBinding in namespace A *can* name a ServiceAccount from namespace B, and the grant applies in A. 7. **Cluster-scoped resource under a RoleBinding** — nodes, PVs and namespaces need a ClusterRoleBinding; the rules are inert otherwise. 8. **Cached token or stale binding** — `roleRef` is immutable, so an "updated" binding may in fact be a stale one that was never recreated. ## Fixing it well Grant the minimum: the exact verbs, the exact resource including subresource, in a `Role` bound by a `RoleBinding` in the workload's namespace. Reach for a ClusterRole only when the workload genuinely needs the permission in multiple namespaces or on cluster-scoped objects. For Secrets, narrow with `resourceNames` to the specific objects — remembering that this only works with `get`, not `list`. Then verify the same way you diagnosed: re-run `kubectl auth can-i --list` as the ServiceAccount and confirm the new rule appears and nothing broader crept in. Restart the workload only if it caches its permission failures; the API server picks up RBAC changes within seconds, no restart required. ## Do not The wrong fix is binding `cluster-admin` "temporarily" to unblock a deploy. Those bindings are never removed, and they are exactly what an attacker who compromises the pod inherits. If you must unblock fast, bind the built-in `view` or `edit` ClusterRole scoped by a RoleBinding to that one namespace, and file the narrowing as follow-up work.
- The 403 names the user as system:serviceaccount:orders:default even though you created a ServiceAccount called worker. What went wrong?The pod template never set serviceAccountName, so the pod was assigned the namespace's default ServiceAccount. Adding permissions to the worker ServiceAccount will change nothing until the Deployment's pod spec references it and the pods are recreated. Set spec.template.spec.serviceAccountName: worker and roll the Deployment.
- You added resourceNames to restrict a Secret grant and now list requests fail entirely. Why?resourceNames constrains a rule to specific object names, but list and watch operate over a whole collection and the authorizer cannot filter a collection by name, so a list request against a resourceNames-limited rule is denied. Grant get with resourceNames for the specific secrets, and if the workload genuinely needs to enumerate, either grant list without resourceNames on a narrower namespace or change the workload to fetch secrets by name.
- Why is binding cluster-admin to unblock a failing deploy a bad habit even 'temporarily'?Temporary bindings are rarely removed, and every pod using that ServiceAccount then holds full cluster control, so compromising the workload compromises the cluster. It also destroys the diagnostic signal — you never learn which permission was actually needed. If speed matters, bind the built-in view or edit ClusterRole with a RoleBinding limited to the one namespace and follow up by narrowing it.
saying these in an interview costs you the question
- Guessing at rules instead of reading the verb and resource out of the 403 message
- Forgetting that ServiceAccount subjects are system:serviceaccount:<ns>:<name>
- Adding permissions to a ServiceAccount the pod is not actually using
- Assuming an RBAC change needs a pod restart to take effect
- Resolving it by binding cluster-admin and never narrowing it