51

Admission Controllers

Video: Day 51 — Admission Controllers & Webhooks • Theme: the gatekeepers that mutate and validate every request after authn/authz.

Key terms

TermMeaning
Admission controllerCode that intercepts requests after authn/authz, before storage
Mutating webhookCan change the object (add defaults, sidecars, labels)
Validating webhookCan only accept or reject the object
AdmissionReviewThe request/response object exchanged with a webhook
failurePolicyFail or Ignore if the webhook is unreachable
Built-in controllerCompiled-in admission plugin (e.g. NamespaceLifecycle)
Dynamic admissionWebhook-based controllers registered at runtime

Problem & solution

Authentication tells the api-server who you are; authorization tells it what you may do. But you still need policy on the content of objects: inject a sidecar, force resource limits, block :latest images, deny privileged pods. RBAC cannot express that.

Solution: Admission controllers run inside the api-server request path, after authn/authz and after schema validation, but before the object is persisted to etcd. They can mutate the object and/or validate it.

The analogy

After a trucker proves who they are and what they may bring in, a customs inspector still examines the actual paperwork before anything is filed: a first inspector may stamp-correct a form, adding a missing seal or a default value, and a second may reject it outright if it breaks the rules. Nothing reaches the port ledger until the papers pass. In Kubernetes that inspector is an admission controller, where a mutating webhook stamp-corrects the object, a validating webhook accepts or rejects it, and only then is it written to etcd.

Where this fits in the cluster

The same cluster entities appear in every day's notes; the <== marks what this day touches.

The request path: where admission sits

Admission has two phases, and order matters: all mutating controllers run first (so later validators see the final object), then schema validation runs, then all validating controllers run.

If any controller rejects, the whole request fails and nothing is stored.

Built-in admission controllers

The api-server compiles in many plugins, enabled via a flag. Several are on by default in a kubeadm cluster.

# inspect what the api-server has enabled
ps -ef | grep kube-apiserver | tr ' ' '\n' | grep admission

# typical flag in /etc/kubernetes/manifests/kube-apiserver.yaml
#   --enable-admission-plugins=NodeRestriction,...

Common built-ins:

  • NamespaceLifecycle — blocks objects in terminating/non-existent namespaces.
  • LimitRanger — applies a namespace LimitRange (default/req limits).
  • ResourceQuota — enforces namespace quotas.
  • ServiceAccount — auto-mounts the default service account token.
  • NodeRestriction — limits what a kubelet can modify.
  • DefaultStorageClass — stamps the default class on PVCs without one.
  • MutatingAdmissionWebhook and ValidatingAdmissionWebhook — call your webhooks.

Dynamic admission with webhooks

Two special built-ins let you plug in your own logic at runtime. You register a configuration that tells the api-server which operations to send to your HTTPS webhook server as an AdmissionReview.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: deny-privileged-pods
webhooks:
  - name: pods.policy.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Fail                 # block requests if the webhook is down
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
    clientConfig:
      service:
        name: policy-webhook
        namespace: policy-system
        path: /validate-pods
      caBundle: <base64-CA-cert>        # api-server verifies the webhook TLS
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations
kubectl describe validatingwebhookconfiguration deny-privileged-pods

The webhook returns an AdmissionReview with allowed: true|false. A mutating webhook returns a base64 JSONPatch in patch to modify the object.

Native validation: ValidatingAdmissionPolicy (CEL)

Since 1.30, ValidatingAdmissionPolicy (GA) lets you write validation rules in CEL directly in the api-server — no external webhook server to run, host, or keep TLS-current.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-resource-limits
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: "object.spec.template.spec.containers.all(c, has(c.resources.limits))"
      message: "every container must set resource limits"

Use cases and failure modes

  • Policy/governance: block :latest, require labels, deny privileged pods (Kyverno and OPA Gatekeeper are webhook-based admission systems).
  • Injection: service meshes (Istio/Linkerd) use a mutating webhook to add the proxy sidecar automatically.
  • Failure policy: Fail is safe-by-default but can wedge the cluster if your webhook is down (you may be unable to create the very pods that back it). Scope rules tightly and exempt critical namespaces with namespaceSelector.

End-to-end: a pod create through admission

The full path a CREATE pod takes through the two webhook phases.

End-to-end example: a validating webhook that requires a label

A complete walkthrough: deploy a webhook service that rejects any Pod missing the label team, register a ValidatingWebhookConfiguration wired to its CA, then show an allowed apply versus a rejected one.

Step 1 — deploy the webhook server (Deployment + Service on 443).

# webhook-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: label-webhook
  namespace: policy-system
spec:
  replicas: 1
  selector:
    matchLabels: { app: label-webhook }
  template:
    metadata:
      labels: { app: label-webhook }
    spec:
      containers:
        - name: server
          image: ghcr.io/example/label-webhook:1.0
          args: ["--tls-cert=/certs/tls.crt", "--tls-key=/certs/tls.key"]
          ports:
            - containerPort: 8443
          volumeMounts:
            - name: certs
              mountPath: /certs
              readOnly: true
      volumes:
        - name: certs
          secret:
            secretName: label-webhook-tls
---
apiVersion: v1
kind: Service
metadata:
  name: label-webhook
  namespace: policy-system
spec:
  selector: { app: label-webhook }
  ports:
    - port: 443
      targetPort: 8443

The server answers POST /validate-pods with an AdmissionReview: it returns allowed: false when request.object.metadata.labels.team is absent.

kubectl create namespace policy-system
# create the serving cert as a Secret the Deployment mounts (cert-manager or manual)
kubectl -n policy-system create secret tls label-webhook-tls \
  --cert=webhook.crt --key=webhook.key
kubectl apply -f webhook-server.yaml
kubectl -n policy-system rollout status deploy/label-webhook
# deployment "label-webhook" successfully rolled out

Step 2 — register the ValidatingWebhookConfiguration.

# webhook-config.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: require-team-label
webhooks:
  - name: pods.require-team.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Fail
    timeoutSeconds: 5
    namespaceSelector:
      matchLabels:
        team-policy: enforce        # only act in opted-in namespaces
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
    clientConfig:
      service:
        name: label-webhook
        namespace: policy-system
        path: /validate-pods
        port: 443
      caBundle: LS0tLS1CRUdJTiBD...   # base64 of the CA that signed the serving cert
# inject the CA bundle the api-server uses to trust the webhook TLS
CA=$(base64 -w0 ca.crt)
sed -i "s|caBundle:.*|caBundle: ${CA}|" webhook-config.yaml
kubectl apply -f webhook-config.yaml
# validatingwebhookconfiguration.admissionregistration.k8s.io/require-team-label created

kubectl label namespace default team-policy=enforce
kubectl get validatingwebhookconfiguration require-team-label

Step 3 — allowed apply (the Pod carries the required label).

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: good-pod
  namespace: default
  labels:
    team: payments
spec:
  containers:
    - name: app
      image: nginx:1.27
EOF
# pod/good-pod created

kubectl get pod good-pod
# NAME       READY   STATUS    RESTARTS   AGE
# good-pod   1/1     Running   0          6s

Step 4 — rejected apply (label missing -> webhook denies, nothing stored).

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: bad-pod
  namespace: default
spec:
  containers:
    - name: app
      image: nginx:1.27
EOF
# Error from server: admission webhook "pods.require-team.example.com" denied
#   the request: pod is missing required label "team"

kubectl get pod bad-pod
# Error from server (NotFound): pods "bad-pod" not found

Step 5 — confirm failurePolicy behaviour and scope.

# scale the webhook to zero and retry: failurePolicy Fail blocks pod creates
kubectl -n policy-system scale deploy/label-webhook --replicas=0
kubectl run probe --image=nginx -n default
# Error from server: failed calling webhook ... connection refused
kubectl -n policy-system scale deploy/label-webhook --replicas=1

# namespaces without the team-policy=enforce label are never sent to the webhook
kubectl create namespace sandbox
kubectl run free --image=nginx -n sandbox   # allowed: selector excludes this ns

Key takeaways

  • Admission runs after authn/authz, before etcd — it governs object content.
  • Mutating webhooks run first (change objects); validating webhooks run last (accept/reject).
  • Many controllers are built in (LimitRanger, ResourceQuota, NodeRestriction, ...).
  • Webhooks add dynamic policy at runtime; mind failurePolicy and tight rules.
  • ValidatingAdmissionPolicy (CEL) does in-process validation with no webhook server.

Checklist

  • [ ] Listed --enable-admission-plugins on the api-server
  • [ ] Explained mutating-before-validating ordering
  • [ ] Created a ValidatingWebhookConfiguration and read its clientConfig
  • [ ] Described the risk of failurePolicy: Fail with a down webhook
  • [ ] Wrote a CEL ValidatingAdmissionPolicy rule