Security¶
A compromised container doesn't ask permission — it just tries things. Whether it gets anywhere depends on four separate gates it has to clear: what its identity is allowed to ask for (RBAC), what identity it carries at all (ServiceAccount), what the process itself is permitted to do on the node (SecurityContext), and who it can reach over the network (NetworkPolicy). None of these four is optional insurance for the others — a hole in one is exactly what the next one exists to contain.
Scope: the core-concepts page introduced RBAC's four objects (Role/ClusterRole, RoleBinding/ClusterRoleBinding) and least privilege. This page goes one layer deeper: how RBAC is used in practice, what identity a Pod actually runs with, the container-level hardening that limits what a compromised process can do, and how all of it — RBAC, identity, hardening, NetworkPolicy — stacks into a single defense-in-depth picture.
TL;DR — in 30 seconds:
- Four independent gates stack as defense-in-depth: RBAC (what the identity may ask for), ServiceAccount identity (which identity a Pod carries, and whether its token is even mounted), SecurityContext/Pod Security Standards (what the process may do on the node), NetworkPolicy (who it can reach) — a gap in one is contained by the others.
- A ClusterRole can be bound cluster-wide (ClusterRoleBinding) or scoped to one namespace (RoleBinding) — the common pattern for a reusable permission set without re-authoring the same Role per namespace.
- Every Pod runs as some ServiceAccount — the namespace's
defaultif you don't specify one — and its token is auto-mounted unless you turn that off; give workloads their own scoped ServiceAccount instead of leaving everything ondefault.
1. RBAC, revisited: subject → binding → role¶
Every access decision comes down to the same chain: a subject (who) is granted a Role (what's allowed) through a binding (the grant that connects them).
flowchart LR
subgraph who["Subject"]
sa["ServiceAccount"]
user["User / Group"]
end
subgraph grant["Binding"]
rb["RoleBinding<br/>(one namespace)"]
crb["ClusterRoleBinding<br/>(cluster-wide)"]
end
subgraph what["Role"]
role["Role<br/>(namespaced verbs+resources)"]
crole["ClusterRole<br/>(cluster-wide or reusable)"]
end
sa --> rb --> role
sa --> rb --> crole
user --> crb --> crole
A ClusterRole is more flexible than its name suggests: it can be bound cluster-wide via a ClusterRoleBinding, or bound to just one namespace via an ordinary RoleBinding — the common pattern for a reusable permission set (e.g. "read Pods and logs") that many namespaces each grant locally, rather than authoring the same Role in every namespace.
Kubernetes ships a handful of built-in ClusterRoles for the common cases, from narrowest to broadest:
| ClusterRole | Grants | Typical subject |
|---|---|---|
| view | read-only on most objects, no Secrets | a read-only human or dashboard |
| edit | read/write on most objects, no RBAC objects themselves | an app team deploying into its own namespace |
| admin | edit, plus managing Roles/RoleBindings within the namespace |
a namespace owner |
| cluster-admin | every verb on every resource, cluster-wide | break-glass only — almost nothing should hold this permanently |
Note — aggregated ClusterRoles.
view/edit/adminare built by aggregation: a ClusterRole can declare anaggregationRulethat pulls in permissions from other ClusterRoles matching a label selector. This is how a custom controller's CRD permissions get merged intoeditcluster-wide without hand-editing the built-in Role — add a small ClusterRole labeled to match the aggregation rule, and it folds in automatically. (Go deeper: the upstream Aggregated ClusterRoles reference — optional, not required to defend this.)
2. ServiceAccounts — the identity a Pod runs with¶
Every Pod authenticates to the api-server as some identity, and if you don't specify one, it gets the namespace's default ServiceAccount — which, by default, can still authenticate (though it typically has no RBAC permissions bound to it out of the box).
- A Pod's ServiceAccount token is mounted into the container automatically (a projected volume), and anything with a shell in that container can read it and act as that identity against the api-server.
automountServiceAccountToken: falseturns this off for Pods that never need to call the api-server — removing a token a compromised container could otherwise use is a concrete, low-cost hardening step.- Give each workload its own ServiceAccount with only the RBAC it needs, rather than leaving everything
on
default— the same least-privilege idea from the core-concepts page, applied per-workload instead of per-namespace.
Connects to AWS. This is the same ServiceAccount the core-concepts page's IRSA note refers to: on EKS, a ServiceAccount can be annotated to assume an IAM role via OIDC, so the token mounted here carries AWS permissions too. RBAC still governs the cluster side; IRSA governs the AWS side — two separate grants riding on one identity.
3. SecurityContext — hardening the container itself¶
RBAC and identity govern what a Pod is allowed to ask the cluster for. SecurityContext governs what the container process itself is allowed to do on the node — a different, complementary layer. It can be set at the Pod level (applies to all containers) or overridden per container.
| Field | Effect |
|---|---|
runAsNonRoot / runAsUser |
refuse to start (or force a specific UID) instead of running as root inside the container |
readOnlyRootFilesystem |
the container's filesystem is read-only; anything it must write goes to an explicit mounted volume |
allowPrivilegeEscalation: false |
blocks a process from gaining more privileges than its parent (e.g. via a setuid binary) |
capabilities.drop |
removes Linux capabilities (e.g. ALL, then add back only what's truly needed) instead of the container's full default set |
privileged: true |
disables container isolation almost entirely, giving near-host access — avoid unless the workload genuinely needs it (e.g. a node-level agent) |
Warning. These fields hurt nothing to set defensively and catch a wide class of image mistakes for free — a container that never expected to run as root, or never expected to write outside
/tmp, simply fails safely instead of silently having more access than it needs. TreatrunAsNonRoot,readOnlyRootFilesystem, and droppingALLcapabilities as a sensible starting default, then add back only what a workload demonstrably needs.
4. Pod Security Standards — enforcing a baseline cluster-wide¶
SecurityContext is opt-in per Pod spec — nothing stops a badly-written manifest from requesting
privileged: true. Pod Security Standards are three predefined policy levels the cluster can enforce
so an out-of-policy Pod is rejected before it ever runs:
| Level | Policy |
|---|---|
| Privileged | unrestricted — the escape hatch for trusted system workloads (e.g. CNI plugins) |
| Baseline | blocks the known-dangerous cases (privileged: true, host namespaces, dangerous capabilities) while staying compatible with most ordinary workloads |
| Restricted | current pod-hardening best practice — requires runAsNonRoot, drops ALL capabilities by default, blocks privilege escalation |
Pod Security Admission (built into the api-server) enforces these per-namespace, via a label —
e.g. pod-security.kubernetes.io/enforce: restricted — with no separate controller to install. A namespace
can set different levels for enforce (reject), audit (log only), and warn (surface to the client),
which is how a cluster migrates a namespace toward restricted gradually: warn first to see what would
break, then enforce once workloads are compliant. (Go deeper: the upstream Pod Security Standards
concept page — optional, not required to defend this.)
5. Defense-in-depth: the layers stacked together¶
None of the above is a substitute for the others — each closes a different gap, and together they form layered defense so that one failure doesn't mean full compromise:
flowchart TB
a["RBAC<br/>what the identity may ask the api-server for"] --> b["ServiceAccount identity<br/>which identity a Pod carries, and whether its token is mounted at all"]
b --> c["SecurityContext / Pod Security Standards<br/>what the container process may do on the node"]
c --> d["NetworkPolicy<br/>who the Pod may talk to over the network"]
If an attacker compromises a container: SecurityContext/Pod Security Standards limit what that process can do on the node (no root, no extra capabilities, read-only filesystem); a scoped ServiceAccount with no needlessly-mounted token limits what it can ask the api-server to do even if it tries; RBAC limits what that identity is allowed to do even if it does ask; and a NetworkPolicy (introduced on the networking page) limits who it can reach over the network even if the container is fully compromised. A gap in one layer is contained by the others — which is the whole point of defense-in-depth over relying on any single control.
6. How this connects¶
- This extends the core-concepts page's RBAC section — same four objects, now with the built-in ClusterRoles and the aggregation pattern that makes them extensible.
- NetworkPolicy (the networking page, deepened here as the outermost layer) and SecurityContext are both defense-in-depth controls that assume something else already failed — they don't replace RBAC or scoped ServiceAccounts, they contain the blast radius when those are bypassed.
- IRSA (core-concepts, and the AWS module's IAM page) is the same ServiceAccount identity from §2, extended to carry AWS permissions as well as cluster permissions.
Common gotchas
- Leaving every workload on the namespace's
defaultServiceAccount. Fix: give each workload its own scoped ServiceAccount with only the RBAC it needs —defaultaccumulating broad permissions over time is a common way least-privilege quietly erodes. - Granting
cluster-admin(or a broad built-in ClusterRole) because writing a narrow Role feels like extra work. Fix:cluster-adminis every verb on every resource, cluster-wide — treat it as break-glass only; almost nothing should hold it permanently, andview/editusually covers the real need. - Assuming a Pod with no ServiceAccount specified has no identity/token at all. Fix: it still gets
the namespace's
defaultServiceAccount and a mounted token that can authenticate to the api-server (typically with no RBAC bound) — setautomountServiceAccountToken: falsefor Pods that never call the api-server. - Treating SecurityContext hardening (
runAsNonRoot, dropped capabilities) as redundant with RBAC. Fix: they answer different questions — RBAC governs what the identity may ask the api-server for; SecurityContext governs what the process may do on the node. A compromised container can still do node-level damage even with tight RBAC.
Check yourself — a Pod's manifest doesn't specify a serviceAccountName. Does it run with no identity?
No — it gets the namespace's default ServiceAccount automatically, and that token is mounted into
the container unless automountServiceAccountToken: false is set, even though default typically has
no RBAC bound to it out of the box. A compromised container can still read and use that token to
authenticate as default against the api-server.
Check yourself — name the four layers in the defense-in-depth stack this page describes, and what each one stops.
RBAC (limits what the identity may ask the api-server for), ServiceAccount identity/token mounting (limits which identity a Pod carries and whether it can even authenticate), SecurityContext/ Pod Security Standards (limits what the process may do on the node itself), and NetworkPolicy (limits who it can reach over the network) — each closes a gap the others don't, so one failure doesn't mean full compromise.
You can defend this when you can trace a request through subject → binding → Role, explain why a Pod
should get its own scoped ServiceAccount instead of using default, name what runAsNonRoot and
readOnlyRootFilesystem actually prevent, describe the three Pod Security Standard levels, and explain why
RBAC, SecurityContext, and NetworkPolicy are complementary layers rather than redundant ones.