Kubernetes Privileged Pod Options, Node Escape Vectors, and Admission Gating
Last verified2026-10 Also see: Kubernetes security (RBAC, Pod Security table, Falco, escape-paths table) · Container runtimes and sandboxing · Sandbox isolation deep dive · Build platform security (rootless BuildKit) · Linux security fundamentals (namespaces, capabilities) · Cluster security lab
Start here: the basics in plain words
A pod is not a tiny virtual machine. It is one or more ordinary Linux processes that the kernel has been told to show less, allow less and give less. Every security-relevant field in a pod spec turns one of those restrictions back off. "Privileged pod options" is the name for the knobs that, once set, leave a workload standing on the node with almost nothing between it and the shared host kernel. The defender's whole job here is to refuse to admit pods that set those knobs, and this page is organised around that: understand each knob, then write the admission rule that blocks it.
A kernel feature giving a process its own view of one resource: its own process list (PID), network interfaces (NET), mount table (MNT), shared memory (IPC). The process still runs on the one shared host kernel.
Root's power split into ~40 named pieces (CAP_SYS_ADMIN, CAP_SYS_PTRACE…). A container gets a small default set, so "root in a container" is deliberately a weak root.
A per-process allow-list of system calls. The runtime default (RuntimeDefault) blocks dozens of the riskiest syscalls, including the ones that create new namespaces or load kernel modules.
The resource-accounting layer. In the old cgroup v1 it also had a host-side hook (release_agent) that became a classic escape; cgroup v2 redesigned that away.
Where you set user ID, capabilities, seccomp, read-only root, privileged and allowPrivilegeEscalation on a pod or container.
Pod-level fields that drop one wall each: share the node's process list, its network, its shared memory, or mount a path from the node's own filesystem.
Memory hook"a pod is a process, not a VM." Every escape is the same story: the workload reaches the one shared host kernel because an isolation wall was removed or a kernel bug was reachable. So the defence is always the mirror image: non-root, drop all capabilities, no privileged, no host namespaces, no sensitive host mounts, default seccomp, enforced by Pod Security Admission plus a policy engine. Learn the knobs so you can recognise the pod spec that must never be admitted.
a hardened pod each knob removes one wall ┌─────────────────────┐ privileged: true → drops nearly every wall at once │ non-root UID │ hostPID: true → shares the node's process list │ caps: drop ALL │ hostNetwork: true → shares the node's network + reaches node-local services │ seccomp RuntimeDef. │ hostPath: / → mounts the node's root filesystem into the pod │ no host namespaces │ CAP_SYS_ADMIN → unlocks many kernel primitives │ readOnlyRootFS │ a kernel/runtime CVE → one shared kernel, so one bug can be enough └─────────────────────┘
Why it matters
Interviewers use privileged pods to tell apart candidates who treat a container as a magic black-box boundary from those who know it is a process on a shared kernel. The strong answer names the exact kernel mechanism each flag toggles and then shows the admission control that keeps such pods out of production, because in a real cluster the control plane is where you win or lose this. A multi-tenant cluster, or any cluster that runs CI builds, is the case that bites: a workload that can create pods, or set a host flag, or mount a host path, can usually turn that into control of the node and then the other tenants on it.
-
1. Workload gains pod-create or an over-broad RBAC verbattackthe foothold
- Often an over-scoped ServiceAccount, or a CI step that can apply manifests
-
2. It schedules a pod with a host knob setattackthe request
- hostPath, hostPID, hostNetwork, privileged, or a dangerous capability
-
3. That knob exposes the nodeattackthe escape
- The pod now touches the node's filesystem, processes, network or kernel directly
-
Defender wins at step 2defendadmission control refuses the pod
- Pod Security Admission + a ValidatingAdmissionPolicy reject the spec before it is ever scheduled
🎯On the job. The review question is never "is this container hardened?" in isolation; it is "what stops someone applying a pod that isn't?" If the answer is "we ask people not to", there is no control. The control is admission: Pod Security Admission for the common cases, a
ValidatingAdmissionPolicyfor the cluster-specific ones.
SecurityContext vs host namespaces: what each field actually changes
Start with the difference the first interview question always probes: privileged: true versus a single host* flag.
securityContext.privileged: truethe broad one. It hands the container nearly the full capability set, removes the seccomp and device restrictions, and unmasks parts of /proc and /sys that are normally hidden. It is "undo most of the walls at once". A privileged container is, for practical purposes, root on the node.
hostPID: truenarrow and specific. The pod shares the node's process-ID namespace, so it can see and signal every process on the host, not just its own. On its own it is not full root, but combined with a capability like CAP_SYS_PTRACE it lets the workload inspect and attach to host processes, which is a path to whatever those processes hold.
hostNetwork: truethe pod shares the node's network namespace. It now sits on the node's interfaces and IP, so it can reach services bound to the node's loopback or link-local addresses, and it sidesteps NetworkPolicy, which is applied to pod network namespaces, not the host's. In a cloud this is the usual route to the instance metadata endpoint (169.254.169.254) that NetworkPolicy would otherwise have fenced off.
hostIPC: truethe pod shares the node's System V IPC and POSIX shared memory, so it can read or tamper with shared-memory segments other host processes are using.
Removes nearly all container restrictions at once. Treat as "root on the node". Baseline profile forbids it.
Node process list is visible and signalable. Dangerous with CAP_SYS_PTRACE. Baseline forbids it.
Node's network stack. Bypasses NetworkPolicy; reaches node-local and metadata services. Baseline forbids it.
Node's shared memory. Read/tamper other hosts' segments. Baseline forbids it.
A single capability that unlocks mount, many namespace and other kernel operations. The "almost privileged" capability. Restricted drops ALL caps.
The reassuring part: all five of the above are forbidden by the Baseline Pod Security profile, and Baseline is cheap to turn on. The gaps that need more than Baseline are
hostPathscoping and cluster-specific rules, which is why the sections below move from the built-in profiles to a customValidatingAdmissionPolicy.
hostPath and the node filesystem
The pieces you need to knowA hostPath volume mounts a directory or file from the node's own filesystem straight into the pod. The kubelet credentials, the container runtime's control socket, the node's cron and service definitions, and the node's root filesystem all live on that same node filesystem. If a pod can mount a sensitive host path writable, it can influence what the node itself runs as root.
What goes wrongWrite access to the node's root filesystem, or to the runtime's control socket, lets a workload ask the node (as root) to do things on its behalf: schedule a privileged task, or launch another container with the node's filesystem attached. The common theme is that hostPath turns "I control a pod" into "I control what the node runs", because the pod and the node are now sharing files.
Why it's badThe node runs every other tenant's pods. Reaching the node from one pod means reaching all of them. This is why hostPath is forbidden entirely by Baseline, and why, when a cluster genuinely needs a host mount (a log shipper reading /var/log), you want a rule that allows only that one prefix and nothing else.
🎯On the job. The highest-value single admission rule on a shared cluster is "no
hostPath, except read-only under an explicit allow-list of prefixes like/var/log/". A request to mount/,/etc,/var/run, or a runtime socket path is the finding. The lab below shows a policy enforcing exactly this, live.
Container breakout mechanics, explained from zero
These are the named escapes an interviewer expects you to recognise. The goal here is to understand the mechanism and the fix, so you can reason about which admission rule or kernel version removes each one. None of this is a recipe; it is the defender's model of why the wall failed.
cgroup v1 release_agent — CVE-2022-0492
The pieces you need to knowA cgroup is a group of processes the kernel accounts and limits together. In the old cgroup v1 design, a cgroup could have a release_agent: a path to a program the kernel runs, as root on the host, when the last process in that cgroup exits. That feature existed to let the system clean up after a cgroup emptied.
What went wrongA kernel check was missing (cgroup_release_agent_write in kernel/cgroup/cgroup-v1.c, tracked as CVE-2022-0492, CWE-862 Missing Authorization). Under the right conditions a process could set the release_agent path without the capability that should have been required. Because the kernel runs that program on the host as root, a workload that could both set the path and trigger the cgroup to empty got the host to execute a program of its choosing with root.
Why it's bad and what removes it. It turned a container with the wrong settings into root on the node. Three things each independently shut it down: the kernel fix added the missing capability check (upstream commit 24f6008564183a…); cgroup v2 redesigned release notifications so there is no host-side release_agent program to point at, so the whole class disappears on a cgroup-v2 node; and an LSM such as SELinux in enforcing mode blocked the escape on distributions that ship it (Red Hat rated it CVSS 7.0, NVD 7.8). The defender's lesson: run cgroup v2 (the modern default), and do not grant the capabilities and host mounts the attack needed in the first place.
📰Real incident — CVE-2022-0492 (2022). The
release_agentflaw was reachable from a container that hadCAP_SYS_ADMINor could write the relevant cgroup files, and it became a widely-cited container-escape demonstration. The lesson that stuck: features that let an unprivileged side name a program the kernel runs as root are a recurring escape shape, and cgroup v2 plus least-privilege containers close it.
runc working-directory file-descriptor leak — CVE-2024-21626
The pieces you need to knowrunc is the low-level runtime that actually starts containers. When it sets up a container it briefly holds open file descriptors to host resources, including the host's /sys/fs/cgroup.
What went wrongThose descriptors leaked into the container's first process (CVE-2024-21626, affecting runc >=1.0.0-rc93 and <=1.1.11, fixed in 1.1.12, CVSS up to 8.6). If a container image set its working directory to a path pointing through one of the leaked descriptors, the container's process started with a working directory that was actually in the host's filesystem, outside the container. A crafted image could disguise this with an innocent-looking symlink.
Why it's bad and what removes it. A working directory on the host filesystem is a doorway from the container into the host, and variants could overwrite host binaries, giving node control. The fix shipped in runc 1.1.12. The defender's lesson: patch the runtime (this is a "keep runc current" item), and run only trusted images, because the trigger lived inside the image.
BuildKit entitlement and cache-mount flaws — CVE-2024-23651 / CVE-2024-23653
The pieces you need to knowBuildKit is the engine behind modern docker build. It runs build steps, and some capabilities (like running a step with elevated privileges) are gated behind an entitlement you must explicitly enable (security.insecure).
What went wrongTwo 2024 flaws, both fixed in BuildKit v0.12.5:
BuildKit's APIs for running interactive containers from a build did not properly validate the entitlement, so a malicious build could start a container with elevated privileges it was never granted.
a race condition between two parallel build steps sharing the same cache-mount subpaths could expose files from the host to the build container.
Why it's bad and what removes it. CI runners build untrusted Dockerfiles all day; a build that can grab host files or run a privileged container is a runner compromise. Both are fixed in v0.12.5, and the published workaround is to not accept BuildKit frontends from untrusted sources and to avoid building untrusted Dockerfiles that use cache mounts. The defender's lesson ties straight to build platform security: keep BuildKit patched, run builds rootless and sandboxed, and never hand a CI builder privileged: true or a mounted runtime socket.
ImportantNotice the pattern across all four: each was fixed by a version bump and was avoidable by least privilege (no
CAP_SYS_ADMIN, cgroup v2, no host mounts, no privileged builds). You rarely rely on a single mitigation. Patch and refuse the dangerous pod spec.
Hardening CI builders without privileged pods
The usual reason teams reach for privileged: true is "we need to build container images in the cluster". You do not need privileged for that any more.
run the build engine as an unprivileged user inside a user namespace, so even "root" in the builder maps to a non-root UID on the node. No privileged, no mounted runtime socket.
schedule the build pod onto a stronger runtime (gVisor via a RuntimeClass, or a Kata/Firecracker microVM) so a builder escape hits a second wall. See the sandbox isolation deep dive and the cluster security lab, which routes an untrusted pod to a gVisor handler via RuntimeClass.
A /var/run/containerd.sock or docker socket in a build pod is equivalent to node root: it lets the pod ask the node to run any container with any mount. This belongs on every admission deny-list.
🎯On the job. If a build pipeline sets
privileged: true"so Docker works", the fix is rootless BuildKit, and it is a drop-in change for most builds. Flag the privileged flag and the socket mount together; they are the two tells of a builder that trusts its workload too much.
Pod Security Admission: the built-in first line
What it isPod Security Admission (PSA) is a built-in admission controller that enforces the three Pod Security Standards profiles by namespace label. It replaced the removed PodSecurityPolicy (PSP) in Kubernetes 1.25.
No restrictions. For trusted system workloads only.
Blocks the known-dangerous knobs: host namespaces, privileged, hostPath, host ports, most added capabilities, non-default seccomp/AppArmor/SELinux. Cheap, high value.
Baseline plus: must run non-root, drop ALL capabilities (NET_BIND_SERVICE may be re-added), allowPrivilegeEscalation=false, seccomp RuntimeDefault or Localhost, and a short allow-list of volume types (no hostPath).
Each profile runs in up to three modes at once, set per namespace: enforce (reject violating pods), audit (record them in the audit log) and warn (return a warning to whoever applied it). A common safe rollout is enforce at baseline, warn and audit at restricted, so you block the dangerous pods immediately while surfacing what would need to change to reach restricted.
In practiceLabelling a namespace and then trying a host-namespace pod shows the enforcement and the warning together (real output from the cluster security lab):
$ kubectl label ns psa-demo \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
namespace/psa-demo labeled
$ kubectl -n psa-demo apply -f hostpid-pod.yaml
Error from server (Forbidden): pods "hostpid" is forbidden:
violates PodSecurity "baseline:latest": host namespaces (hostPID=true) ← enforced: rejected
$ kubectl -n psa-demo apply -f plain-pod.yaml
Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile ← warned, still created
pod/plain createdPSA's gapsPSA is namespace-coarse and profile-fixed. It cannot express "hostPath is allowed, but only under /var/log", it cannot vary a rule by image or team, and whoever can set namespace labels (or create a namespace) can downgrade the profile. For anything cluster-specific you reach for a policy engine.
📰Real incident — PodSecurityPolicy removal (Kubernetes 1.25, 2022). PSP, the original built-in, was deprecated in 1.21 and removed in 1.25 because its authorisation model was confusing and it was hard to roll out safely. Clusters that had leaned entirely on PSP had to migrate to PSA or a third-party admission engine. The lesson: build your pod-admission policy on a supported, testable mechanism, and expect to layer PSA with a CEL policy or an engine like Kyverno or Gatekeeper.
ValidatingAdmissionPolicy: cluster-specific rules in CEL, no webhook
What it isA ValidatingAdmissionPolicy (VAP) lets you write admission rules in CEL (Common Expression Language) that the API server evaluates in-process, with no external webhook to run, secure and keep available. It went stable (admissionregistration.k8s.io/v1) in Kubernetes 1.30. This is how you express the rules PSA cannot: scoped hostPath, deny host namespaces cluster-wide, block runtime-socket mounts, require a RuntimeClass for a given namespace.
A policy has two parts: the ValidatingAdmissionPolicy (the CEL rules) and a ValidatingAdmissionPolicyBinding (where they apply, and whether failures Deny, Warn or Audit). CEL expressions see object (the incoming resource), oldObject, request, params (an optional parameter resource) and namespaceObject.
In practiceThis policy denies host namespaces, blocks every hostPath except read paths under /var/log/, and rejects privileged containers. It is bound everywhere except the system namespaces, with Deny and Audit:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: deny-host-access}
spec:
failurePolicy: Fail # ← if the policy can't evaluate, fail closed
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE","UPDATE"]
resources: ["pods"]
validations:
- expression: "!(object.spec.?hostPID.orValue(false) || object.spec.?hostNetwork.orValue(false) || object.spec.?hostIPC.orValue(false))"
message: "host namespaces (hostPID/hostNetwork/hostIPC) are not allowed"
- expression: "!has(object.spec.volumes) || object.spec.volumes.all(v, !has(v.hostPath) || v.hostPath.path.startsWith('/var/log/'))"
message: "hostPath volumes are only allowed under /var/log/" # ← the rule PSA can't express
- expression: "object.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || !c.securityContext.privileged)"
message: "privileged containers are not allowed"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: deny-host-access}
spec:
policyName: deny-host-access
validationActions: [Deny, Audit] # ← reject, and record in the audit log
matchResources:
namespaceSelector:
matchExpressions:
- {key: kubernetes.io/metadata.name, operator: NotIn, values: [kube-system, local-path-storage]}Applied to the lab cluster, it rejects a pod that mounts the node root and admits one that mounts only /var/log/pods:
$ kubectl -n vap-demo apply -f hostroot-pod.yaml # mounts hostPath: /
The pods "hostroot" is invalid: ValidatingAdmissionPolicy 'deny-host-access'
denied request: hostPath volumes are only allowed under /var/log/ ← rejected
$ kubectl -n vap-demo apply -f logs-pod.yaml # mounts hostPath: /var/log/pods
pod/logs created ← allowedThe Audit action writes the rejection into the API-server audit log, which is where your detection picks it up:
{ "verb": "create",
"requestURI": "/api/v1/namespaces/vap-demo/pods?fieldValidation=Strict",
"responseStatus": { "code": 422, "status": "Failure",
"message": "...denied request: hostPath volumes are only allowed under /var/log/" },
"annotations": {
"validation.policy.admission.k8s.io/validation_failure":
"[{\"message\":\"hostPath volumes are only allowed under /var/log/\",\"policy\":\"deny-host-access\",\"binding\":\"deny-host-access\",\"validationActions\":[\"Deny\",\"Audit\"]}]" ← alert on this
} }TipSee it yourselfThe cluster security lab builds a real
kindcluster with API-server audit logging and walks the RBAC + hostPath privesc path, Pod Security Admission blocking it, and aRuntimeClassrouting an untrusted pod to a gVisor handler. The PSA and VAP output above was captured from it.
🎯On the job. Prefer a
ValidatingAdmissionPolicyover an external validating webhook when you can: no extra service to run, no availability or certificate to manage, and a webhook that is down forces you to choose between failing open (no policy) or failing closed (no deploys). Keep the webhook only for logic CEL cannot express. SetfailurePolicy: Failso a policy that cannot evaluate rejects rather than waves the pod through, and alert on thevalidation.policy.admission.k8s.io/validation_failureaudit annotation, since a burst of those is someone probing what they are allowed to schedule.
Interview Questions
privileged: true and hostPID: true or hostNetwork: true?privileged: true is broad: it grants nearly the full capability set, removes the seccomp and device restrictions, and unmasks /proc and /sys, so the container is effectively root on the node. hostPID and hostNetwork are narrow: each shares exactly one host namespace, the node's process list or the node's network stack, and nothing else. The practical consequence is that hostNetwork bypasses NetworkPolicy and reaches node-local and metadata services, and hostPID plus a capability like CAP_SYS_PTRACE lets you inspect host processes, but neither is, on its own, the all-at-once undo that privileged is. All three are forbidden by the Baseline Pod Security profile.
With hostPID: true the pod shares the node's process-ID namespace, so it sees every host process, and if it also holds CAP_SYS_PTRACE it can attach to and read the memory of those processes, including privileged daemons, which is a path to whatever secrets or tokens they hold. I would not rely on hoping no pod sets that combination; I'd block it at admission. Baseline Pod Security already forbids hostPID, and a ValidatingAdmissionPolicy can additionally deny CAP_SYS_PTRACE and host namespaces cluster-wide, so the pod is never scheduled in the first place.
release_agent breakout. What was required, and why does cgroup v2 eliminate it?In cgroup v1 a cgroup could name a release_agent, a program the kernel runs as root on the host when the cgroup empties. CVE-2022-0492 was a missing capability check on setting that path, so a container with CAP_SYS_ADMIN or write access to the cgroup files could point it at a program of its choosing and get the host to run it as root. It is removed three ways: the kernel patch added the missing check, an enforcing LSM like SELinux blocked it, and cgroup v2 redesigned release notifications so there is no host-side agent program to point at, which deletes the whole class. The defender's takeaway is run cgroup v2 and don't grant CAP_SYS_ADMIN to workloads.
hostNetwork: true, how does it bypass egress controls and reach cloud metadata, and what do you do about it?NetworkPolicy is enforced on pod network namespaces, so a pod that shares the node's network namespace with hostNetwork: true simply isn't subject to it, and it sits directly on the node's interfaces, including the link-local route to the metadata endpoint at 169.254.169.254. That is the usual path to steal the node's cloud credentials. The fix is admission, not network policy: forbid hostNetwork with Baseline Pod Security and a VAP, and defend the metadata endpoint at the node with a host firewall rule and IMDSv2-style hop-limit and token requirements so even node-local access is gated.
privileged: true or mounting the containerd socket?Use rootless BuildKit: the build engine runs as an unprivileged user in a user namespace, so even root inside the builder maps to a non-root UID on the node, and it needs neither the privileged flag nor a mounted runtime socket. Mounting /var/run/containerd.sock into a build pod is equivalent to handing it node root, because it can ask the node to run any container with any host mount, so that belongs on every admission deny-list. For extra depth I'd schedule the builder onto a gVisor or microVM RuntimeClass so a BuildKit escape, like the 2024 entitlement and cache-mount CVEs fixed in v0.12.5, meets a second wall.
PSA replaced the removed PodSecurityPolicy in 1.25 and is far simpler: three fixed profiles, privileged, baseline and restricted, applied by namespace label in enforce, audit and warn modes. The gaps are that it is namespace-coarse and the profiles are fixed, so it cannot say "hostPath only under /var/log", cannot vary by image or team, and anyone who can label or create a namespace can downgrade the profile. So in a multi-tenant cluster you run PSA as the cheap first line, enforce at baseline and warn at restricted, and layer a ValidatingAdmissionPolicy or a policy engine on top for the cluster-specific rules and to control who can change namespace labels.
A ValidatingAdmissionPolicy, stable since Kubernetes 1.30, holds CEL rules the API server evaluates in-process, with a binding that says where they apply and whether a failure denies, warns or audits. For hostPath I write a CEL expression asserting every volume either has no hostPath or its path starts with an allowed prefix like /var/log/, and for privileged I assert no container sets securityContext.privileged, then bind it to all namespaces except kube-system with validationActions Deny and Audit. The advantages over a webhook are no extra service to keep available, no certificate to manage, and no fail-open-versus-fail-closed dilemma when a webhook is down; I set failurePolicy: Fail so an unevaluable policy rejects, and alert on the validation_failure audit annotation to catch probing.