Kubernetes RBAC
Kubernetes RBAC is deny-by-default, but the foot-guns are in the grants: a wildcard verb, a Role that can create pods, or a stray cluster-admin binding quietly equals full-cluster takeover. Least privilege here is a verb-by-verb discipline, not a checkbox.
A teammate is debugging a flaky deploy and the fastest way to make the error go away is a one-liner everyone has typed at 2 a.m.: kubectl create clusterrolebinding ci-debug --clusterrole=cluster-admin --serviceaccount=ci:runner. The pipeline goes green, the ticket closes, nobody reverts it. Six months later that CI runner’s pod is popped through an unrelated app bug — and because its service-account token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token and bound to cluster-admin, the attacker doesn’t get one pod. They get every secret in every namespace, the ability to schedule a privileged pod on any node, and a path to the underlying host. RBAC was on the whole time. It said “yes” because someone told it to.
By the end of this lesson you’ll be able to read a Role/RoleBinding pair and say exactly what it grants, spot the three bindings that silently equal cluster takeover, and design a service-account that can do its job and nothing else.
The four nouns, and why two of them are the trap
Kubernetes RBAC is built from exactly four object kinds, and the entire security model lives in how you combine them.
- A Role is a namespaced list of rules: tuples of
apiGroups,resources, andverbs(get,list,watch,create,update,patch,delete). A Role grants nothing until it is bound. - A ClusterRole is the same shape but cluster-scoped — it can name cluster-wide resources (nodes, persistentvolumes), and it can be reused across namespaces.
- A RoleBinding attaches a Role (or a ClusterRole) to a subject inside one namespace.
- A ClusterRoleBinding attaches a ClusterRole to a subject across the entire cluster.
The subject is usually a ServiceAccount — the identity every pod runs as. Here is the part people miss: by default each pod gets a service-account token mounted into its filesystem, and that token is the pod’s credential to the API server. So the blast radius of any RBAC grant is “whatever code an attacker can get running in a pod that uses that service account.” RBAC is genuinely deny-by-default — a brand-new service account can do essentially nothing — but the danger isn’t the default. It’s the grants you add, because two combinations turn a small mistake into total compromise: a ClusterRoleBinding (cluster-wide, not one namespace) and the built-in cluster-admin ClusterRole (every verb on every resource, including *).
The cluster-admin foot-guns, ranked
The reason RBAC incidents look identical across companies is that the same three patterns keep appearing. Here is what each one actually grants, why it’s tempting, and the safe replacement.
| Foot-gun | What it really grants | Why it happens | Least-privilege fix |
|---|---|---|---|
| cluster-admin binding | Every verb on every resource, all namespaces — full takeover | ”Make the error go away” debugging; copy-pasted Helm values | A scoped Role with the 3-4 verbs actually used |
Wildcard verb/resource (*) | Today’s resources and every future one the operator adds | ”I don’t know the exact verbs yet, I’ll tighten later” | Enumerate verbs explicitly; never ship verbs: [”*“] |
create pods / pods/exec | Schedule a privileged pod or exec in — pivot to host/other SAs | Looks like a normal app verb; reviewers wave it through | Treat pod-create as privileged; gate with Pod Security + policy |
get/list secrets | Read every secret in scope, including other SAs’ tokens | ”The app needs one secret” → granted on the whole verb | Scope by resourceNames; prefer projected/CSI secrets |
escalate / bind on roles | Grant yourself permissions you don’t have — privilege escalation | Hidden in an operator’s ClusterRole nobody audits | Never grant escalate/bind to workloads |
The throughline: RBAC has no deny rule. You cannot “subtract” a permission after the fact — the union of all bound rules is what the subject can do, and if any rule says yes, it’s yes. That is why over-granting is so dangerous: there’s no safety net rule to catch you, so least privilege has to be enforced at write time, verb by verb. The aggregation behavior makes this worse: ClusterRoles labeled rbac.authorization.k8s.io/aggregate-to-edit: "true" get automatically merged into the built-in edit role, so a custom resource’s operator can silently widen what edit-bound users can do without you touching your bindings.
▸Common mistake
The most common real-world escalation isn’t cluster-admin directly — it’s create pods combined with no Pod Security restriction. If a service account can create a pod, an attacker who owns that SA can schedule a pod with hostPath: / mounted, or hostPID: true, read the kubelet’s credentials off the node, and pivot to every workload on that node — including ones with far more powerful tokens. That’s why “can this SA create pods?” is the single highest-signal question in an RBAC review, and why pod-create must be paired with the Pod Security Admission restricted profile to mean anything.
Designing a least-privilege service account
The senior reflex is to build grants bottom-up from observed need, not top-down from convenience. The workflow:
- Start from deny. A new ServiceAccount has no permissions. Resist the urge to bind a broad built-in role “to get unblocked.”
- Enumerate the actual verbs. Run the workload, capture which API calls it makes (audit logs, or
kubectl auth can-i --list), and grant exactly those —get/list/watchon the specific resources, nothing wildcarded. - Prefer Role over ClusterRole, RoleBinding over ClusterRoleBinding. Keep the grant in one namespace unless the resource is genuinely cluster-scoped. A namespaced grant caps the blast radius at one namespace.
- Scope secrets by
resourceNames. If the app needs one secret, name it; don’t grant the wholesecretsverb. - Turn off the token mount when unused. Set
automountServiceAccountToken: falseon pods that never call the API server — most app pods don’t, and an unmounted token is a credential that can’t be stolen. - Audit with
kubectl auth can-i. Before shipping, ask the cluster directly:kubectl auth can-i create pods --as=system:serviceaccount:app:web -n appshould returnno.
A backend pod needs to read one ConfigMap and one Secret in its own namespace to boot. CI is red because the SA can't read them. Pick the correct grant.
A Role grants `verbs: [list]` on `secrets` in one namespace, bound to a pod's ServiceAccount. The pod is compromised. What can the attacker do with the mounted token?
Why is `create pods` such a high-signal permission to flag in an RBAC review, even without an explicit cluster-admin binding?
Order these steps for designing a least-privilege service account, from first to last:
- 1 Start from deny — a fresh ServiceAccount with no bindings
- 2 Enumerate the exact verbs the workload actually calls
- 3 Grant a namespaced Role (prefer Role over ClusterRole), scoped by resourceNames
- 4 Bind it with a RoleBinding to that one ServiceAccount
- 5 Verify with `kubectl auth can-i` that nothing extra is allowed
- 01Walk through how a single cluster-admin ClusterRoleBinding on a CI service account becomes full-cluster compromise when one pod is breached.
- 02Why is `list secrets` more dangerous than it looks, and what's the least-privilege alternative?
Kubernetes RBAC is assembled from four objects — Role and ClusterRole (the verb/resource rules) plus RoleBinding and ClusterRoleBinding (which attach them to a subject, usually a ServiceAccount). The model is genuinely deny-by-default and allow-only: a fresh SA can do nothing, and the union of every bound rule is what its auto-mounted token can do, with no deny rule to claw a grant back. That’s why the foot-guns are all over-grants, not defaults: a cluster-admin ClusterRoleBinding (every verb everywhere), a wildcard verb that also covers future resources, create pods (schedule a privileged pod, pivot to the node), list secrets (read every secret including other tokens), and escalate/bind (grant yourself more). Least privilege is a write-time discipline — start from deny, enumerate the real verbs, prefer namespaced Role + RoleBinding, scope secrets by resourceNames, turn off the token mount when unused, and verify with kubectl auth can-i. Now when you see a binding in a review, your first question is: what’s the subject, is it cluster-wide, and can it create pods or read secrets?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.