How to Fix Kubernetes RBAC Forbidden: User Cannot List Resource (2026)
This guide is for anyone who just hit Error from server (Forbidden): pods is forbidden: User "..." cannot list resource "pods" in API group "" in the namespace "...", whether from kubectl, a CI job, a controller or an AI agent's tool call. By the end you will know which identity was refused and which rule is missing, and you will have a least-privilege Role and RoleBinding that fixes it, plus the EKS, GKE and AKS steps for when the cluster is not the only authorizer. Commands match the Kubernetes v1.37 documentation (latest patch v1.37.2, released 28 October 2026) and the kubectl v1.32.3 help output I checked; the RBAC v1 API is identical on every supported version. You need kubectl access to the cluster, jq for Step 4, and for Step 3 an identity allowed to impersonate, which cluster admins usually are. For the concepts behind all this, read the companion Kubernetes RBAC explainer.
Step 1: Read the forbidden error message
The API server builds every authorization denial from one template in its source, so the message already names the missing rule:
Error from server (Forbidden): deployments.apps is forbidden: User "system:serviceaccount:ci:deployer" cannot list resource "deployments" in API group "apps" in the namespace "prod"
Read it field by field:
User "..."is the identity that was refused, and the subject your binding needs.cannot listis the verb.kubectl geton a collection islist, on one objectget, and-waddswatch.kubectl applyneedsget, pluscreatefor new objects andpatchfor existing ones.resource "deployments"goes inresources. Subresources appear aspods/logorpods/exec.in API group "apps"goes inapiGroups. An empty""is the core group: pods, services, ConfigMaps, Secrets.in the namespace "prod"means you need a RoleBinding inprod.at the cluster scopemeans a cluster-scoped resource or an all-namespaces request, which needs a ClusterRoleBinding.cannot get path "/metrics"is a non-resource URL, granted withnonResourceURLsin a ClusterRole.
Two suffixes change the diagnosis. The RBAC authorizer appends its evaluation errors, so : RBAC: role.rbac.authorization.k8s.io "deployer" not found means a binding points at a role that does not exist. On GKE, a message ending requires one of ["container.roles.list"] permission(s). means neither RBAC nor IAM granted the action, per Google's troubleshooting page. And error: You must be logged in to the server (Unauthorized) is not RBAC at all: authentication failed before authorization ran.
Step 2: Confirm which identity the API server sees
A lot of RBAC debugging turns out to be fixing the wrong identity. Ask the API server rather than reading your kubeconfig:
# Which context is kubectl using?
kubectl config current-context
# Who the API server thinks you are (SelfSubjectReview API, stable since v1.28)
kubectl auth whoami
Output is similar to:
ATTRIBUTE VALUE
Username jane@example.com
Groups [developers system:authenticated]
For a workload, check which service account the pod runs as. A pod without serviceAccountName runs as default:
kubectl get pod web-7d4b9c -n payments -o jsonpath='{.spec.serviceAccountName}{"\n"}'
For a token a CI job or agent uses, pass it explicitly:
kubectl auth whoami --token="$TOKEN"
On EKS the username comes from the access entry; by default it is the assumed-role STS ARN with {{SessionName}} filled in at login, and the groups are the entry's Kubernetes groups. If the identity is not the one you expected, fix the context, AWS profile or pod spec first and stop there.
Step 3: Reproduce the denial with kubectl auth can-i
kubectl auth can-i sends a SelfSubjectAccessReview, the same question the API server asked, and works whatever authorizers are configured. With --as you ask it as the failing identity:
SA=system:serviceaccount:payments:ci-deployer
# The exact check from the error. Prints yes or no.
kubectl auth can-i list pods -n payments --as="$SA"
# Group-qualified resources and subresources
kubectl auth can-i patch deployments.apps -n payments --as="$SA"
kubectl auth can-i get pods --subresource=log -n payments --as="$SA"
kubectl auth can-i create pods --subresource=exec -n payments --as="$SA"
# Everything this identity may do in the namespace
kubectl auth can-i --list -n payments --as="$SA"
# Exit code only (0 = yes, 1 = no) for CI gates
kubectl auth can-i patch deployments.apps -n payments --as="$SA" -q && echo allowed
The --list output looks like this (abbreviated) for a service account with no grants beyond the defaults:
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
[/api/*] [] [get]
Three caveats. --as needs the impersonate permission, and for human users you may need --as-group too. The API reference warns that --list "may be incomplete depending on the server's authorization mode". And on EKS, AWS documents that --list omits access-policy permissions and that --as forces RBAC-only evaluation, so an access-policy user can be told no and still succeed for real.
If can-i says no, continue to Step 4. If it says yes and the real request still fails, jump to Steps 6 to 8.
Step 4: Find the bindings that already exist
Before writing anything new, find out what the identity already has. This lists every binding that names the service account, in any namespace:
kubectl get rolebindings,clusterrolebindings -A -o json | jq -r \
--arg ns payments --arg sa ci-deployer '
.items[]
| select(any(.subjects[]?; .kind == "ServiceAccount" and .name == $sa and .namespace == $ns))
| "\(.kind)/\(.metadata.name) ns=\(.metadata.namespace // "cluster-wide") -> \(.roleRef.kind)/\(.roleRef.name)"'
Against my test fixture (jq 1.7.1) it prints one line per binding:
RoleBinding/ci-deployer ns=payments -> Role/deployer
For a human, match .kind == "User" or "Group" instead. Then read what the role actually grants:
kubectl get role deployer -n payments -o yaml
kubectl describe rolebinding ci-deployer -n payments
If you have krew, kubectl rbac-tool policy-rules -e '^system:serviceaccount:payments:ci-deployer$' from rbac-tool flattens every rule for a subject, and kubectl who-can list pods -n payments answers the reverse question. The usual findings: the binding is in the wrong namespace; the ServiceAccount subject has no namespace; roleRef.kind says Role for something that is a ClusterRole (the built-in view, edit and admin are ClusterRoles, so --role=view points at nothing); or the role has get but not list, or "" where it needed apps.
Step 5: Write a least-privilege Role and RoleBinding
Grant exactly what the error asked for, in the namespace it named:
# payments-deployer.yaml (rbac.authorization.k8s.io/v1, Kubernetes 1.30 to 1.37)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: payments
rules:
- apiGroups: [""] # core group: the error said API group ""
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
namespace: payments # where access is granted
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: payments # where the service account lives; required
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deployer
kubectl apply -f payments-deployer.yaml
kubectl auth can-i list pods -n payments --as=system:serviceaccount:payments:ci-deployer
# yes
To generate YAML instead of writing it, use --dry-run=client. kubectl create role validates resource names against the live API, so it needs cluster access; kubectl create rolebinding does not:
kubectl create role deployer -n payments --verb=get,list,watch --resource=pods,pods/log \
--dry-run=client -o yaml > role.yaml
kubectl create rolebinding ci-deployer -n payments --role=deployer \
--serviceaccount=payments:ci-deployer --dry-run=client -o yaml
The second command printed this with kubectl v1.32.3 and an empty kubeconfig, which shows it never contacts the cluster:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
creationTimestamp: null
name: ci-deployer
namespace: payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deployer
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: payments
Three variations cover most other cases:
# Reuse a built-in ClusterRole inside one namespace (note --clusterrole, not --role)
kubectl create rolebinding ci-view -n payments --clusterrole=view \
--serviceaccount=payments:ci-deployer
# Access to another namespace: the binding lives in the target namespace,
# the subject keeps its home namespace
kubectl create rolebinding reports-reader -n analytics --clusterrole=view \
--serviceaccount=payments:ci-deployer
# Cluster-scoped resources or -A: ClusterRole plus ClusterRoleBinding
kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes
kubectl create clusterrolebinding ci-node-reader --clusterrole=node-reader \
--serviceaccount=payments:ci-deployer
Two errors can appear while applying. user "..." (groups=[...]) is attempting to grant RBAC permissions not currently held is escalation prevention: you can only grant permissions you hold, so have someone who holds them apply it, and do not solve it by handing out escalate or bind. And roleRef is immutable, so changing a binding's role fails with field is immutable; delete and recreate the binding, or use kubectl auth reconcile -f, which does that for you.
Step 6: Fix service account and token problems
If the error names system:serviceaccount:<namespace>:default, the pod has no serviceAccountName. Create a dedicated account, bind it, and set it in the pod template:
kubectl create serviceaccount web -n payments
kubectl patch deployment web -n payments \
-p '{"spec":{"template":{"spec":{"serviceAccountName":"web"}}}}'
If an in-cluster client fails with open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory, the token was never mounted: automountServiceAccountToken: false is set on the pod or its service account. Set it to true on the pod spec for workloads that genuinely call the API.
For CI jobs and agents outside the cluster, mint a short-lived token with the TokenRequest API and test it before handing it over:
TOKEN=$(kubectl create token ci-deployer -n payments --duration=15m)
kubectl auth whoami --token="$TOKEN"
kubectl auth can-i patch deployments.apps -n payments --token="$TOKEN"
The kubectl help notes the server may return a longer or shorter lifetime than requested. Better still for CI is no Kubernetes token at all: authenticate with the CI provider's OIDC token to the cloud, as in GitHub Actions OIDC with AWS, GCP and Azure, and map that role in Step 7 or 8. Old Secret-based tokens cause a different symptom: since v1.24 none are created automatically, and the legacy token cleaner invalidates unused ones after a year, which returns Unauthorized, not Forbidden.
One upgrade trap: since v1.35, WebSocket exec, attach and port-forward need create on the subresource, where get used to be enough. If kubectl exec broke after an upgrade, add this rule:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"] # enforced for WebSocket upgrades since v1.35
Step 7: Fix forbidden errors on EKS with access entries
On EKS, an IAM principal reaches Kubernetes through an access entry (the aws-auth ConfigMap is deprecated). Check what the cluster knows about the principal:
# Which IAM principal is kubectl using? (returns the assumed-role STS ARN)
aws sts get-caller-identity --query Arn --output text
# Access entries need authenticationMode API or API_AND_CONFIG_MAP
aws eks describe-cluster --name my-cluster \
--query 'cluster.accessConfig.authenticationMode' --output text
# Entry and attached policies use the IAM role ARN, not the STS ARN
aws eks describe-access-entry --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/ci-deployer
aws eks list-associated-access-policies --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/ci-deployer
Then grant access one of two ways, using AWS CLI 2.12.3 or later as the access policy docs require:
# Option A: an AWS-managed access policy, scoped to one namespace
aws eks create-access-entry --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/ci-deployer --type STANDARD
aws eks associate-access-policy --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/ci-deployer \
--access-scope type=namespace,namespaces=payments \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy
# Option B: map the role to a Kubernetes group and bind your own Role (Step 5)
aws eks create-access-entry --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/ci-deployer \
--type STANDARD --kubernetes-groups payments-deployers
kubectl create rolebinding payments-deployers -n payments --role=deployer \
--group=payments-deployers
The gotchas from the access entry docs: EKS does not check that your group names match any binding, so a typo fails silently; entries are eventually consistent and can take several seconds; and if an IAM role is deleted and recreated with the same ARN, the old entry no longer matches and must be recreated. Turning access entries on with aws eks update-cluster-config --name my-cluster --access-config authenticationMode=API_AND_CONFIG_MAP cannot be reversed to ConfigMap-only mode. If IAM itself is refusing the AWS calls, my guide to AWS AccessDenied errors covers that layer.
Step 8: Fix forbidden errors on GKE and AKS
On GKE, RBAC is checked first, and if no RBAC policy allows the action, IAM is checked. The container.clusters.get permission only lets you authenticate. The cleanest fix is a small IAM role to get in plus RBAC for the actions, with the Google account email as the user name:
gcloud projects add-iam-policy-binding my-project \
--member=user:jane@example.com --role=roles/container.clusterViewer
kubectl create rolebinding jane-view -n payments --clusterrole=view \
--user=jane@example.com
For groups, Google Groups for RBAC requires each group to be a member of gke-security-groups. If you grant through IAM instead, note that roles/container.viewer excludes Secrets and roles/container.developer includes them.
On AKS with Azure RBAC for Kubernetes authorization (preconfigured on AKS Automatic), the grant is an Azure role assignment, which can be scoped to a namespace:
AKS_ID=$(az aks show --resource-group my-rg --name my-aks --query id --output tsv)
az role assignment create --role "Azure Kubernetes Service RBAC Reader" \
--assignee jane@example.com --scope "$AKS_ID/namespaces/payments"
Microsoft says new assignments can take up to five minutes to propagate, so wait before retesting. RBAC Reader excludes Secrets; RBAC Writer can read them.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
cannot list resource "pods" in API group "" in the namespace "x" |
No binding grants list on pods in x to that identity |
Role with apiGroups: [""], resources: ["pods"], verbs: ["list"] plus a RoleBinding in x |
... at the cluster scope |
-A, a cluster-scoped resource, or a missing -n |
ClusterRole plus ClusterRoleBinding, or pass -n |
User is system:serviceaccount:<ns>:default |
Pod has no serviceAccountName |
Set serviceAccountName and bind the dedicated account |
Binding exists, can-i still says no |
Wrong namespace, subject missing namespace, or roleRef.kind: Role for a ClusterRole such as view |
Recreate the binding with --clusterrole and the full subject |
Ends in RBAC: role.rbac.authorization.k8s.io "x" not found |
Binding points at a deleted or misspelled role | Create the role or fix roleRef.name |
attempting to grant RBAC permissions not currently held |
You lack the permissions you are granting | Apply as an identity that holds them; do not grant escalate or bind |
roleRef: ... field is immutable |
Editing a binding's role | Delete and recreate it, or kubectl auth reconcile -f |
kubectl exec forbidden after upgrading to v1.35 |
WebSocket exec now needs create on pods/exec |
Add create on pods/exec |
You must be logged in to the server (Unauthorized) |
Authentication, not RBAC: expired token, no EKS access entry, invalidated legacy token | Refresh credentials, create the access entry, or use kubectl create token |
GKE: requires one of ["container.pods.list"] permission(s) |
Neither RBAC nor IAM grants the action | RoleBinding for the Google account or group, or an IAM role |
FAQ
What does API group "" mean in a Kubernetes forbidden error?
It is the core API group, which holds pods, services, ConfigMaps, Secrets, namespaces and nodes. In a Role you write it as apiGroups: [""]. Resources from other groups name their group, such as apps for deployments or batch for jobs.
Why does kubectl auth can-i say yes when my request is still forbidden?
Usually because you checked a different identity or context from the one that failed, so run kubectl auth whoami with the same kubeconfig, token or pod. Also check the subresource: kubectl logs needs get on pods/log, not just on pods. If both match, the denial is coming from outside RBAC: an admission webhook, or the EKS, GKE or AKS layer.
How do I give a service account access to another namespace?
Create the RoleBinding in the namespace being accessed and name the service account with its home namespace in the subject. kubectl create rolebinding reader -n target --clusterrole=view --serviceaccount=source:my-sa lets my-sa from source read most objects in target, with no ClusterRoleBinding.
Do I need a ClusterRoleBinding for kubectl get pods -A?
Yes. Listing across all namespaces is a cluster-scope request, so it needs a ClusterRole bound with a ClusterRoleBinding. If the person only needs a few namespaces, bind per namespace and have them pass -n.
The first thing I check in production is kubectl auth whoami, because a correct permission on the wrong identity is the quickest way to lose an afternoon here. After that I keep every Role and RoleBinding in Git beside the workload, generate them with --dry-run=client, and add kubectl auth can-i -q checks to CI so a missing verb fails the pipeline, not the deploy. The official references are the RBAC documentation, checking API access, the RBAC good practices, EKS access entries, GKE RBAC and AKS Entra ID authorization.
Related: How to Use GitHub Actions OIDC With AWS, GCP and Azure (2026), How to Enforce Pod Security Standards with Kyverno 1.19 (2026) and How to Write Kubernetes NetworkPolicies: Default Deny to Egress (2026).
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Encrypt Kubernetes Secrets in Git with SOPS and age (2026)
Step-by-step SOPS 3.13.3 and age 1.3.2 setup: generate age keys, write .sops.yaml creation rules, encrypt and edit Secrets, decrypt with Flux kustomize-controller or Argo CD via KSOPS, rotate keys safely, and fix MAC mismatch and no identity matched errors.
11 minOct 31, 2026How to Sign and Verify Container Images with Cosign Keyless (2026)
Step-by-step: sign container images with cosign v3.1.4 keyless in GitHub Actions, verify with certificate identity flags, attach SBOM and SLSA attestations, enforce signatures in Kubernetes with Kyverno 1.19 ImageValidatingPolicy or Sigstore policy-controller, and fix common errors.
11 minOct 20, 2026How to Set Up cert-manager with Let's Encrypt and Fix Errors (2026)
Install cert-manager v1.21 with Helm, create Let's Encrypt ClusterIssuers for HTTP-01 (Gateway API or Ingress) and DNS-01, then debug a Certificate stuck not ready by walking CertificateRequest, Order and Challenge, with rate limits, ARI and alerts.
14 min