OnCallReady

Lesson 17.35 · Kubernetes: Scheduling, Health & Security · 12 min read

kubectl auth can-i as a debugging tool

In plain words

Imagine you're the building manager and someone says their key card doesn't open a door. Instead of borrowing their card and walking around trying doors, you have a machine at the front desk: type in the person's name and the door, and it says "yes" or "no" instantly, exactly as the door would. It can also print everything that person's card opens.

kubectl auth can-i is that machine. It asks the API server's authorizer directly, for you or, with --as, for anyone you're allowed to impersonate, including ServiceAccounts (--as=system:serviceaccount:dev:app). --list prints all the rules an identity has in a namespace. And the exit code (0 yes, 1 no) makes it scriptable.

Ask the authorizer directly

The problem. "It says Forbidden" - but for whom, for what, and which binding is missing? Guessing and re-deploying wastes an afternoon. kubectl auth can-i asks the authorizer the exact question and answers yes or no.

What you need to know already: RBAC (17.30), ServiceAccounts (17.33), exit codes and $? (1.7).

kubectl auth can-i VERB RESOURCE sends a SelfSubjectAccessReview (a "may I do this?" question object) - or, with --as, the same question impersonating someone - and prints the authorizer's decision. Exit code 0 for yes, 1 for no - scriptable:

$ kubectl auth can-i create deployments -n prod
yes
$ kubectl auth can-i create deployments -n prod --as=jane
no
$ kubectl auth can-i list pods -n dev --as=system:serviceaccount:dev:app
yes
$ kubectl auth can-i get secrets -n dev --as=system:serviceaccount:dev:app -q; echo $?
1

--as works because you (cluster-admin) have the impersonate verb. It is the fastest way to answer the Notion question "how do you check what a specific ServiceAccount is allowed to do": you do not need its token, just its name in the system:serviceaccount:NS:NAME form.

The variations you will need:

$ kubectl auth can-i get pods --subresource=log -n dev --as=jane       # subresources
$ kubectl auth can-i create pods/exec -n dev --as=jane                 # also accepted for create
$ kubectl auth can-i get configmap/app-config -n dev --as=release-bot  # a named object (resourceNames)
$ kubectl auth can-i list pods -A --as=jane                            # cluster-wide: only ClusterRoleBindings count
$ kubectl auth can-i list nodes --as=pager --as-group=oncall           # group membership
$ kubectl auth can-i '*' '*'                                           # am I cluster-admin?
$ kubectl auth can-i get /healthz --as=jane                            # non-resource URLs

Careful with kubectl auth can-i get pods/log: kubectl reads TYPE/NAME, so that asks about a pod named "log". Use --subresource=log.

Everything an identity can do: --list

$ kubectl auth can-i --list -n dev --as=system:serviceaccount:dev:app
Resources                                       Non-Resource URLs                      Resource Names   Verbs
pods                                            []                                     []               [get list watch]
deployments.apps                                []                                     []               [get list watch]
selfsubjectreviews.authentication.k8s.io        []                                     []               [create]
selfsubjectaccessreviews.authorization.k8s.io   []                                     []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                     []               [create]
                                                [/.well-known/openid-configuration]    []               [get]
                                                [/api]                                 []               [get]
                                                [/api/*]                               []               [get]
                                                [/healthz]                             []               [get]
...

The rows you added are on top; the selfsubject* and non-resource URL rows come from the default system:basic-user, system:discovery, system:public-info-viewer and system:service-account-issuer-discovery bindings that every authenticated identity has. --list is per namespace (-n); cluster-wide grants show up in every namespace.

From "Forbidden" to the missing rule

Read the error, then write the rule in the same words:

Error from server (Forbidden): deployments.apps "api" is forbidden: User "system:serviceaccount:ci:deployer" cannot patch resource "deployments" in API group "apps" in the namespace "prod"
- apiGroups: ["apps"]          # "in API group apps"
  resources: ["deployments"]   # "resource deployments"
  verbs: ["patch"]             # "cannot patch"  (kubectl apply/set/scale send an HTTP PATCH; replace sends PUT, 9.21)
# bound by a RoleBinding in prod to ServiceAccount ci/deployer

The opposite question: who can do this?

"Who can delete pods in prod?" has no built-in kubectl answer (the kubectl who-can plugin does it; krew is kubectl's plugin installer). By hand, you check the bindings (-A = all namespaces, -o wide adds the ROLE and subject columns):

# an illustration: the ci/deployer bindings of the incident
kubectl get rolebindings,clusterrolebindings -A -o wide | grep -E 'ci|deployer'
$ kubectl get rolebindings -n prod -o custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name

and describe the roles they reference. Because RBAC is additive, every binding that matches the subject counts - including group bindings the subject belongs to.

401 or 403

error: You must be logged in to the server (Unauthorized)      <- 401: authentication failed (token/cert)
Error from server (Forbidden): ... cannot list resource ...     <- 403: authenticated, RBAC said no

A 401 is never fixed with a Role. Check the credential: expired token, deleted SA, client certificate from another cluster, a kubeconfig pointing at the wrong CA (certificate authority - the issuer your client trusts, 9.15).

What you can now do

Why it helps

This turns RBAC debugging from guesswork into one command. When a team's controller logs "Forbidden", you don't need its token; you ask can-i with its name, fix the Role, and ask again. When a security review asks "can the CI ServiceAccount read secrets in prod?", can-i answers it definitively.

It also saves you from the traps: pods/log read as a pod named "log" (use --subresource=log), cluster-wide questions where only ClusterRoleBindings count, and 401 versus 403 (a 401 is never fixed with a Role). And "how do you check what a specific ServiceAccount is allowed to do?" is a Notion question and a common interview and CKA task.

Commands in this lesson

kubectl

FAQ

How do I check what a ServiceAccount can do without its token?

Impersonate it: kubectl auth can-i list pods -n dev --as=system:serviceaccount:dev:app, or --list to see all its rules in a namespace. This works because your identity (cluster-admin, say) has the impersonate verb. You don't need the token; the name in the system:serviceaccount:NS:NAME form is enough.

Why does can-i get pods/log say no when the user can read logs?

kubectl reads TYPE/NAME, so pods/log asks about a pod named "log". Use kubectl auth can-i get pods --subresource=log -n dev --as=jane. For create, pods/exec is accepted as a subresource, but --subresource is the unambiguous form for all verbs.

Why do I see permissions in --list that I never granted?

Every authenticated identity has the default bindings system:basic-user, system:discovery, system:public-info-viewer and system:service-account-issuer-discovery. They allow creating self-subject reviews (which is how can-i itself works) and reading discovery endpoints like /api and /healthz. Your own rules are listed on top; the rest is the same for everyone.

Is there a command for "who can delete pods in prod"?

Not built into kubectl. The kubectl who-can krew plugin answers it. By hand, list the RoleBindings in the namespace and all ClusterRoleBindings, look at the roles they reference, and remember that group bindings count for every member. Because RBAC is additive, every matching binding contributes.

Can I use can-i in scripts?

Yes. It exits 0 for yes and 1 for no, and -q suppresses the output. That's useful in CI to assert that a ServiceAccount has exactly the access it should, for example failing the CI job if the deployer can get secrets in prod.

In an interview Junior

How do you check whether a user or ServiceAccount can perform an action?

Ask the authorizer directly with kubectl auth can-i - it prints yes or no and exits 0 or 1, so it is scriptable:

kubectl auth can-i create deployments -n prod
kubectl auth can-i list secrets -n dev --as=jane
kubectl auth can-i patch deployments -n prod --as=system:serviceaccount:ci:deployer
kubectl auth can-i get pods --subresource=log -n dev --as=jane
kubectl auth can-i --list -n dev --as=system:serviceaccount:dev:app

--as impersonates (you need the impersonate verb, which cluster-admin has), so you do not need the other identity's credentials. --list shows everything that identity may do in the namespace.

From a Forbidden message to the fix: write the rule in the same words - cannot patch resource "deployments" in API group "apps" in the namespace "prod" becomes apiGroups: ["apps"], resources: ["deployments"], verbs: ["patch"] in a Role in prod, bound to that ServiceAccount. Then can-i again. A 401 is never fixed with a Role.

Also asked: A controller in the cluster logs Forbidden errors. How do you fix it? · How would you find out who can delete pods in a production namespace? · What is the difference between a 401 and a 403 from the API server?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.