RBAC questions in interviews rarely stop at “what’s the difference between a Role and a ClusterRole”. The follow-up is usually a scenario: a CI pipeline needs to deploy, a monitoring agent needs to read metrics, a developer needs to debug. The good answer grants exactly that and can explain which innocent-looking permissions would give away much more. This guide builds a deployer identity step by step and then looks at the grants that escalate.
The manifests target Kubernetes 1.30+ and parse as valid YAML. The can-i answers and error text show what kubectl returns for these rules; they weren’t captured from a real cluster.
How a request gets authorised
Every API request carries an identity (a user, a group or a ServiceAccount) and asks to do a verb (get, list, watch, create, update, patch, delete, deletecollection) on a resource (deployments, pods, secrets) in an API group (apps, "" for the core group, batch), optionally in a namespace and optionally on a named object.
RBAC answers with four kinds of object:
- Role: a list of rules valid in one namespace.
- ClusterRole: the same, but not tied to a namespace. Can also cover cluster-scoped resources like nodes, and non-resource URLs like
/healthz. - RoleBinding: grants a Role or ClusterRole to subjects, inside the binding’s namespace.
- ClusterRoleBinding: grants a ClusterRole to subjects across every namespace.
There is no deny. If any binding anywhere allows the request, it’s allowed. So you can’t take a permission away from someone by adding a rule; you have to find and remove the binding that grants it. This is why auditing RBAC means listing bindings, not reading one Role.
The combination that confuses people is ClusterRole plus RoleBinding. It grants the ClusterRole’s rules but only within the RoleBinding’s namespace. It lets you define view, edit or a custom deployer once and bind it per namespace, rather than copying a Role into each namespace.
A deployer for one namespace
The CI system deploys the checkout app into shop with kubectl apply and waits for kubectl rollout status. Start with what it actually does, then write rules for exactly that:
- apply a Deployment, Service and ConfigMap:
get,create,patch(server-side apply usespatch) - wait on the rollout:
get,list,watchon deployments and replicasets, andlist/watchon pods for status - nothing with Secrets, nothing outside
shop
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: shop
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: shop
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "patch"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "create", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
namespace: shop
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: shop
roleRef:
kind: Role
name: deployer
apiGroup: rbac.authorization.k8s.ioA few deliberate choices:
- No
delete. Removing a resource from the repo shouldn’t silently delete it in production. If the pipeline needs pruning, adddeletewith intent. - No
update. Apply usespatch;updatewould let the pipeline replace whole objects. - No wildcards.
resources: ["*"]would includesecrets,rolesand anything a CRD adds later. A rule that seems tidy today grows silently. automountServiceAccountToken: false. This identity is used from outside the cluster via a short-lived token (kubectl create token ci-deployer -n shop --duration=15m, or the CI platform’s workload identity). No Pod needs it mounted.
Then check it, using impersonation. You need permission to impersonate, which cluster admins have:
$ SA=system:serviceaccount:shop:ci-deployer
$ kubectl auth can-i patch deployments -n shop --as=$SA
yes
$ kubectl auth can-i delete deployments -n shop --as=$SA
no
$ kubectl auth can-i get secrets -n shop --as=$SA
no
$ kubectl auth can-i patch deployments -n payments --as=$SA
no
$ kubectl auth can-i --list -n shop --as=$SA
Resources Non-Resource URLs Resource Names Verbs
deployments.apps [] [] [get list watch create patch]
replicasets.apps [] [] [get list watch]
configmaps [] [] [get create patch]
services [] [] [get create patch]
pods [] [] [get list watch]
...The ... stands for permissions every authenticated identity gets from built-in ClusterRoles like system:basic-user (for example, creating selfsubjectaccessreviews, which is what can-i itself uses).
When the pipeline fails with a missing permission, the error names exactly what’s missing:
Error from server (Forbidden): deployments.apps "checkout" is forbidden:
User "system:serviceaccount:shop:ci-deployer" cannot delete resource "deployments"
in API group "apps" in the namespace "shop"Read it as verb, resource, group, namespace, and add exactly that, or decide the pipeline shouldn’t be doing it.
Grants that are bigger than they look
This is where least privilege usually fails, and it’s what a security-minded interviewer will push on.
create pods (or deployments, jobs, cronjobs). A Pod can mount any Secret in its namespace and can run as any ServiceAccount in its namespace. So anyone who can create Pods in shop can read every Secret in shop and act with every ServiceAccount’s permissions there. The Pod is the escalation path, not the Secret rule. That’s why the deployer above is still a sensitive identity, and why Pod Security Admission matters: without it, the same person can create a privileged Pod with a hostPath mount and reach the node.
list secrets, or watch. get on a named Secret with resourceNames is narrow. list returns every Secret’s contents in the namespace in one call; there’s no metadata-only list in RBAC.
pods/exec and pods/attach. These are subresources, so get pods doesn’t include them, but granting them is a shell inside the container with its environment variables (often credentials) and its ServiceAccount token. Debug access for developers usually means pods/log (read logs) and maybe pods/portforward, and exec only in non-production namespaces.
escalate, bind and impersonate. RBAC normally stops you creating a Role with permissions you don’t hold. The escalate verb on roles lifts that check; bind lets you bind roles you don’t hold; impersonate lets you act as any user, group or ServiceAccount. Treat all three as admin.
Editing RoleBindings at all. If an identity can create or patch RoleBindings in a namespace, it can bind itself the namespace’s admin ClusterRole (subject to the escalation check, which bind bypasses).
resourceNames narrows a rule to specific objects, which is useful for “read the feature-flags ConfigMap and nothing else”. It can’t restrict create or list, because at the time of those requests the object name isn’t known (create) or there are many (list).
Interview practice
“A developer asks for the built-in edit ClusterRole in the production namespace so they can read logs while debugging incidents. What would you grant instead, and why?”
Answer
edit is much more than reading logs. It allows creating and modifying most namespaced workload resources, including Pods and Deployments, which means it can read every Secret in production by mounting it, and it includes pods/exec, which is a shell in production containers. It doesn’t include Roles or RoleBindings, but it already covers more than incident debugging needs.
I’d create a small ClusterRole, say log-reader, with get, list and watch on pods, get on pods/log, and read access to events and deployments/replicasets so they can see rollout state, then bind it with a RoleBinding in production to the developer’s group rather than the individual. I’d verify with kubectl auth can-i get pods --subresource=log -n production --as=... --as-group=... and confirm create pods/exec returns no. If incidents occasionally need exec, that should be a time-limited, audited elevation (a break-glass group, or a just-in-time access tool), not a standing grant.