RBAC: Turning Identity Into Permission
Article 51 ended at the authentication stage: the API server attaches a username + groups to the request, but grants no permission. The authorization stage decides that, and our cluster runs in Node,RBAC mode (see the --authorization-mode flag in Article 7). This article focuses on RBAC — how to turn a username or group into concrete permissions on each resource.
Four objects, two axes
RBAC has four object types, split along two axes: define permission and bind permission to someone, each axis having a namespaced and a cluster-scoped variant.
DEFINE PERMISSION BIND PERMISSION TO WHO
(what may be done on what) (which user/group/SA)
namespaced Role RoleBinding
(permission in 1 namespace) (binds within 1 namespace)
cluster ClusterRole ClusterRoleBinding
(cluster-scoped resources (binds cluster-wide)
or reused across ns)
A rule in a Role has three parts: apiGroups (the API group, "" is core), resources (resource types like pods, secrets), and verbs (get, list, watch, create, update, patch, delete). RBAC is purely additive — there's no "deny" rule. The default is deny-all; permission comes only from allow rules. The consequence: to block someone, you simply withhold the permission rather than writing a deny rule.
A ServiceAccount that only reads pods
Stand up an SA, a Role to read pods, and a RoleBinding tying the two together:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {namespace: rbac-demo, name: pod-reader}
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {namespace: rbac-demo, name: reader-binding}
subjects:
- kind: ServiceAccount
name: reader
namespace: rbac-demo
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
kubectl auth can-i checks permission without actually calling; --as asks on behalf of another identity (requires impersonate permission — admin has it):
SA=system:serviceaccount:rbac-demo:reader
kubectl auth can-i list pods --as=$SA -n rbac-demo
kubectl auth can-i create pods --as=$SA -n rbac-demo
kubectl auth can-i list secrets --as=$SA -n rbac-demo
kubectl auth can-i list pods --as=$SA -n default # a different namespace
yes # list pods in rbac-demo
no # create pods — the Role only allows get/list/watch
no # list secrets — the Role doesn't mention secrets
no # list pods in default — the Role only applies in rbac-demo
can-i is just a prediction; verify with reader's real token (how to get a token is in Article 51) to see the API server actually block it:
TOK=$(kubectl -n rbac-demo create token reader)
kubectl --kubeconfig=/dev/null --token="$TOK" --server=https://<apiserver> \
--certificate-authority=ca.pem -n rbac-demo get pods
kubectl ... get secrets
kubectl ... run x --image=busybox
No resources found in rbac-demo namespace. # list pods: allowed (just no pods yet)
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:rbac-demo:reader"
cannot list resource "secrets" in API group "" in the namespace "rbac-demo"
Error from server (Forbidden): pods is forbidden: ... cannot create resource "pods" ...
Token enforcement matches can-i exactly: listing pods works (returns empty because the namespace is empty), listing secrets and creating pods both 403 Forbidden with a message naming the username, verb, resource, namespace — the four things RBAC decides on.
How the authorizer decides
Why do can-i and the token give the same result? Both ask the same machinery: the authorizer chain that Article 7 configured via --authorization-mode=Node,RBAC. A request is allowed if any authorizer in the chain (Node, then RBAC) approves; if all of them have "no opinion," it's denied. That's why RBAC is purely additive: it has no concept of "deny," only "approve" or "no opinion."
Internally, the RBAC authorizer does one simple thing: for the request's identity (user + groups + service account), it scans every RoleBinding/ClusterRoleBinding pointing at that identity, gathers the rules from the referenced Role/ClusterRole, and if one rule matches all three (apiGroup + resource + verb), it approves immediately. No rule matches → "no opinion" → deny. Ask the authorizer directly with a SubjectAccessReview to see what it decides and why:
kubectl create -f - -o jsonpath='{.status.allowed} reason={.status.reason}' <<'EOF'
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
user: system:serviceaccount:rbac-demo:reader
resourceAttributes: {namespace: rbac-demo, verb: list, resource: pods}
EOF
true reason=RBAC: allowed by RoleBinding "reader-binding/rbac-demo" of Role "pod-reader" to ServiceAccount "reader/rbac-demo"
The authorizer returns true and names exactly which binding allows it — exactly the path it walks: from identity → binding → role → matching rule. Ask about secrets (no rule matches):
false reason=
false with an empty reason — because there's no rule to point to, and RBAC doesn't explain a denial, it simply stays silent and doesn't approve. (kubectl auth can-i internally sends a SelfSubjectAccessReview just like this; it's only a convenient wrapper for the same question.) Once you understand this mechanism, the two properties "additive" and "default deny" stop being axioms to memorize and become direct consequences of the scan-and-match algorithm.
RoleBinding pointing at a ClusterRole
You don't always need to write a new Role. Kubernetes ships four built-in ClusterRoles: cluster-admin (full power), admin (full power within a namespace), edit (read-write most resources), view (read-only). A RoleBinding can point at a ClusterRole instead of a Role — in that case it grants that ClusterRole's permissions but only within the RoleBinding's namespace:
kubectl -n rbac-demo create rolebinding view-binding \
--clusterrole=view --serviceaccount=rbac-demo:viewer
Check viewer's permissions:
SA=system:serviceaccount:rbac-demo:viewer
kubectl auth can-i get pods --as=$SA -n rbac-demo # yes
kubectl auth can-i list services --as=$SA -n rbac-demo # yes
kubectl auth can-i list secrets --as=$SA -n rbac-demo # no
kubectl auth can-i create pods --as=$SA -n rbac-demo # no
kubectl auth can-i get pods --as=$SA -n default # no
yes # read pods
yes # read services
no # CANNOT read secrets — even as "view"
no # view is read-only, no create
no # the RoleBinding is scoped to rbac-demo, doesn't reach default
view allows reading almost everything but deliberately leaves out secrets — because reading a secret is nearly reading the passwords, tokens, keys. Double-check by inspecting the view ClusterRole's rules:
kubectl get clusterrole view -o jsonpath='{range .rules[*]}{.resources}{"\n"}{end}' | grep secrets
(prints nothing — view has no secrets)
The fact that viewer doesn't reach beyond rbac-demo shows an easily-confused point: the same ClusterRole view, but how it's bound decides the scope. Bound by a RoleBinding, the permission is confined to one namespace; bound by a ClusterRoleBinding, it applies cluster-wide. The cluster-admin ClusterRoleBinding from Article 51 is the latter example — it grants cluster-wide to the group system:masters.
🧹 Cleanup
kubectl delete namespace rbac-demo
Deleting the namespace takes the Role, RoleBinding, and two ServiceAccounts with it. The built-in ClusterRoles (view, edit...) are cluster-provided objects, left untouched. The commands used here are at github.com/nghiadaulau/kubernetes-from-scratch, folder 52-rbac.
Wrap-up
RBAC turns the identity from Article 51 into concrete permissions via four objects on two axes: Role/ClusterRole define permission (apiGroups + resources + verbs), RoleBinding/ClusterRoleBinding bind permission to a subject (User, Group, ServiceAccount). RBAC is additive and default-deny, so to restrict someone you remove permission rather than write a deny rule. We stood up an SA that can only get/list/watch pods, confirmed with both kubectl auth can-i and a real token that it reads pods but reading secrets or creating pods both return 403, and the permission doesn't spill into another namespace. A RoleBinding can point at a built-in ClusterRole (view) to grant permission confined to one namespace; view deliberately excludes secrets. The core point: the same ClusterRole, bound by a RoleBinding is confined to a namespace, bound by a ClusterRoleBinding is cluster-wide.
Both Articles 51 and 52 revolve around ServiceAccount without dissecting it. Article 53 digs into it directly: how tokens are issued and rotated, by what mechanism a pod receives a token automatically, and how tightly the audience/expiry in the JWT bind the token.