Every request that reaches the Kubernetes API server — a kubectl get pods, a controller reconciling a Deployment, a pod calling the API from inside the cluster — is checked twice before anything happens: who are you? (authentication) and are you allowed to do this? (authorisation). RBAC — Role-Based Access Control — is how Kubernetes answers the second question. It is the layer that decides whether a request becomes an action or a Forbidden.
This lesson takes RBAC apart field by field. By the end you will be able to read any Role, ClusterRole, RoleBinding or ClusterRoleBinding and say exactly what it grants and to whom; you will understand ServiceAccounts and the short-lived tokens that pods now use; and you will be able to prove any permission with a single command. This is the foundational companion to the advanced Least-Privilege RBAC design lesson — here we build the mental model and cover every primitive; there we cover designing, aggregating and auditing RBAC at scale.
Learning objectives
By the end of this lesson you will be able to:
- Explain where RBAC sits in the request pipeline: authentication → authorisation → admission, and what RBAC does and does not control.
- Name the four RBAC objects and choose the right one by scope (namespaced vs cluster-wide).
- Write an RBAC rule correctly —
apiGroups,resources,subresources,verbs,resourceNames,nonResourceURLs— and avoid the wildcard trap. - Describe the three subject kinds (User, Group, ServiceAccount) and how each is authenticated.
- Create and use ServiceAccounts, understand bound/projected tokens vs legacy Secret tokens, control automounting, and attach imagePullSecrets.
- Use
kubectl auth can-i(including--as,--as-group, and--list) to verify permissions as ground truth. - Apply least-privilege patterns and recognise the common RBAC mistakes that send people reaching for
cluster-admin.
Prerequisites
You need a working kubectl and a cluster you can experiment in — a free local one is perfect (kind, minikube, or k3d). It helps to have met the core objects from earlier in the course — Pods, Deployments & Services — and to be comfortable with kubectl itself (get, describe, apply). You should also understand Namespaces, because the namespaced-vs-cluster-wide distinction is the heart of RBAC. This is the Security lesson of the Kubernetes Zero-to-Hero course; everything runs on free, local tooling with no cloud account required. Targets the current API (Kubernetes v1.30+), where RBAC has been stable (rbac.authorization.k8s.io/v1) for years.
Where RBAC sits: the request pipeline
When any client talks to the API server, the request passes through three gates in order. RBAC is only the middle one — getting this picture right prevents most confusion.
| Stage | Question | What does it | Failure shows as |
|---|---|---|---|
| Authentication | Who are you? | Validates a client certificate, bearer token, or OIDC token and produces a username + groups (or a ServiceAccount identity). Does not consult RBAC. | 401 Unauthorized |
| Authorisation | Are you allowed? | One or more authorisers decide. RBAC is the usual one; others are Node, ABAC, and Webhook. The request is allowed if any authoriser says yes. | 403 Forbidden |
| Admission control | Is the object acceptable / should it be changed? | Mutating and validating admission webhooks (and built-in controllers like ResourceQuota, Pod Security Admission) run after authorisation. | admission webhook denied the request |
Three consequences worth internalising:
- RBAC never authenticates. It receives an already-established identity (the username/groups string) and matches it. It never checks that a user “exists” — Users and Groups are not Kubernetes objects (more in Subjects below).
- Authorisation is a union of authorisers. On a managed cluster the Node authorizer grants kubelets exactly what they need for their own node; RBAC handles everyone else. A “yes” from any enabled authoriser wins, so RBAC can only grant, never deny over the top of another authoriser.
- RBAC is purely additive. Within RBAC there are no deny rules. A subject’s effective permission is the union of every binding that matches them. You reduce access by removing or narrowing bindings — never by adding a denial.
If you ever see
Forbiddenwith a clear message naming the missing verb/resource, that is the authorisation stage (RBAC). If you seeadmission webhook ... denied, authorisation already said yes and a later policy said no. They are different problems with different fixes.
The four RBAC objects: scope is everything
RBAC has exactly four object kinds, all in the API group rbac.authorization.k8s.io/v1. They split along two axes: permissions (Role/ClusterRole — what may be done) and bindings (RoleBinding/ClusterRoleBinding — who gets it). The dimension people trip over is scope, not function.
| Object | Scope | Holds | Grants permissions in |
|---|---|---|---|
Role |
namespaced | a set of rules | its own namespace only |
ClusterRole |
cluster-wide (not namespaced) | a set of rules | depends on how it’s bound; can also grant cluster-scoped resources & non-resource URLs |
RoleBinding |
namespaced | links subjects → a Role or a ClusterRole | one namespace (the binding’s own) |
ClusterRoleBinding |
cluster-wide | links subjects → a ClusterRole | every namespace + cluster-scoped resources |
A Role and a ClusterRole are inert on their own — they are just permission templates. Nothing happens until a binding ties a Role/ClusterRole to one or more subjects. A binding has two halves: a roleRef (which permission set) and a subjects list (who receives it).
Why ClusterRole exists when Role seems enough
A ClusterRole is needed for three things a namespaced Role cannot express:
- Cluster-scoped resources —
nodes,persistentvolumes,namespaces,clusterrolesthemselves,storageclasses. These do not live in a namespace, so only aClusterRole(bound by aClusterRoleBinding) can grant them. - Non-resource URLs — endpoints like
/healthz,/metrics,/version,/api. These are not REST resources; you grant them withnonResourceURLs, which only aClusterRolesupports. - A reusable permission set across many namespaces — author the rules once as a
ClusterRole, then grant them per namespace with aRoleBinding. This is the single most important RBAC pattern (see below).
The combination that surprises everyone
A RoleBinding can reference either a Role or a ClusterRole. When a RoleBinding references a ClusterRole, the subject gets those permissions only in the binding’s namespace — the cluster-wide reach of the ClusterRole is clipped to that one namespace.
| roleRef ↓ / binding → | RoleBinding (in ns team-a) |
ClusterRoleBinding |
|---|---|---|
Role (in ns team-a) |
permissions in team-a |
invalid — a ClusterRoleBinding cannot reference a namespaced Role |
ClusterRole |
permissions in team-a only (clipped) |
permissions in all namespaces + cluster-scoped resources |
This table is the most exam-tested fact in RBAC. The workhorse is the bottom-left cell: one ClusterRole named app-developer, bound by a RoleBinding in each tenant namespace — reusable, yet scoped.
# One reusable permission set...
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: app-developer
rules:
- apiGroups: ["", "apps"]
resources: ["pods", "deployments", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# ...scoped to exactly one namespace by a RoleBinding referencing it
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-a-developers
namespace: team-a # grant applies ONLY here
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole # reference the cluster role...
name: app-developer
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: "eng-team-a" # ...for this group, in this namespace only
roleRefis immutable. Once a binding is created you cannot change which Role/ClusterRole it points at — you must delete and recreate it. Thesubjectslist, by contrast, can be edited freely.
The anatomy of a rule, field by field
The permission lives inside the rules list of a Role or ClusterRole. A rule is, conceptually, apiGroups × resources × verbs, optionally narrowed by resourceNames, with subresources addressed via resources. This is where exhaustiveness matters, so here is every field.
| Field | What it does | Values | Default | When to set | Gotcha |
|---|---|---|---|---|---|
apiGroups |
Which API group(s) the resources belong to. | [""] = the core group (pods, services, configmaps, secrets, nodes…); ["apps"], ["batch"], ["networking.k8s.io"], ["rbac.authorization.k8s.io"], etc. ["*"] = all groups. |
none (required for resource rules) | Always, for resource rules. | The core group is the empty string "", not "core" and not omitting the field. Forgetting this is the #1 beginner error. |
resources |
Which resource type(s), by their plural, lowercase API name. | ["pods"], ["deployments"], ["secrets"], ["pods/log"], ["deployments/scale"], ["*"]. |
none (required, unless nonResourceURLs is used) |
Always, for resource rules. | Use the plural name as it appears in the API (pods, not Pod). Subresources are written resource/subresource and are separate grants. |
verbs |
Which actions are permitted. | See the verb table below; ["*"] = all verbs. |
none (required) | Always. | watch is separate from list; many tools need both. deletecollection is separate from delete. |
resourceNames |
Restrict the rule to named instances of the resource. | e.g. ["my-config", "tls-cert"]. |
empty = all instances | When you want “read this secret” not “read all secrets”. | Does not work with create, list, watch, or deletecollection (the name isn’t known at request time, or the verb is collection-wide). It works with get, update, patch, delete. |
nonResourceURLs |
Grant access to non-resource endpoints (not REST objects). | ["/healthz", "/metrics", "/version", "/api/*"]. |
none | Monitoring/health probes against the API server. | ClusterRole only; cannot be combined with resources in the same rule; pairs with non-resource verbs like get. |
The verbs, exhaustively
Verbs map to HTTP methods against the API. Memorise these — they are the vocabulary of every rule.
| Verb | What it allows | Maps to |
|---|---|---|
get |
Read a single named object. | GET /…/name |
list |
Read a collection (all objects of a type in scope). | GET /… (collection) |
watch |
Stream changes to a collection (open a watch). | GET …?watch=true |
create |
Create a new object. | POST |
update |
Replace an entire object. | PUT |
patch |
Partially modify an object (incl. kubectl rollout restart, label edits). |
PATCH |
delete |
Delete a single named object. | DELETE /…/name |
deletecollection |
Delete all objects of a type in scope at once. | DELETE (collection) |
Plus special, non-CRUD verbs that are real escalation paths — covered in Security notes:
| Special verb | On resource | Effect |
|---|---|---|
bind |
roles, clusterroles |
Bind that (possibly higher-privilege) role to a subject. |
escalate |
roles, clusterroles |
Create/update a role with more rights than you currently hold (bypasses the escalation check). |
impersonate |
users, groups, serviceaccounts |
Act as another subject — effectively become them. |
use |
podsecuritypolicies (legacy) / certain admission policy resources |
Permission to use a policy object. |
approve / sign |
certificatesigningrequests (subresources) |
Approve or sign CSRs. |
A crucial subtlety:
getdoes not implylist. They are independent verbs against different endpoints. A role with onlygetlets you read a pod if you already know its name, butkubectl get pods(which lists) will returnForbidden. Grant["get","list","watch"]together for normal read access. Likewise,listreturns full objects — there is no “list names only”, so anyone withlist secretscan read secret values.
Worked rule: every field in play
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: payments-operator
namespace: team-payments
rules:
# Full lifecycle on deployments in this namespace
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Scale subresource is a SEPARATE grant
- apiGroups: ["apps"]
resources: ["deployments/scale"]
verbs: ["update", "patch"]
# Read pod logs (subresource), but not exec
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# Read EXACTLY one configmap by name, nothing else
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["payments-feature-flags"]
verbs: ["get", "update", "patch"]
Read it as four independent grants OR’d together. Note that pods/log (read logs) and pods/exec (shell in) are different subresources — granting one never grants the other.
Subjects: who a binding grants to
The subjects list of a binding holds one or more of three kinds. Each is authenticated differently before RBAC ever sees it.
kind |
What it is | How it’s authenticated | apiGroup value |
Is it a real object? |
|---|---|---|---|---|
User |
A human (or external automation), identified by a username string. | Client certificate CN, OIDC token, or auth proxy header. | rbac.authorization.k8s.io |
No — just a string the authenticator asserts. |
Group |
A set of users, identified by a group-name string. | Cert O field, OIDC groups claim, or built-in groups. |
rbac.authorization.k8s.io |
No — also just a string. |
ServiceAccount |
A namespaced identity for workloads (pods, controllers). | A bearer token (bound/projected) mounted into the pod. | "" (the core group) — and you give name + namespace. |
Yes — a v1 ServiceAccount object. |
Two facts that catch people out:
- Users and Groups are not created in Kubernetes. There is no
kubectl create user. The API server trusts whatever username/groups your authenticator produces; RBAC simply matches the string. So a binding toUser: alice@corp.comworks the moment Alice authenticates as that string — even if nobody “made” Alice. - ServiceAccounts are the only subject kind that is a real, namespaced object, which is why they have
name+namespaceandapiGroup: "".
The full SA username form, which you use with impersonation and in some bindings, is:
system:serviceaccount:<namespace>:<name>
And every SA automatically belongs to two groups: system:serviceaccounts (all SAs) and system:serviceaccounts:<namespace> (all SAs in that namespace).
Built-in users and groups you’ll meet
| Identity | Kind | Meaning |
|---|---|---|
system:masters |
Group | Super-group — bound to cluster-admin by default; cluster super-user. Anyone with a cert in O=system:masters is god. |
system:authenticated |
Group | Every successfully authenticated request. |
system:unauthenticated |
Group | Anonymous requests (if anonymous auth is on). |
system:serviceaccounts |
Group | All ServiceAccounts cluster-wide. |
system:serviceaccounts:<ns> |
Group | All ServiceAccounts in one namespace. |
system:node:<name> |
User | A kubelet’s identity (handled mostly by the Node authorizer). |
Default ClusterRoles: don’t reinvent these
Kubernetes ships default (built-in) ClusterRoles so you rarely write read/write roles from scratch. The four “user-facing” ones are the ones to know.
| ClusterRole | Grants | Typical use |
|---|---|---|
view |
read-only (get/list/watch) on most resources except Secrets/roles/bindings. |
Auditors, dashboards, junior read access. |
edit |
read/write on most resources (create/update/delete), but not roles/bindings and not quota/limits. Can read Secrets (historically) and exec into pods. | Developers in their own namespace (bind with a RoleBinding). |
admin |
everything edit does plus managing Roles/RoleBindings within a namespace (but not the namespace object or resource quota). |
Namespace owner / team lead. |
cluster-admin |
everything, everywhere — all verbs on all resources in all namespaces + non-resource URLs. | Break-glass only. Bound to system:masters by default. |
These built-ins are aggregated (they absorb labelled add-on ClusterRoles automatically) — a powerful but double-edged feature covered in the advanced RBAC lesson. For most teams the pattern is: bind view/edit/admin with a RoleBinding per namespace, and reserve cluster-admin for emergencies.
Never bind
cluster-adminwith aClusterRoleBindingto a person or a workload SA “to make the error go away”. The whole point of RBAC is least privilege;cluster-admindiscards it.
ServiceAccounts: identity for workloads
A User is for humans; a ServiceAccount (SA) is for workloads. Every pod runs as exactly one SA, and that SA is how the pod authenticates to the API server (and how RBAC decides what the pod may do).
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-processor
namespace: team-payments
automountServiceAccountToken: false # see "automounting" below
A pod selects its SA with spec.serviceAccountName. If you omit it, the pod uses the namespace’s default SA.
apiVersion: apps/v1
kind: Deployment
metadata: { name: order-processor, namespace: team-payments }
spec:
template:
spec:
serviceAccountName: order-processor # else "default" is used
containers:
- name: app
image: ghcr.io/acme/orders:1.4.0
The default ServiceAccount
Every namespace gets a default SA automatically (created by a controller). Two important points:
- The
defaultSA has no RBAC permissions of its own — out of the box it cannot do anything against the API. (Its token is still a valid identity, which matters forsystem:serviceaccountsgroup bindings, but it carries no granted verbs by default.) - Best practice: never bind permissions to
default, and give each workload its own SA. Binding todefaultgrants those permissions to every pod in the namespace that didn’t pick a different SA — a broad, accidental blast radius.
Tokens: bound/projected vs the legacy Secret token
How a pod actually authenticates is the part that changed most in modern Kubernetes. There are three eras; know all three because you’ll meet old manifests.
| Token type | How obtained | Lifetime | Audience-scoped? | Auto-rotated? | Status |
|---|---|---|---|---|---|
| Bound / projected token (TokenRequest API) | The kubelet requests a token for the pod’s SA and projects it into the pod via a projected volume. |
Short (default ~1h; kubelet refreshes before expiry) | Yes (default audience = API server; can request others) | Yes | Default since v1.22+; the right way. |
| Legacy auto-created Secret token | Pre-1.24, the SA controller auto-created a kubernetes.io/service-account-token Secret and mounted it. |
Never expires (static) | No | No | Removed as default in v1.24+. SAs no longer auto-get a Secret. |
| Manually created Secret token | You create a kubernetes.io/service-account-token Secret with kubernetes.io/service-account.name annotation. |
Never expires (static) | No | No | Still possible, but discouraged — treat as a static credential. |
Inside a pod, the projected token (and CA cert + namespace) lands at:
/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
/var/run/secrets/kubernetes.io/serviceaccount/namespace
To mint a token from the command line (for testing or short-lived automation), use the TokenRequest API directly:
# Short-lived token for an SA (audience = API server by default)
kubectl create token order-processor -n team-payments
# With a custom expiry and audience
kubectl create token order-processor -n team-payments \
--duration=30m --audience=https://kubernetes.default.svc
kubectl create tokenis the modern replacement for the old trick of reading a long-lived token out of a Secret. Prefer it everywhere. If you truly need a non-expiring token (rare — e.g. an external CI system that can’t refresh), you must create the Secret explicitly; do so knowing you’ve created a static credential to guard and rotate.
Automounting the token
By default, the SA token is mounted into every pod — handy if the workload calls the API, but a needless credential for one that doesn’t (a web frontend that only serves HTTP has no reason to hold a cluster token). Anyone who lands code execution in that container gets a ready-made credential.
Control it at two levels (pod-level wins if both are set):
# On the ServiceAccount: default for all pods using this SA
apiVersion: v1
kind: ServiceAccount
metadata: { name: web-frontend, namespace: shop }
automountServiceAccountToken: false
---
# On the Pod: overrides the SA setting for this pod
apiVersion: apps/v1
kind: Deployment
metadata: { name: web-frontend, namespace: shop }
spec:
template:
spec:
serviceAccountName: web-frontend
automountServiceAccountToken: false # belt-and-braces
Rule of thumb: default to false, and switch it on only for workloads that genuinely talk to the API.
imagePullSecrets on a ServiceAccount
A ServiceAccount can also carry image pull credentials so pods using it can pull from a private registry without each pod specifying the secret. First create a docker-registry secret, then attach it to the SA:
kubectl create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=acme-bot \
--docker-password='<token>' \
-n team-payments
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-processor
namespace: team-payments
imagePullSecrets:
- name: regcred # every pod using this SA inherits this pull credential
Now any pod with serviceAccountName: order-processor can pull private images without its own imagePullSecrets. (Note imagePullSecrets is a list of {name} objects; it references docker-registry secrets, not RBAC.)
The RBAC model at a glance
The diagram traces a request from a subject (a User, Group, or a pod’s ServiceAccount) through a binding (RoleBinding for one namespace, or ClusterRoleBinding cluster-wide) to a permission set (Role inside a namespace, or ClusterRole spanning the cluster), where the matching rule — apiGroups × resources × verbs — is what finally allows or forbids the action at the API server. Read it left to right: who → via which binding → gets which rules → over which resources.
Hands-on lab: build and test RBAC end to end
Everything here runs on a free local cluster and is fully reversible. We will create a namespace, a ServiceAccount, a least-privilege Role, bind it, mint a token, and prove the boundaries with kubectl auth can-i.
0. Start a cluster
kind create cluster --name rbac-lab # or: minikube start / k3d cluster create
kubectl cluster-info
1. Namespace and ServiceAccount
kubectl create namespace team-payments
kubectl create serviceaccount ci-deployer -n team-payments
2. A least-privilege Role and a RoleBinding
Save as rbac-lab.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: team-payments
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# Deliberately NO secrets, NO pods/exec
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: team-payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deployer
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: team-payments
kubectl apply -f rbac-lab.yaml
Expected:
role.rbac.authorization.k8s.io/deployer created
rolebinding.rbac.authorization.k8s.io/ci-deployer-binding created
3. Prove the permissions with auth can-i (impersonation)
kubectl auth can-i asks the real authoriser, so it is ground truth. Impersonate the SA with --as:
# Should be ALLOWED
kubectl auth can-i create deployments -n team-payments \
--as=system:serviceaccount:team-payments:ci-deployer # -> yes
kubectl auth can-i get pods -n team-payments \
--as=system:serviceaccount:team-payments:ci-deployer # -> yes
# Should be FORBIDDEN (we never granted these)
kubectl auth can-i get secrets -n team-payments \
--as=system:serviceaccount:team-payments:ci-deployer # -> no
kubectl auth can-i create pods/exec -n team-payments \
--as=system:serviceaccount:team-payments:ci-deployer # -> no
# Wrong namespace -> the Role doesn't reach there
kubectl auth can-i create deployments -n default \
--as=system:serviceaccount:team-payments:ci-deployer # -> no
List everything the SA can do in the namespace:
kubectl auth can-i --list -n team-payments \
--as=system:serviceaccount:team-payments:ci-deployer
Impersonation itself is a privileged action. As the cluster-admin in your kubeconfig you can
--asanyone; a normal user needs theimpersonateverb. That’s whyauth can-i --asworks for you here without extra setup.
4. Mint a real token and call the API as the SA
TOKEN=$(kubectl create token ci-deployer -n team-payments --duration=10m)
# Use the token directly against the API server
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
curl -sk -H "Authorization: Bearer $TOKEN" \
"$APISERVER/apis/apps/v1/namespaces/team-payments/deployments" | head
# Same token trying secrets -> 403 Forbidden body
curl -sk -H "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/namespaces/team-payments/secrets"
The first call returns a deployment list (or an empty list); the second returns a 403 Forbidden status object — RBAC enforcing exactly what you bound.
5. Validation
kubectl describe rolebinding ci-deployer-binding -n team-payments
kubectl get role deployer -n team-payments -o yaml
You should see the binding’s roleRef → deployer and the single SA subject.
6. Cleanup
kubectl delete -f rbac-lab.yaml
kubectl delete serviceaccount ci-deployer -n team-payments
kubectl delete namespace team-payments
kind delete cluster --name rbac-lab # or: minikube delete / k3d cluster delete
Cost note: zero — everything is local. No cloud account, no charges.
Common mistakes & troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Forbidden even though “the Role looks right” |
The core API group was written as "core" or omitted. |
Use apiGroups: [""] (empty string) for pods/services/secrets/configmaps/nodes. |
kubectl get pods is Forbidden but get a named pod works |
Granted get but not list. |
Add list (and watch); these are independent verbs. |
| Role + RoleBinding created, subject still Forbidden | Role/RoleBinding live in the wrong namespace, or the SA’s namespace in the subject is wrong. | Ensure the RoleBinding is in the namespace where access is needed; verify subjects[].namespace. |
ClusterRoleBinding to a Role rejected |
A ClusterRoleBinding can only reference a ClusterRole. | Reference a ClusterRole, or switch to a RoleBinding. |
| Changed which role a binding points at, got an error | roleRef is immutable. |
Delete and recreate the binding. |
| Pod can read Secrets you never granted | Pod uses the default SA which someone bound, or you granted broad list that includes secrets. |
Give the pod its own SA; exclude secrets from broad read roles; never bind default. |
resourceNames rule ignored for kubectl get <type> |
resourceNames does not apply to list/watch/create. |
Use get with the explicit name, or accept that listing returns all. |
Cannot reach nodes / persistentvolumes |
These are cluster-scoped; a namespaced Role can’t grant them. | Use a ClusterRole + ClusterRoleBinding. |
| Old manifest reads a token from a Secret that no longer exists | Auto-created SA token Secrets were removed in v1.24+. | Use kubectl create token (TokenRequest) or projected tokens. |
The fastest debugging loop is always: reproduce the exact decision with kubectl auth can-i <verb> <resource> -n <ns> --as=<subject>, then add the specific missing verb/resource to a scoped Role — never widen to cluster-admin.
Best practices
- Author permission sets once as a
ClusterRole, scope them per namespace with aRoleBinding. This is the canonical reusable pattern. - Prefer the built-in
view/edit/adminClusterRoles bound per namespace before writing custom roles. - Enumerate
apiGroups,resources, andverbsexplicitly. Avoid*— a wildcard silently absorbs future API types, including CRDs. - Give every workload its own ServiceAccount. Never bind anything to the
defaultSA, and never reuse one SA across unrelated workloads. - Set
automountServiceAccountToken: falseby default, enabling it only for workloads that call the API. - Bind to Groups, not individuals (wire OIDC and bind the IdP group) so access lives in your identity provider with an audit trail — see the advanced lesson.
- Keep all Roles/Bindings in Git, reviewed and reconciled, so every permission change is peer-reviewed and reversible.
- Verify with
kubectl auth can-iafter every change — assert both the can and the cannot.
Security notes
RBAC is a primary security boundary, so a few risks deserve emphasis even at the fundamentals level:
list/getonsecretsis read access to secret values (they’re base64, not encrypted). A “read-only” role that includessecretscan read every mounted SA token in the namespace and then impersonate those SAs. Excludesecretsfrom broad read roles; grant specific secrets byresourceNamesonly.pods/execandpods/attachinherit the target pod’s identity — granting them is granting whatever that pod can do. Keep exec on its own, tightly bound role and audit it.escalate,bind, andimpersonateare super-powers.escalate/bindlet a subject grant themselves more than they hold (bypassing the built-in escalation check);impersonatelets them become anyone. Treatcreateonclusterroles/rolebindingsas privileged too.createonpodsplus a privileged SA in the namespace lets a subject launch a pod that mounts that SA’s token (or a hostPath/privileged pod) — an indirect escalation. Scope pod-creation carefully in shared namespaces.- The mounted SA token is a credential. Disable automount where unused; prefer short-lived bound tokens over static Secret tokens; never paste tokens or
kubectl get secret -o yamloutput into logs or tickets. system:mastersandcluster-adminare the keys to the kingdom. Keep exactly one audited break-glass path and alert on any new binding to either.
These threads are developed fully in Least-Privilege RBAC design (aggregation footguns, escalation-path hunting, continuous auditing) and in Pod Security Admission.
Interview & exam questions
-
What is the difference between authentication and authorisation in Kubernetes, and where does RBAC sit? Authentication establishes who you are (cert/OIDC/token → username + groups) and fails with
401. Authorisation decides whether you may act and fails with403. RBAC is an authoriser in the authorisation stage; admission control runs after, and is a separate concern. -
Role vs ClusterRole — when must you use a ClusterRole? Use a ClusterRole for cluster-scoped resources (nodes, PVs, namespaces, the RBAC objects themselves), for non-resource URLs (
/healthz,/metrics), or to define a reusable permission set you’ll bind per namespace. -
A RoleBinding references a ClusterRole. What does the subject get? Only the permissions of that ClusterRole within the binding’s namespace — the cluster-wide reach is clipped to that one namespace. This is the workhorse multi-tenant pattern.
-
Can a ClusterRoleBinding reference a Role? No. A ClusterRoleBinding can only reference a ClusterRole. A namespaced Role can only be referenced by a RoleBinding.
-
What are the components of an RBAC rule?
apiGroups×resources×verbs, optionally narrowed byresourceNames, ornonResourceURLs(+ verbs) for non-resource endpoints. The core group is the empty string"". -
Does
getimplylist? No. They are separate verbs on different endpoints.getreads one named object;listreads a collection.kubectl get <type>needslist. -
Is RBAC additive, and can you write a deny rule? RBAC is purely additive — effective permission is the union of all matching bindings. There are no deny rules; you reduce access only by removing/narrowing bindings.
-
What is the default ServiceAccount and what can it do? Every namespace has a
defaultSA, used by any pod that doesn’t name another. It has no RBAC permissions by default. Best practice: give workloads their own SA and never bind todefault. -
How do modern pods authenticate to the API server, and how did this change in v1.24? Via short-lived, audience-scoped bound/projected tokens from the TokenRequest API, auto-rotated by the kubelet. Before v1.24, the SA controller auto-created a non-expiring
service-account-tokenSecret; that auto-creation was removed in v1.24. -
How do you mint a token for a ServiceAccount from the CLI, and why prefer it?
kubectl create token <sa> -n <ns> [--duration --audience]. It returns a short-lived bound token instead of a static secret, so there’s no long-lived credential to leak. -
What does
automountServiceAccountToken: falsedo and when should you set it? It stops the SA token from being mounted into the pod. Default it tofalse; enable it only for workloads that actually call the API, reducing the credential blast radius if a container is compromised. -
Name three RBAC verbs/permissions that are privilege-escalation paths.
escalateandbindon roles/clusterroles (grant yourself more than you hold / bind a high-priv role), andimpersonateon users/groups/serviceaccounts (become another subject).list/getonsecretsis an underrated one too.
Quick check
- Which API group string covers pods, services, configmaps and secrets?
- You created a
Role+RoleBindingindefault, but your workload runs inteam-aand getsForbidden. Why? - True or false: a
RoleBindingcan reference aClusterRole. - Where does a pod’s projected ServiceAccount token appear inside the container?
- Which command proves, as ground truth, whether a specific subject may delete secrets in
team-a?
Answers
- The core group — the empty string
"". - A
Role/RoleBindingonly grant in their own namespace. They must live inteam-a(or be a ClusterRole bound via a RoleBinding inteam-a). - True. The subject then gets that ClusterRole’s permissions only in the binding’s namespace.
/var/run/secrets/kubernetes.io/serviceaccount/token(alongsideca.crtandnamespace).kubectl auth can-i delete secrets -n team-a --as=<subject>(e.g.--as=system:serviceaccount:team-a:ci).
Exercise
In a fresh local cluster, build a read-only auditor and a scoped operator, then prove the boundaries:
- Create namespace
shopand two ServiceAccounts:auditorandoperator. - Bind
auditorto the built-inviewClusterRole using a RoleBinding scoped toshop(not a ClusterRoleBinding). - Write a custom
Roledeploy-managergranting full lifecycle ondeploymentsanddeployments/scale, plus read onpods/pods/log, and bind it tooperator. - Using
kubectl auth can-i --as, assert:auditorcanlist podsbut cannotcreate deploymentsand cannotget secrets;operatorcanpatch deploymentsandupdate deployments/scalebut cannotget secretsorcreate pods/exec. - Mint a 5-minute token for
operatorwithkubectl create tokenand confirm it can list deployments but is403on secrets viacurl. - Set
automountServiceAccountToken: falseon theauditorSA and verify a pod using it has no token file mounted. - Clean everything up.
Bonus: try to bind operator to a Role via a ClusterRoleBinding and observe the rejection — then explain why.
Certification mapping
| Exam | Where this maps |
|---|---|
| CKA | “Security → RBAC” objective — create Roles/ClusterRoles and bindings, use auth can-i. Core, frequently tested. |
| CKAD | Application security — ServiceAccounts for pods, serviceAccountName, token mounting, imagePullSecrets. |
| CKS | Cluster setup & hardening — least-privilege RBAC, minimising SA tokens, restricting secrets/exec, escalation paths (deepened in the advanced lesson). |
| KCNA | Cloud-native security fundamentals — recognising the RBAC model and the four objects. |
Glossary
- RBAC — Role-Based Access Control; the API server’s authorisation mechanism.
- Authentication — establishing identity (username + groups); fails with
401. - Authorisation — deciding whether an identity may perform an action; fails with
403. - Role — a namespaced set of permission rules.
- ClusterRole — a cluster-scoped set of rules; can grant cluster-scoped resources and non-resource URLs, or be bound per namespace.
- RoleBinding — links subjects to a Role or ClusterRole, granting in one namespace.
- ClusterRoleBinding — links subjects to a ClusterRole, granting cluster-wide.
- Rule —
apiGroups×resources×verbs, optionally narrowed byresourceNames, ornonResourceURLs. - Verb — an action:
get,list,watch,create,update,patch,delete,deletecollection, plus special verbs (bind,escalate,impersonate, …). - Subject — who a binding grants to:
User,Group, orServiceAccount. - ServiceAccount (SA) — a namespaced identity for workloads; the only subject kind that is a real object.
- default SA — the per-namespace SA used by pods that don’t name another; has no permissions by default.
- Bound / projected token — a short-lived, audience-scoped, auto-rotated SA token (TokenRequest API); the modern default.
automountServiceAccountToken— whether the SA token is mounted into a pod.- imagePullSecrets — registry credentials attached to an SA so its pods can pull private images.
system:masters— the super-group bound tocluster-admin.- Aggregation — ClusterRoles that absorb the rules of labelled add-on ClusterRoles (covered in the advanced lesson).
Next steps
- Go deeper: Designing Least-Privilege RBAC: Roles, Aggregation & Auditing at Scale — the advanced companion: reusable patterns, ClusterRole aggregation footguns, OIDC group binding, escalation-path hunting, and continuous auditing.
- Previous lesson: Kubernetes Ingress, In Depth: Controllers, Rules, TLS & the Gateway API.
- Next lesson: Kubernetes Jobs, CronJobs & DaemonSets, In Depth.
- Related: ConfigMaps & Secrets, In Depth (RBAC for secrets) and Pod Security Admission (the pod-level hardening that pairs with RBAC).