RBAC: Who Can Do What to the Cluster
· tech
📑 Contents
- Authentication vs authorization: K8s only governs the second gate
- RBAC’s building blocks: a Role is permissions, a Binding is glue
- Namespaced or cluster-wide: two dimensions not to confuse
- ServiceAccount: an identity for workloads
- Reflections
- Confuse authentication with authorization and you’ll never fully learn RBAC
- ”A Role belongs to nobody” is the most critical, and most counter-intuitive, point
- Least privilege isn’t fastidiousness; it’s caging the blast radius in advance
The previous ten posts were about making things “run and be reachable”. This one switches dimension: who has the right to give the cluster orders? A kubectl delete hits the API Server — how does it know who you are, and on what grounds does it let you delete? That’s the territory of RBAC (Role-Based Access Control), and the piece most worth digesting in the Cluster Architecture domain that carries the biggest weight on the CKA. The opening move is separating two things that are constantly conflated: authentication and authorization.
Authentication vs authorization: K8s only governs the second gate
The division between these two words is the foundation for understanding RBAC: authentication (authn) asks “who are you”; authorization (authz) asks “what may you do”. RBAC is purely the latter — it never verifies identity; it only decides, on the premise that “identity is known”, whether this person may perform some action.
One counter-intuitive fact: there is no “User” object in K8s at all. You don’t kubectl create user. Human identity is decided by external authentication mechanisms — client certificates, bearer tokens, cloud IAM, OIDC… After the API Server verifies, all it holds is a string of “username + groups”, and RBAC matches permissions against that string. The only identity K8s manages itself is the ServiceAccount, for programs (covered below). Remember: User / Group come from outside; only the ServiceAccount is an in-cluster object.
RBAC’s building blocks: a Role is permissions, a Binding is glue
RBAC has only four object kinds, in two pairs, and it all clicks once you see the division between “role” and “binding”:
- Role / ClusterRole: a set of permissions — “which actions on which resources”. It’s just a list of permissions; it belongs to nobody on its own.
- RoleBinding / ClusterRoleBinding: a binding — it glues a Role onto some subject.
A rule has three parts: apiGroups (which API group the resource belongs to) + resources (pods, deployments…) + verbs (get, list, watch, create, update, delete…). And RBAC has the same temperament as NetworkPolicy: allow only, no deny; rules add up as a union; the default is nothing permitted. You can only “add” permissions one at a time, until they’re just enough.
Namespaced or cluster-wide: two dimensions not to confuse
The Role pair has a namespaced / cluster split, and so does the Binding pair — these two dimensions are independent, and their combination decides “where the permission takes effect”:
One more key point: cluster-level resources that “belong to no namespace” — nodes, PersistentVolumes, namespaces themselves — can only be authorised with a ClusterRole; a Role can’t reach them. To give someone “view all nodes”, it’s always ClusterRole + ClusterRoleBinding.
ServiceAccount: an identity for workloads
Humans log in with credentials; what identity does a program running inside the cluster use (a CI bot, a controller that needs to read the K8s API)? The answer is the ServiceAccount — an identity purpose-built for workloads. Every Pod runs as some SA (the namespace’s default SA if none is specified); the API Server mounts that SA’s token into the Pod, and when the program calls the API with it, RBAC matches permissions against that SA.
So to let a Pod list Pods, the standard three steps: create a ServiceAccount → create a Role (or ClusterRole) → bind the two with a RoleBinding, then have the Pod specify that SA. The principle to hold on to here is least privilege: that default SA can do almost nothing by default, and that’s deliberate — don’t hand a workload cluster-admin to save effort; that’s leaving the keys to the whole cluster in the door.
Those “standard three steps” in YAML are exactly the three blocks in the binding-chain diagram:
apiVersion: v1
kind: ServiceAccount # ① subject: an identity for the workload
metadata: { name: ci-bot, namespace: ci }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # ② role: a set of permissions (namespaced, owned by nobody)
metadata: { name: pod-reader, namespace: ci }
rules:
- apiGroups: [ "" ] # "" = the core API group (where pods live)
resources: [ "pods" ]
verbs: [ "get", "list", "watch" ] # read only, no delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # ③ binding: glue the Role onto the subject
metadata: { name: ci-bot-can-read-pods, namespace: ci }
subjects:
- { kind: ServiceAccount, name: ci-bot, namespace: ci }
roleRef:
{ kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }
roleRef points at “which permissions to grant”, subjects at “to whom” — without this RoleBinding, pod-reader is just unowned permissions lying around, and ci-bot has nothing at all. On the Pod side, write serviceAccountName: ci-bot, and when it runs and calls the API it has only read access to pods; everything else is 403.
To check whether a permission actually exists, don’t guess; ask with kubectl auth can-i:
kubectl auth can-i delete pods # can I myself delete pods
kubectl auth can-i list nodes --as=system:serviceaccount:ci:ci-bot # impersonate an SA to test
Reflections
Confuse authentication with authorization and you’ll never fully learn RBAC
I’ve seen too many people (my earlier self included) treat “can connect to the cluster” and “can operate the cluster” as the same thing. They’re two independent gates: a certificate only proves “you are aidan”; whether aidan may delete a Production Deployment is a separate RBAC matter. Once that line was clear, a lot of odd phenomena had instant answers — 401 means identity wasn’t verified (an authentication problem); 403 means identity is fine but there’s no permission (an authorization problem), and the two are investigated in completely different directions. Now, whenever I hit a permission error, the first thing I do is check whether it’s 401 or 403, and go straight to the right gate instead of poking blindly.
”A Role belongs to nobody” is the most critical, and most counter-intuitive, point
RBAC seems to have a lot of parts when you first learn it, but the real key is understanding that a Role is just a list of permissions floating in the air; it doesn’t belong to anyone on its own. For permissions to land on a person or a program, a Binding has to glue them across. This separation of “definition” from “grant” looks verbose at first, but the payoff is large: the same pod-reader can be bound to ten people and ten SAs, with the permission definition maintained once. It’s the same security philosophy as the “allow-list, default deny, only add never subtract” I saw in NetworkPolicy — a permission system’s default must be “no”, and every opening must be an explicit, traceable binding.
Least privilege isn’t fastidiousness; it’s caging the blast radius in advance
When granting workloads permissions, my discipline is start from zero and add, not start from admin and trim. It’s especially tempting to compromise when busy — “give it broad permissions to get it running, tighten later” — and “later” never comes. But a ServiceAccount’s permissions are exactly the capability an attacker inherits the instant a Pod is compromised: grant cluster-admin, and one fallen Pod is the whole cluster fallen. It’s the permission-layer version of the “think about the blast radius first” I kept repeating in the SRE posts — the value of least privilege isn’t what it saves on ordinary days; it’s that at the moment something goes wrong, it locks the disaster inside one namespace rather than the whole cluster.