01 — Authentication, authorization, RBAC¶
The full API request gauntlet: TLS → authentication (who are you?) → authorization (may you?) → admission (mutate then validate) → etcd; why there are no User objects; ServiceAccounts and bound/projected tokens; RBAC end to end (Role vs ClusterRole, bindings, verbs/resources/resourceNames, aggregation, the default ClusterRoles,
auth can-i+ impersonation) — applied by giving every Bookstore service its own least-privilege identity.
Estimated time: ~30 min read · ~60 min hands-on
Prerequisites: Part 00 ch.04 — the API request pipeline (authn → authz → admission) · Part 00 ch.06 — apiVersion/Kind and resource verbs · Part 03 ch.02 — ServiceAccount tokens land as projected volumes
You'll know after this: • distinguish authentication, authorization and admission as separate stages · • create ServiceAccounts and bind them to least-privilege Roles/ClusterRoles · • read RBAC verbs, resources and resourceNames to predict whether a request is allowed · • use kubectl auth can-i and impersonation to debug access denials · • replace the default ServiceAccount with per-workload identities across the Bookstore
Why this exists¶
Part 00 ch.04 established that the API server is the only door to the cluster and that every request runs a fixed pipeline. That chapter named the stages; this one operates them, because every access-control bug and every "why can this pod do that?" question is answered inside this pipeline.
The Bookstore has shipped happily so far on an unstated assumption: every
workload runs as the namespace's default ServiceAccount, which on a fresh
cluster can do almost nothing — by luck, not design. That is exactly the
anti-pattern: a shared, ambient identity whose permissions nobody reviewed. A
catalog Pod that is compromised should not be able to read the database Secret,
list every Pod, or — worst — modify RBAC. To make "least privilege" real you
must know precisely who a request is, how that is decided, and how
permissions are granted and scoped. This is the Access
Control pattern.
Mental model¶
Think of the API server as a secure building with three checkpoints in a fixed order, and you never reach the next one until you clear the current:
- Authentication — the ID check at the door. "Prove who you are."
Kubernetes itself has no user database: identity is asserted by a
trusted authenticator (a client certificate signed by the cluster CA, a
ServiceAccount bearer token, an OIDC token from your IdP). Output: a
username + groups, or
401. A human "user" is just a string the authenticator vouches for; there is noUserobject you cankubectl get. - Authorization — the access list. "You are
alice/ this Pod's SA — are you allowed to create pods in namespace bookstore?" One or more authorizers (Node, RBAC, ABAC, Webhook) vote; they are OR'd — the first to say allow wins; if none allow,403. - Admission — the inspector. Already-authorized requests are first mutated (defaulting, webhooks, sidecar injection) then validated (schema, then validating webhooks/policy — PSA, Kyverno). Only a request that survives all three is converted to storage form and written to etcd.
Two invariants to keep: mutate-before-validate is fixed (a validating policy always sees the final object), and the API server is the sole etcd client (so this gauntlet is the only place anything is enforced — raw etcd access bypasses all of it, which is why etcd access ≈ cluster-admin).
Diagrams¶
The request pipeline: TLS → authN → authZ → admission → etcd (Mermaid)¶
flowchart LR
req["Client request
kubectl / controller /
kubelet / Bookstore Pod"]
tls["TLS handshake
(mutual where client certs)"]
authn["AuthN
cert / SA token / OIDC
=> username + groups"]
authz["AuthZ (OR of authorizers)
Node | RBAC | ABAC | Webhook"]
mut["Mutating admission
defaulting + webhooks
(may change object)"]
sch["Schema + object validation"]
val["Validating admission
PSA / Kyverno / webhooks
(may reject, no mutate)"]
etcd[("etcd
(storage version)")]
resp["Response to client"]
req --> tls --> authn --> authz --> mut --> sch --> val --> etcd
etcd --> resp
authn -. "401 unauthenticated" .-> resp
authz -. "403 forbidden" .-> resp
mut -. "webhook deny" .-> resp
val -. "policy deny" .-> resp
RBAC object matrix: scope × binding (ASCII)¶
│ grants permissions for ... │ ... where it applies
────────────────┼───────────────────────────────────┼────────────────────────
Role │ namespaced resources in ONE ns │ that one namespace
ClusterRole │ cluster-scoped resources, OR │ cluster-wide, OR
│ namespaced resources as a template │ (per binding below)
────────────────┼───────────────────────────────────┼────────────────────────
RoleBinding │ binds a Role OR a ClusterRole ... │ ... within ONE namespace
ClusterRole- │ binds a ClusterRole only ......... │ ... cluster-wide
Binding │ │ (every namespace)
────────────────┴───────────────────────────────────┴────────────────────────
Key combinations:
Role + RoleBinding -> perms in one ns (Bookstore uses this)
ClusterRole + RoleBinding -> reusable perm set, applied to ONE ns
ClusterRole + ClusterRoleBinding -> cluster-wide (e.g. cluster-admin)
Role + ClusterRoleBinding -> INVALID (a Role can't go cluster-wide)
RBAC is PURELY ADDITIVE: no deny rules. Absence of an allow = denied.
A subject's permissions = union of every binding that names it.
Hands-on with the Bookstore¶
Assumed working directory: the guide repo root (full-guide/). Requires
the bookstore namespace (Part 01 ch.03).
This chapter adds 05-serviceaccounts-rbac.yaml (numbered 05- so a
whole-dir apply creates the identities right after the namespace and before
any workload that references them) and wires serviceAccountName +
automountServiceAccountToken: false into the workloads — purely additive
edits; every prior field (config, Secret-built DB_DSN, probes, preStop
sleep, volumes, labels, the Part 04 scheduling layer) is unchanged.
1. Inspect the pipeline you already depend on¶
# AuthN: who does the API server think YOU are (from your kubeconfig)?
kubectl auth whoami
# AuthZ stage, directly — the SubjectAccessReview API behind `can-i`:
kubectl auth can-i create pods -n bookstore
kubectl auth can-i 'delete' clusterroles # almost certainly: no
# What can the *default* SA in bookstore do? (the ambient identity so far)
kubectl auth can-i --list \
--as=system:serviceaccount:bookstore:default -n bookstore
# → essentially nothing useful: that it "works" today is luck, not design.
2. Create a dedicated, least-privilege identity per service¶
New file
examples/bookstore/raw-manifests/05-serviceaccounts-rbac.yaml.
It defines one ServiceAccount per workload and one tiny Role. The design
decision: the Go services and the stock images never call kube-apiserver
(verified in app/catalog/main.go
— it talks to Postgres/Redis, never the Kubernetes API). So the correct least
privilege is no API permissions and no mounted token at all:
apiVersion: v1
kind: ServiceAccount
metadata: { name: catalog-sa, namespace: bookstore }
automountServiceAccountToken: false # no projected token mounted
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # namespaced
metadata: { name: catalog-config-reader, namespace: bookstore }
rules:
- apiGroups: [""] # "" = core group (configmaps)
resources: ["configmaps"]
resourceNames: ["catalog-config"] # ONLY this object, by name
verbs: ["get"] # not list/watch (they ignore names)
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: catalog-config-reader-binding, namespace: bookstore }
subjects: [ { kind: ServiceAccount, name: catalog-sa, namespace: bookstore } ]
roleRef: { kind: Role, name: catalog-config-reader, apiGroup: rbac.authorization.k8s.io }
storefront-sa, orders-sa, postgres-sa, redis-sa, rabbitmq-sa and the
batch migrate-sa (shared by the db-migrate Job and cleanup CronJob) get
no binding — with no RoleBinding/ClusterRoleBinding naming them, RBAC
denies them everything, which is exactly right for a workload that never calls
the API. There is one SA per workload and no Bookstore Pod uses the
namespace default SA (the production-note rule, applied to itself). The
catalog Role is a deliberately minimal teaching example of resource-name
scoping
(get on exactly one named ConfigMap; list/watch are not granted
because they ignore resourceNames and would leak every ConfigMap's
existence). It grants nothing on secrets: a get secrets returns
cleartext, so that verb is credential disclosure
(Part 03 ch.02).
# from the repo root (full-guide/)
kubectl apply -f examples/bookstore/raw-manifests/00-namespace.yaml
kubectl apply -f examples/bookstore/raw-manifests/05-serviceaccounts-rbac.yaml
kubectl get sa,role,rolebinding -n bookstore
3. Wire the identities into the workloads (additive)¶
10-catalog-deploy.yaml,
11-storefront-deploy.yaml,
14-orders-deploy.yaml
and 20-postgres-statefulset.yaml
each gain two fields in template.spec (nothing else changes):
spec:
serviceAccountName: catalog-sa # the workload's own identity
automountServiceAccountToken: false # belt-and-braces: no token mounted
# ...all prior fields (scheduling layer, containers, volumes) unchanged...
The same two fields are added to every other Bookstore workload in the
namespace as well — 12-redis.yaml (redis-sa), 13-rabbitmq.yaml
(rabbitmq-sa), the manual-canary 30-catalog-canary.yaml (catalog-sa,
both stable+canary templates), and the postgres:16 DB batch jobs
21-db-migrate-job.yaml / 22-cleanup-cronjob.yaml (the shared migrate-sa).
No Bookstore Pod is left on the default ServiceAccount. The four manifests
above are shown as the worked example; the edits are mechanically identical
everywhere.
(The per-workload securityContext fields are added to these manifests in
ch.02; this chapter's manifest edits are identity only.
Note this is distinct from the namespace's PSA restricted labels, which
are already present in the shared 00-namespace.yaml you applied above —
ch.02 explains them, but they are enforcing from the first apply, hence the
restricted-shaped debug pod earlier.) Apply the prerequisites then the
workload:
# from the repo root (full-guide/) — prerequisite chain, in order
kubectl apply -f examples/bookstore/raw-manifests/00-namespace.yaml
kubectl apply -f examples/bookstore/raw-manifests/05-serviceaccounts-rbac.yaml
kubectl apply -f examples/bookstore/raw-manifests/15-catalog-config.yaml
kubectl apply -f examples/bookstore/raw-manifests/16-db-credentials.yaml
# cluster-scoped scheduling dep + local image (Part 04 / repo README):
kubectl apply -f examples/bookstore/raw-manifests/35-priorityclasses.yaml
kind load docker-image bookstore/catalog:dev --name bookstore # if using kind
# catalog carries DB_DSN, so it needs Postgres + the schema Job to go Ready
# (its /readyz pings Postgres). Bring those up and gate on the Job first.
kubectl apply -f examples/bookstore/raw-manifests/20-postgres-statefulset.yaml
kubectl rollout status statefulset/postgres -n bookstore
kubectl apply -f examples/bookstore/raw-manifests/21-db-migrate-job.yaml # schema
kubectl wait --for=condition=complete job/db-migrate -n bookstore --timeout=120s
kubectl apply -f examples/bookstore/raw-manifests/10-catalog-deploy.yaml
kubectl rollout status deployment/catalog -n bookstore
4. Prove the least privilege — and prove the token is gone¶
# catalog-sa can GET its one ConfigMap by name, and NOTHING else:
kubectl auth can-i get configmap/catalog-config \
-n bookstore --as=system:serviceaccount:bookstore:catalog-sa # yes
kubectl auth can-i list configmaps \
-n bookstore --as=system:serviceaccount:bookstore:catalog-sa # no
kubectl auth can-i get secret/db-credentials \
-n bookstore --as=system:serviceaccount:bookstore:catalog-sa # no
kubectl auth can-i --list \
-n bookstore --as=system:serviceaccount:bookstore:orders-sa # ~nothing
# The token is genuinely not projected. catalog is distroless (no shell) —
# inspect from an EPHEMERAL public-image Pod using the SAME ServiceAccount.
# IMPORTANT: `bookstore` already enforces PSA `restricted` (it is baked into
# the single canonical 00-namespace.yaml from the start — the labels and the
# full reasoning are ch.02's topic, but the file carries them now, so this
# applies HERE too). Therefore EVERY ad-hoc debug Pod you run in `bookstore`
# must itself be restricted-compliant or PSA rejects it before it starts —
# busybox/curl/netshoot all run fine under this securityContext, so set it:
kubectl run sa-peek -n bookstore --image=busybox:1.36 --restart=Never -i --rm \
--overrides='{"spec":{"serviceAccountName":"catalog-sa",
"automountServiceAccountToken":false,
"securityContext":{"runAsNonRoot":true,"runAsUser":65532,
"seccompProfile":{"type":"RuntimeDefault"}},
"containers":[{"name":"sa-peek","image":"busybox:1.36",
"securityContext":{"allowPrivilegeEscalation":false,
"capabilities":{"drop":["ALL"]}},
"command":["sh","-c","ls /var/run/secrets/kubernetes.io/serviceaccount 2>&1 || echo NO-TOKEN-MOUNTED"]}]}}'
# → NO-TOKEN-MOUNTED : the SA exists, but no API credential is in the Pod.
# (`--as` is impersonation: it needs RBAC to impersonate; cluster-admin
# kubeconfigs have it. It's the canonical way to audit another identity.)
Why these
--overrides(a teaching point, not a workaround). This guide uses one canonical00-namespace.yaml, and it carries thepod-security.kubernetes.io/enforce: restrictedlabels from the very first apply (the mechanics and rationale are ch.02's subject; the file is shared, not duplicated per chapter). A practical consequence lands here, in ch.01: the moment youkubectl apply 00-namespace.yaml, any Pod inbookstore— including a throwawaykubectl rundebug Pod — must satisfyrestrictedor admission rejects it. So every ephemeral pod in the Part 05 hands-on includes the restrictedsecurityContextshown above (runAsNonRoot,runAsUser,seccompProfile: RuntimeDefault,allowPrivilegeEscalation: false,drop: ["ALL"]). Internalize this now: hardening a namespace hardens everything you run in it, debugging included.Lineage / forward refs. This chapter establishes identity and RBAC. ch.02 owns and explains the
restrictedPSA labels and the workloads'securityContext— those labels live in the single shared00-namespace.yamland are already in force the moment you apply it (which is why the debug pod above is restricted-shaped); ch.02 is where the why and how are taught, not where they are first switched on. ch.03 adds an admission policy (Kyverno) at the validating-admission stage you saw above; ch.04 deepens Secret encryption + audit logging (which records every authN/authZ decision made here). RBAC onsecretswas introduced in Part 03 ch.02 — this chapter is the general model behind it.
How it works under the hood¶
Authentication — every credential is "asserted identity"¶
The API server runs configured authenticators in order; the first to
positively identify the request wins, producing a user.Info (a username
string + a set of group strings). Kubernetes stores no user records — it
trusts the authenticator. The common ones:
- X.509 client certificates. The client presents a TLS cert signed by the
cluster CA;
CN→ username,O(organization) → groups. This is how admin kubeconfigs and kubelets authenticate. Revocation is awkward (no CRL by default) — short-lived certs or another method is preferred for humans. - ServiceAccount tokens (the workload identity). A signed JWT whose subject
is
system:serviceaccount:<NS>:<NAME>with groupssystem:serviceaccountsandsystem:serviceaccounts:<NS>. Modern clusters issue bound, projected tokens via the TokenRequest API: the kubelet mounts a short-lived token that is audience-scoped, time-limited, and bound to the Pod object (it is invalidated when the Pod is deleted) and auto-rotated before expiry. The legacy model — a permanent token in aSecretauto-created per SA — is deprecated and off by default (no auto-created token Secret in current Kubernetes); a never-expiring token is a standing liability.automountServiceAccountToken: false(set on both the SA and the podSpec in the Bookstore) stops even the projected token from being mounted when the workload never calls the API. - OIDC. For humans: the API server validates an ID token from your
identity provider (
--oidc-issuer-urletc.); claims map to username/groups. This is how real clusters do SSO — there is still no User object, just a trusted issuer. - Webhook / authenticating proxy. The API server delegates token review to an external service (used by managed providers, e.g. EKS's IAM authenticator).
kubectl auth whoami shows the result of this stage for your kubeconfig.
Authorization — OR of authorizers, RBAC the workhorse¶
After identity, the configured authorizers (--authorization-mode) are
consulted in order and OR'd — any allow permits the request; an explicit
no opinion falls through to the next; only if none allow is it 403. The
typical chain is Node,RBAC (kubelets are authorized by the Node
authorizer; everything else by RBAC). Other modes: ABAC (static
attribute file — legacy), Webhook (external decision — how cloud IAM is
bolted on). RBAC is the one you author:
- A Role (namespaced) / ClusterRole (cluster-scoped or a reusable
template) holds
rules, each =apiGroups×resources(optionallysubresourceslikepods/exec,pods/log, and/orresourceNamesto scope to specific objects) ×verbs(get list watch create update patch delete deletecollection, plus non-resource verbs and special ones likeimpersonate,bind,escalate).apiGroups: [""]is the core group (pods, services, configmaps, secrets).resourceNamesconstrains to named objects but is ignored bylist/watch/create(you cannot name-scope a collection list) — which is exactly why the Bookstore Role grants onlyget. - A RoleBinding grants a Role or a ClusterRole within one namespace; a ClusterRoleBinding grants a ClusterRole cluster-wide. (A ClusterRole bound by a RoleBinding applies only in that binding's namespace — the standard way to reuse a permission template per-namespace.)
- RBAC is purely additive — there are no deny rules. A subject's effective permission is the union of every binding that names it (directly or via a group). "Restricting" something means not granting it (and ensuring no other binding grants it) — there is no override. This is why over-broad default bindings are dangerous: you can't subtract them, only avoid/remove.
- The
system:masterssuper-group bypasses RBAC entirely. Any request whose authenticated groups includesystem:mastersis granted unconditional cluster-admin by a hard-coded authorizer that runs before RBAC — no RoleBinding is involved and none can revoke it (RBAC has no deny rules, so you cannot "restrict"system:masters). The kubeadm-generated admin client certificate is exactlyO=system:masters(that's why it is all-powerful). The only mitigation is not issuing certs/tokens that carry that group: treat thesystem:masterskubeconfig as a break-glass credential, keep it offline, and give humans/automation narrowly-bound identities instead. - Aggregation. A ClusterRole with
aggregationRule.clusterRoleSelectorsauto-unions in the rules of any ClusterRole matching the label selector. The built-inadmin/edit/viewroles are aggregated, which is how installing a CRD's*-aggregate-to-editClusterRole instantly extendseditto that CRD with no edit to the base role. - Default ClusterRoles ship on every cluster:
cluster-admin(everything, via*),admin(full namespace control incl. RBAC within the ns, via a RoleBinding),edit(read/write most objects but not RBAC and not read Secrets in newer versions),view(read-only, excludes Secrets). Bind these by reference; don't recreate them.system:*ClusterRoles wire up the control plane itself.
kubectl auth can-i <VERB> <RESOURCE> [--as=<USER_OR_SA>] [-n ns] runs a
SubjectAccessReview — it asks the real authorizer, so it's the
authoritative audit tool (and --list enumerates everything an identity can
do). --as / --as-group is impersonation, itself an RBAC-gated power
(impersonate verb): cluster-admins can act as any subject to test policy
without that subject's credentials.
Admission — the last word (covered next, anchored here)¶
Authorized requests are then mutated (built-in plugins like
ServiceAccount, LimitRanger, DefaultStorageClass, plus mutating webhooks)
and only afterwards validated (schema, then ResourceQuota, Pod Security
Admission, validating webhooks / Kyverno / ValidatingAdmissionPolicy). This
is the seam ch.02 and ch.03 plug
into; the ordering guarantee (mutate→validate) is why a validating policy can
trust it sees the final object.
Production notes¶
In production: never run workloads as the
defaultServiceAccount. Give every workload its own SA, grant it the minimum, and setautomountServiceAccountToken: falsefor the (common) case where the app never calls the API. A leaked token from an over-permissioned shared SA is a lateral-movement highway.In production: treat
get/list/watch secrets,pods/exec,pods/attach,impersonate,escalate/bind, andcreateon RBAC objects as crown-jewel verbs. They are all paths to credentials or privilege escalation; audit them withkubectl auth can-i --list --as=…, alarm on RBAC changes (you wire this audit rule in ch.04), and prefer admission policy that blocks over-broad grants.In production: scope tightly with Role + RoleBinding per namespace; reserve
ClusterRoleBindingfor genuinely cluster-wide needs. Reuse the aggregatedview/edit/adminClusterRoles rather than hand-rolling broad custom roles, and rememberedit/viewdeliberately exclude Secrets in current versions — don't undo that.In production (managed — EKS/GKE/AKS): cluster identity is bridged to cloud IAM. EKS maps IAM principals to Kubernetes users/groups (the aws-auth/Access Entries → then RBAC) and IRSA lets a Pod's ServiceAccount assume an IAM role via a projected OIDC token. GKE Workload Identity binds a KSA to a Google service account the same way; AKS uses Microsoft Entra Workload ID. The pattern is identical: the projected, audience-bound SA token is exchanged for a cloud credential — so the bound-token mechanics above are not academic, they are how Pods get cloud access without static keys.
In production: prefer OIDC SSO for humans (short-lived tokens, central revocation, MFA) over long-lived client certs (no built-in revocation). Bind groups from the IdP to RBAC, not individuals, so access follows directory membership.
Quick Reference¶
kubectl auth whoami # your identity (authN result)
kubectl auth can-i <VERB> <RES> [-n ns] # your authZ (SubjectAccessReview)
kubectl auth can-i --list -n <NS> # everything you can do there
kubectl auth can-i <VERB> <RES> \
--as=system:serviceaccount:<NS>:<SA> [-n ns] # audit a workload identity
kubectl auth can-i get secret/<N> --as=<USER> -n <NS> # check a name-scoped grant
kubectl get sa,role,rolebinding,clusterrole,clusterrolebinding [-n ns]
kubectl describe clusterrole view # inspect a default role
kubectl create role r --verb=get --resource=configmaps \
--resource-name=catalog-config -n <NS> --dry-run=client -o yaml # author
Minimal least-privilege identity skeleton:
apiVersion: v1
kind: ServiceAccount
metadata: { name: app-sa, namespace: <NS> }
automountServiceAccountToken: false # if the app never calls the API
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: app-role, namespace: <NS> }
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"] # scope to named object (get only)
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: app-rb, namespace: <NS> }
subjects: [ { kind: ServiceAccount, name: app-sa, namespace: <NS> } ]
roleRef: { kind: Role, name: app-role, apiGroup: rbac.authorization.k8s.io }
# in the workload: spec.serviceAccountName: app-sa
# spec.automountServiceAccountToken: false
Checklist:
- Every workload has a dedicated ServiceAccount (never
default) -
automountServiceAccountToken: falsewhere the app doesn't call the API - Roles grant the minimum verbs;
resourceNames+getover blanketlist - No
get/list/watch secretsgranted unless strictly required -
Role+RoleBindingper namespace;ClusterRoleBindingonly when cluster-wide - Reuse aggregated
view/edit/admin; don't recreate broad roles -
kubectl auth can-i --list --as=…run for each SA (verified least privilege) - RBAC-change /
exec/impersonateauditing in place (ch.04)
Test your understanding¶
Try each before opening the answer drawer. The act of trying is the exercise; the answer is the check.
-
There is no
Userobject in Kubernetes — so where does the username come from whenalicerunskubectl get pods, and how does the cluster decide which RBAC bindings apply?
Show answer
The authenticator (client cert signed by the cluster CA, an OIDC token from the IdP, a bearer token, etc.) **asserts** a username + groups; Kubernetes itself stores no user database. RBAC then matches those strings against `subjects:` in `RoleBinding`/`ClusterRoleBinding` — see §Mental model. That is why binding to *groups* (`oidc:platform-team`) is preferred over individuals: identity follows your IdP, not in-cluster YAML. -
A new engineer reports
kubectl get pods -n bookstorereturnsForbidden, but they swear they're in theplatform-teamgroup. How do you debug this in under 90 seconds, without granting them any new permissions?
Show answer
`kubectl auth whoami` (or look at the JWT) to confirm the authenticator emitted `platform-team` as a group; `kubectl auth can-i list pods -n bookstore --as=alice --as-group=platform-team` to test the binding from your admin context; `kubectl get rolebinding,clusterrolebinding -A -o yaml | grep -B5 platform-team` to find what's actually bound. The most common cause is the group string in RBAC not matching what the IdP actually emits (case, prefix `oidc:`). -
A teammate suggests giving the
payments-workerSAverbs: ["get","list"]onsecrets"so it can read its DB password". Why is this a code smell, and what's the right shape?
Show answer
`list secrets` reads *every* Secret in the namespace, not just one — a compromised payments-worker now exfiltrates every credential. The right shape is `verbs: ["get"]` with `resourceNames: ["payments-db-credentials"]` (a single named Secret), or — far better — mount the Secret as a file/env and set `automountServiceAccountToken: false` so the SA needs no `secrets` permission at all. Better still: use IRSA/Workload Identity to skip Kubernetes Secrets entirely. -
Hands-on extension — break impersonation cleanly. Create a ServiceAccount
app-sawith a Role that allows onlyget configmaps. Then trykubectl auth can-i list configmaps --as=system:serviceaccount:default:app-sa. Now grant yourselfimpersonateonusers/groupsand try--as=system:admin. What does the audit log show?
What you should see
`can-i list` returns `no` (only `get` is granted). The impersonation attempt as `system:admin` works only if you hold `impersonate` on `users` — and the audit log records *two* identities: `user:`, `impersonatedUser: system:admin`. That dual-record is why impersonation is auditable and the right tool for "act as a workload to test its permissions" instead of stealing its token. -
You discover a
ClusterRoleBindingthat bindscluster-admintosystem:authenticated. Explain in one sentence why this is catastrophic, and what the cluster effectively becomes.
Show answer
`system:authenticated` is *any* successfully-authenticated principal — every ServiceAccount, every human, every webhook with a valid token — so the binding silently makes the cluster a single-tenant cluster-admin free-for-all where any compromised Pod becomes cluster-admin. RBAC misconfiguration is the most common path to "from container breakout to full cluster compromise"; this is precisely why audit logs ([ch.04](04-secrets-and-cluster-hardening.md)) must alert on RBAC changes.
Further reading¶
- Ibryam & Huß, Kubernetes Patterns 2e, ch.26 — Access Control — the authentication/authorization/admission model and least-privilege RBAC as a pattern.
- Rosso et al., Production Kubernetes, ch.10 — "Identity" — workload and human identity, token mechanics, and cloud-IAM bridging in production; pair with Lukša, Kubernetes in Action 2e securing-the-API-server material.
- Official: https://kubernetes.io/docs/reference/access-authn-authz/authentication/, https://kubernetes.io/docs/reference/access-authn-authz/rbac/, and the request-flow overview https://kubernetes.io/docs/reference/access-authn-authz/controlling-access/.