Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Every object submitted to the Kubernetes API server — a Pod, a Deployment, a ConfigMap — passes through a pipeline of authentication, authorization, and admission control before it is persisted to etcd. Admission controllers are the last stage of that pipeline: they can mutate an incoming object (mutating webhooks) or reject it outright (validating webhooks) based on policy, after RBAC has already decided the requester is *allowed* to make the request at all. This is the layer where “the user has permission to create Pods” gets narrowed down to “but not a privileged Pod that mounts the host filesystem.”
Pod Security is the specific slice of admission control concerned with the security-sensitive fields of a Pod spec: host namespaces, privileged containers, host path mounts, capabilities, and the user/group a container runs as. Since Kubernetes 1.25, this is enforced natively by Pod Security Admission (PSA), a built-in admission controller that replaced the older, more complex PodSecurityPolicy (PSP) object, which was deprecated in 1.21 and removed in 1.25. Getting this layer right matters because a single over-privileged Pod — one running as root with privileged: true or a host path mount — is frequently the difference between a contained application compromise and a full node or cluster takeover.
Attack Prerequisites
From an attacker or auditor’s perspective, the interesting cases are where admission control is absent, permissive, or bypassable:
- No Pod Security Admission label on the namespace (or a label of
privileged), meaning workloads can requestprivileged: true,hostNetwork,hostPID, or arbitraryhostPathmounts with nothing to stop them. - Ability to create or patch a Pod/Deployment/DaemonSet in a namespace — via stolen credentials, an over-permissioned ServiceAccount token, or CI/CD pipeline access — that is not itself gated by a stricter external policy engine (Kyverno/OPA Gatekeeper).
- Policy configured in
auditorwarnmode instead ofenforce, which logs or warns on a violation but still admits the object. - A policy engine installed but not yet covering all workload kinds (e.g. rules written only for
Podbut notCronJob/Job, which also create Pods indirectly).
How It Works
Pod Security Admission defines three increasingly strict profiles: privileged (unrestricted, effectively opts out), baseline (blocks known privilege-escalation vectors — privileged containers, host namespaces, most dangerous capabilities — while staying broadly compatible with common workloads), and restricted (baseline plus a hardened posture: mandatory non-root, a required seccomp profile, no capability additions, and no privilege escalation). Profiles are applied per namespace via labels, and each of the three enforcement modes — enforce, audit, warn — can carry a different profile at the same time, which is how teams typically roll out restricted in warn mode before flipping it to enforce.
Because PSA is itself just a built-in validating admission controller, it sits in the same admission chain as any custom webhook: mutating webhooks run first (so a Kyverno mutation could, for example, inject a default securityContext before PSA ever evaluates the object), then validating webhooks and built-in validators — including PSA — run and can reject the request outright. This is why organizations layer a policy engine like OPA Gatekeeper or Kyverno on top of PSA: PSA’s three fixed profiles cover the well-known privilege-escalation fields, but custom policy is needed for anything organization-specific — required labels, approved registries, resource limits, image signature verification — that PSA has no concept of.
At the mechanical level, both Gatekeeper and Kyverno register as ValidatingWebhookConfiguration (and optionally MutatingWebhookConfiguration) resources. The API server calls out to them over HTTPS with an AdmissionReview object containing the full incoming resource; the webhook returns allowed: true/false and an optional message, which is exactly the string a kubectl apply shows back to the user on rejection.
Practical Example / Configuration
Applying the restricted Pod Security Standard to a namespace is a label, not a separate object — this alone blocks the most dangerous fields cluster-wide for anything created in that namespace:
apiVersion: v1
kind: Namespace
metadata:
name: app
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
YAMLFor rules PSA does not express natively, a Kyverno ClusterPolicy gives the same enforcement in explicit, readable YAML — here, denying privileged containers and requiring runAsNonRoot:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-and-require-nonroot
spec:
validationFailureAction: Enforce
background: true
rules:
- name: no-privileged-containers
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
containers:
- securityContext:
=(privileged): "false"
- name: require-run-as-non-root
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Pods must set spec.securityContext.runAsNonRoot: true."
pattern:
spec:
securityContext:
runAsNonRoot: true
YAMLThe equivalent in OPA Gatekeeper is a ConstraintTemplate (the Rego policy logic) paired with a Constraint (what the policy applies to):
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequirednonroot
spec:
crd:
spec:
names:
kind: K8sRequiredNonRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirednonroot
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
c.securityContext.privileged == true
msg := sprintf("container %v must not run privileged", [c.name])
}
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not c.securityContext.runAsNonRoot == true
msg := sprintf("container %v must set runAsNonRoot: true", [c.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredNonRoot
metadata:
name: require-nonroot-and-unprivileged
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
YAMLWalkthrough / Exploitation
Apply the namespace label and policy first, then confirm enforcement with a Pod that should be rejected:
kubectl apply -f namespace.yaml
kubectl apply -f kyverno-policy.yaml
BashapiVersion: v1
kind: Pod
metadata:
name: bad-pod
namespace: app
spec:
containers:
- name: web
image: nginx:1.25
securityContext:
privileged: true
YAMLkubectl apply -f bad-pod.yaml
# Error from server (Forbidden): error when creating "bad-pod.yaml":
# pods "bad-pod" is forbidden: violates PodSecurity "restricted:latest":
# privileged (container "web" must not set securityContext.privileged=true),
# runAsNonRoot != true (pod or container "web" must set
# securityContext.runAsNonRoot=true)
BashA compliant Pod that passes both PSA restricted and the Kyverno policy explicitly sets the fields the profile requires rather than relying on defaults:
apiVersion: v1
kind: Pod
metadata:
name: good-pod
namespace: app
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginx:1.25
securityContext:
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop: ["ALL"]
YAMLNote: Rolling out
restrictedcluster-wide in one step routinely breaks existing workloads that assume root or extra capabilities. Setpod-security.kubernetes.io/warnand/audittorestrictedfirst, review the warnings andPolicyReport/audit annotations for a rollout period, fix the flagged workloads, and only then flipenforce.
Opsec: PSA and most policy-engine rules validate Pod specs at admission time, but a Pod created before the policy existed, or via a controller that bypasses normal creation paths, is not retroactively evaluated — pair admission control with periodic audit scans (
kubectl get pods -A -o jsonfed through the same policy, or the policy engine’s own audit mode) to catch drift in already-running workloads.
Detection and Defense
Admission control is a preventive layer; pairing it with visibility closes the loop on anything that slips through or predates the policy:
- Enforce
restrictedby default, reservingbaseline/privilegednamespaces for the specific system workloads (CNI, CSI drivers, monitoring agents) that genuinely need elevated access, and scope those narrowly. - Audit and warn before enforce — roll new policy out in
warn/auditmode, reviewPolicyReportobjects (Kyverno) or constraint audit results (Gatekeeper) for a full rollout window before switching toEnforce. - Fail closed on webhook unavailability — set
failurePolicy: Failon validating webhooks so a Gatekeeper/Kyverno outage blocks new deployments rather than silently admitting everything. - Runtime detection as a backstop — tools like Falco can flag a privileged or host-mounting container that actually starts running, catching anything admission control missed.
- CIS Benchmark auditing — run
kube-benchperiodically to check the API server’s own admission-controller configuration (e.g. that--enable-admission-pluginsincludes the expected set) in addition to workload-level policy.
Real-World Impact
PodSecurityPolicy’s removal in Kubernetes 1.25 left a real gap that many clusters upgraded through without a replacement in place, since PSP required an explicit migration rather than a drop-in equivalent. Clusters that never adopted Pod Security Admission or an external policy engine after that removal are a recurring finding in Kubernetes security assessments: namespaces with no pod-security.kubernetes.io labels at all default to privileged, meaning any workload with a container-creation ability can run as root with hostPath mounts or privileged: true — a well-understood path to node compromise and, in clusters with weak node-to-control-plane isolation, cluster-wide compromise.
Conclusion
Pod Security Admission gives every cluster a built-in, no-extra-install way to stop the most dangerous Pod fields at the door, and layering Kyverno or OPA Gatekeeper on top covers everything organization-specific that the three fixed profiles don’t. The combination only works if it is actually turned on namespace by namespace, rolled out through warn/audit before enforce, and backed by runtime detection for anything that predates the policy — admission control is the gate, not a substitute for watching what is already running.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments