Authorization & RBAC

Roles, bindings, and least privilege.

Intermediate12 min · lesson 46 of 65
In plain terms
RBAC is a guest list of what each person is allowed to do — and it only ever says “yes.” If your name isn’t on a list for some action, the answer is automatically “no.”

kubectl get pods prints your Pods. Then kubectl get secrets, same user, same cluster, comes back with Error from server (Forbidden). Nothing is broken. That refusal is authorization doing exactly its job, and in Kubernetes the thing making the call is almost always RBAC, short for Role-Based Access Control.

By the time a request reaches the kube-apiserver (the single front door that every command, controller, and node agent has to go through), it has already been authenticated. Kubernetes knows the identity behind it: a user, a group, or a service account, which is the identity a Pod runs as. Authorization is the next gate. Think of a guest list on a clipboard where the bouncer can only add a checkmark, never cross one off. If there's no checkmark next to the action you want, you don't get in. That one-way rule matters more than any other detail here. RBAC is additive, and it has no concept of a deny.

Four objects that combine into one sentence

RBAC is four object types, and they always read the same way: this subject (a user, a group, or a service account) can do these verbs on these resources, in this scope. A Role is a set of permissions inside one namespace, and a namespace is just a named partition of the cluster, a way to keep one team's objects separate from another's. A permission is a verb (get, list, watch, create, update, delete) applied to a resource (pods, secrets, and so on). A ClusterRole is the same idea with a wider reach: it can grant access across every namespace, and it's the only way to grant access to things that don't live in a namespace at all, like Nodes or PersistentVolumes.

Roles hold no names of people. A Role is a job description, can read pods, and nothing more. The binding is where you name who gets the job. A RoleBinding hands a role to subjects inside a single namespace. A ClusterRoleBinding hands a ClusterRole to subjects across the whole cluster. So you build access in two pieces: write down a capability, then hand it to someone at a chosen scope.

pod-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: payments
name: pod-reader
rules:
- apiGroups: [""] # "" is the core API group (pods, services, secrets)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: payments
name: dev-can-read-pods
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
terminal
$ kubectl apply -f pod-reader.yaml
output
role.rbac.authorization.k8s.io/pod-reader created
rolebinding.rbac.authorization.k8s.io/dev-can-read-pods created

Two objects, and the developers group can now read Pods and their logs in payments, nowhere else. The Role gives nobody anything until the RoleBinding names a subject.

Prove it before you trust it

Never assume a binding works. Check it. kubectl auth can-i answers a yes or no question about what an identity is allowed to do, and the --as and --as-group flags let you ask it as someone else without logging in as them. This is the fastest way to catch a roleRef pointing at the wrong name, or a binding that named a User when the person actually shows up as a member of a group. One catch: asking on someone else's behalf is itself a permission. The --as flags need the impersonate verb on users and groups, which cluster-admin holds and an ordinary operator usually doesn't, so run these checks from an admin context. Without that verb the can-i command is the thing that comes back Forbidden, which is a confusing way to start debugging a Forbidden. Any identity can still ask about itself by dropping the flags.

terminal
$ kubectl auth can-i list pods --namespace payments \
--as=jordan --as-group=developers
$ kubectl auth can-i delete pods --namespace payments \
--as=jordan --as-group=developers
output
yes
no
terminal
$ kubectl auth can-i --list --namespace payments \
--as=jordan --as-group=developers
output
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
pods [] [] [get list watch]
pods/log [] [] [get list watch]

Run --list as the identity that's getting a surprise Forbidden and the picture is usually obvious in seconds. The three selfsubject rows are defaults every authenticated identity carries so it can ask the apiserver about its own access. (kubectl also lists a handful of read-only discovery URLs like /healthz and /version that every authenticated user gets, trimmed out above.) The pods rows are what your binding actually added. If they're missing, your binding isn't reaching this identity.

What the API server actually does with the request

The RBAC authorizer is a module living inside the kube-apiserver, switched on by the flag --authorization-mode, which on almost every cluster reads Node,RBAC. When a request arrives, the authorizer gathers every RoleBinding and ClusterRoleBinding that names your identity, whether by your username, one of your groups, or your service account. It collects the rules from the roles those bindings point at, then looks for a single rule that matches the verb, the resource, and the namespace you're touching. One match that allows, and the request goes through. No match, and you get default deny, which is the Forbidden from the very first paragraph. Because the whole thing is a union with no deny rule, you can't subtract access later. You narrow by granting less, not by adding a restriction on top.

Two behaviors surprise people. RBAC changes take effect right away, because the apiserver watches these objects live, so there's no restart and no cache to bust. And the built-in system roles are self-healing. On startup the apiserver reconciles them, adding back any default permission you stripped out, so an edit to a shipped role like view or edit can quietly come undone on the next restart. Make your own copy instead of touching the shipped ones.

Which pair of objects grants the access?
You need to grant access
scope decides the pair
one namespace, namespaced things like pods or secrets
Role + RoleBinding
applies only in that namespace
cluster-scoped things (Nodes, PVs) or all namespaces at once
ClusterRole + ClusterRoleBinding
applies everywhere, kube-system too
reuse one definition but keep it to a namespace
ClusterRole + RoleBinding
the RoleBinding scopes it down
The third path is the one people miss: a ClusterRole bound by a RoleBinding grants only inside that namespace, not cluster-wide.

Least privilege, and the grants that look small

Since there's no deny to save you, least privilege has to be a decision you make up front. The rule is boring and it works: the narrowest role at the narrowest scope. Prefer a namespaced Role over a ClusterRole. Delete wildcards, both verbs ["*"] and resources ["*"], because they quietly grant every future resource type too, including ones that don't exist yet. Give each workload its own service account with only what it needs, instead of letting everything ride on the namespace default service account. Treat cluster-admin as radioactive and know exactly who holds it.

Some grants are more powerful than they read. get or list on secrets hands over every credential in that scope, since a Secret's data comes back in the response, only base64-wrapped, not encrypted. create on pods is a short hop from node access, because a Pod can mount a host path or the node's own token. And three verbs, escalate, bind, and impersonate, let a subject give themselves more than they already have. The CKS course takes those apart in depth. For now, know they exist and audit for them the same way you audit cluster-admin.

A ClusterRole isn't automatically cluster-wide
The scope lives in the binding, not in the role. Bind a secret-reader ClusterRole with a RoleBinding and the holder reads Secrets in that one namespace. Bind the exact same ClusterRole with a ClusterRoleBinding and they read every Secret in every namespace, kube-system included. Teams reuse a handy ClusterRole and grab a ClusterRoleBinding out of habit, then can't explain how a monitoring agent ended up holding the entire cluster's credentials. When you audit access, read the binding kind before you read the role.

ClusterRole plus ClusterRoleBinding is global. Namespace RoleBinding to a ClusterRole is the usual least-privilege pattern for apps.

escalate, bind, and impersonate are sharp edges. Treat them like root.

can-i is mandatory before you tell an auditor the policy works. YAML without proof is hope. ClusterRole plus ClusterRoleBinding is global.

Try this

Do this on a scratch cluster where you hold cluster-admin, since the checks below impersonate a service account. Create a lab namespace with its own service account, grant it read-only access to Pods in that namespace, then ask can-i three things: the read you granted, a verb you never granted, and that same read in a namespace you never bound. You get yes, no, no. Neither no came from a deny rule, because there is no deny rule to write. The no is simply the absence of a grant.

terminal
$ kubectl create namespace rbac-lab
$ kubectl create serviceaccount reporter --namespace rbac-lab
$ kubectl create role pod-reader --namespace rbac-lab \
--verb=get --verb=list --resource=pods
$ kubectl create rolebinding reporter-reads-pods --namespace rbac-lab \
--role=pod-reader --serviceaccount=rbac-lab:reporter
terminal
$ kubectl auth can-i list pods --namespace rbac-lab \
--as=system:serviceaccount:rbac-lab:reporter
$ kubectl auth can-i delete pods --namespace rbac-lab \
--as=system:serviceaccount:rbac-lab:reporter
$ kubectl auth can-i list pods --namespace default \
--as=system:serviceaccount:rbac-lab:reporter
output
yes
no
no

Takeaway

RBAC answers authorization after identity is known. Roles name verbs on resources; bindings attach them to users or ServiceAccounts.

Quick check
01A service account in namespace payments is bound with a RoleBinding to a Role that allows only get and list on pods. Later, someone adds a ClusterRoleBinding giving that same service account the built-in edit ClusterRole. What can the service account do now?
Incorrect — No. Nothing wins by being narrower, and there is no deny rule. RBAC takes the union of every binding an identity has.
Correct — A ClusterRoleBinding applies cluster-wide and permissions are purely additive, so the tight Role restricted nothing. The account now has edit across the whole cluster.
Incorrect — No. Bindings never conflict or cancel each other. They can only ever add access, never remove it.
02You edit the built-in edit ClusterRole to remove one of its default permissions. After the next API server restart the permission is back. Why?
Incorrect — the shipped roles are editable and your change did apply; it was undone later, not blocked.
Incorrect — bindings hand roles to subjects; they don't restore permissions inside a role's own rules.
Incorrect — this isn't a database rollback; it's a deliberate reconciliation of the default roles.
Correct — the shipped system roles are self-healing, so edits to view/edit and friends quietly revert; make your own copy instead.
03A monitoring agent's service account is bound to a secret-reader ClusterRole using a ClusterRoleBinding. The team is surprised it can read Secrets in kube-system. Why can it?
Correct — scope lives in the binding, not the role; a ClusterRoleBinding grants cluster-wide, kube-system included.
Incorrect — the identical ClusterRole bound by a RoleBinding is limited to one namespace; the binding kind decided the scope here.
Incorrect — kube-system enforces RBAC like any namespace; nothing exempts it.
Incorrect — a role's name grants nothing; only its rules and the binding's scope matter.

Related