Admission Controllers
Video: Day 51 — Admission Controllers & Webhooks • Theme: the gatekeepers that mutate and validate every request after authn/authz.
Key terms
| Term | Meaning |
|---|---|
| Admission controller | Code that intercepts requests after authn/authz, before storage |
| Mutating webhook | Can change the object (add defaults, sidecars, labels) |
| Validating webhook | Can only accept or reject the object |
| AdmissionReview | The request/response object exchanged with a webhook |
failurePolicy | Fail or Ignore if the webhook is unreachable |
| Built-in controller | Compiled-in admission plugin (e.g. NamespaceLifecycle) |
| Dynamic admission | Webhook-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 namespaceLimitRange(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.MutatingAdmissionWebhookandValidatingAdmissionWebhook— 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:
Failis 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). Scoperulestightly and exempt critical namespaces withnamespaceSelector.
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
failurePolicyand tightrules. - ValidatingAdmissionPolicy (CEL) does in-process validation with no webhook server.
Checklist
- [ ] Listed
--enable-admission-pluginson the api-server - [ ] Explained mutating-before-validating ordering
- [ ] Created a ValidatingWebhookConfiguration and read its
clientConfig - [ ] Described the risk of
failurePolicy: Failwith a down webhook - [ ] Wrote a CEL
ValidatingAdmissionPolicyrule