RBAC: Turning Identity Into Permission

K
Kai··6 min read

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.