Dynamic admission webhooks let you run your own code during the admission pipeline. The API server calls your webhook service (over HTTPS), sends the admission request, and your service responds with either an approval, a rejection, or a patch (mutation). This is how the entire policy and service mesh ecosystem works.
1. Webhook Architecture
The AdmissionReview Protocol
# API server sends (POST body):
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "abc-123",
"kind": {"group": "apps", "version": "v1", "kind": "Deployment"},
"operation": "CREATE",
"namespace": "production",
"userInfo": {"username": "jane", "groups": ["developers"]},
"object": { ... full Deployment spec ... },
"oldObject": null // populated for UPDATE operations
}
}
# Webhook responds:
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "abc-123", // must match request UID
"allowed": true, // or false to reject
"patchType": "JSONPatch", // only for mutating webhooks
"patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2xhYmVscy90ZWFtIiwidmFsdWUiOiJwbGF0Zm9ybSJ9XQ=="
// base64-encoded JSON Patch: [{"op":"add","path":"/metadata/labels/team","value":"platform"}]
}
}
2. Webhook Configuration
ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: policy-validator
webhooks:
- name: validate.policy.example.com
admissionReviewVersions: ["v1"]
# WHAT to intercept:
rules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
resources: ["deployments"]
operations: ["CREATE", "UPDATE"]
scope: Namespaced # or Cluster, or "*"
# WHERE to send:
clientConfig:
service:
name: policy-webhook # K8s Service (in-cluster)
namespace: webhook-system
path: /validate
port: 443
caBundle: LS0tLS1... # CA cert to verify webhook's TLS
# WHEN it fails:
failurePolicy: Fail # or Ignore
# SCOPE control:
namespaceSelector: # Only intercept from matching namespaces
matchExpressions:
- key: environment
operator: In
values: ["production", "staging"]
objectSelector: # Only intercept objects with matching labels
matchLabels:
policy-check: "true"
# PERFORMANCE:
timeoutSeconds: 5 # Max wait (default: 10s, max: 30s)
sideEffects: None # None, NoneOnDryRun (required for dry-run support)
matchPolicy: Equivalent # or Exact
Key Configuration Fields
| Field | Purpose | Impact |
|---|---|---|
rules | What API requests trigger the webhook | Too broad = performance hit; too narrow = gaps |
failurePolicy | What happens if webhook is unreachable | Fail = reject all (safe but blocks cluster). Ignore = allow all (available but unsafe) |
namespaceSelector | Only intercept requests from matching namespaces | Exempt kube-system to avoid bootstrap loops |
timeoutSeconds | Max time to wait for webhook response | Low = fast failures. High = tolerant of slow webhooks. |
sideEffects | Whether webhook has side effects beyond the admission decision | Must be None to work with dry-run |
caBundle | CA certificate to validate webhook's TLS cert | API server won't call webhook without valid TLS |
failurePolicy — The Critical Decision
| Policy | Webhook Down → | Use When |
|---|---|---|
Fail | ALL matching requests are rejected | Security-critical policies (can't allow unvalidated requests) |
Ignore | ALL matching requests are allowed (bypass) | Non-critical (monitoring, labeling) — availability > enforcement |
failurePolicy: Fail with a broken webhook = cluster lockdown. If the webhook Pod crashes or its Service is misconfigured, ALL deployments, scaling, and Pod creation matching the rules are blocked. The cluster becomes unusable. Always: (1) Run webhooks as HA (multiple replicas), (2) Exclude kube-system via namespaceSelector, (3) Monitor webhook latency and error rates.
3. Mutating Webhooks
Mutating webhooks respond with a JSON Patch that modifies the object:
# MutatingWebhookConfiguration (similar structure to Validating):
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: sidecar-injector
webhooks:
- name: inject.sidecar.example.com
admissionReviewVersions: ["v1"]
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE"]
clientConfig:
service:
name: sidecar-injector
namespace: system
path: /inject
caBundle: LS0tLS1...
failurePolicy: Ignore # Don't block Pod creation if injector is down
reinvocationPolicy: IfNeeded # Re-call if another mutator changed the object
namespaceSelector:
matchLabels:
sidecar-injection: enabled # Only inject in labeled namespaces
Mutation Response (JSON Patch)
# Webhook returns patches like:
[
{"op": "add", "path": "/spec/containers/-", "value": {
"name": "envoy-sidecar",
"image": "envoyproxy/envoy:v1.28",
"ports": [{"containerPort": 15001}]
}},
{"op": "add", "path": "/metadata/labels/injected", "value": "true"},
{"op": "add", "path": "/metadata/annotations/injector-version", "value": "v2.1"}
]
# Base64-encode this array → set as response.patch
reinvocationPolicy
| Policy | Behavior |
|---|---|
Never (default) | Webhook called once. Other mutators' changes not visible. |
IfNeeded | Re-call this webhook if another mutating webhook modified the object after it ran. |
4. TLS for Webhooks
Webhooks MUST serve HTTPS. The API server validates the webhook's TLS cert against the caBundle in the configuration.
Options for Managing Webhook TLS
| Approach | How | Pros/Cons |
|---|---|---|
| cert-manager | Annotate the webhook config; cert-manager injects CA + issues certs | ✅ Automatic rotation. Most common approach. |
| Self-signed + caBundle | Generate self-signed cert, base64 CA into webhook config | Simple but manual rotation |
| K8s CA | Use CertificateSigningRequest to get a cert from the cluster CA | No external tool needed |
# cert-manager annotation approach:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: policy-validator
annotations:
cert-manager.io/inject-ca-from: webhook-system/webhook-cert
# cert-manager automatically:
# 1. Issues a TLS cert for the webhook Service
# 2. Injects the CA bundle into the webhook configuration
# 3. Rotates before expiry
failurePolicy: Fail locks your cluster. cert-manager handles rotation automatically. It's the de facto standard for webhook certificate lifecycle.
5. Operational Concerns
Avoiding Bootstrap Deadlocks
# Problem: Webhook in namespace "system" matches ALL Pod creates.
# If the webhook Pod crashes → no Pods can be created → webhook can't restart → DEADLOCK
# Solutions:
# 1. Exclude the webhook's own namespace:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["webhook-system", "kube-system"]
# 2. Use objectSelector to skip specific Pods:
objectSelector:
matchExpressions:
- key: skip-webhook
operator: DoesNotExist
# 3. Use failurePolicy: Ignore for non-critical webhooks
Performance Impact
# Every matching API request adds: # - Network round-trip to webhook service (~1-5ms in-cluster) # - Webhook processing time (depends on your code) # - TLS handshake (amortized with connection pooling) # Best practices: # - Set timeoutSeconds low (3-5s) for latency-sensitive paths # - Keep webhook logic fast (< 100ms) # - Run multiple replicas (HA + distribute load) # - Use namespaceSelector/objectSelector to narrow scope # - Monitor: webhook_admission_duration_seconds metric
Debugging Webhook Issues
# Check if webhook is registered: kubectl get validatingwebhookconfigurations kubectl get mutatingwebhookconfigurations # Check webhook Service/Endpoints: kubectl get svc -n webhook-system kubectl get endpoints -n webhook-system # Check webhook Pod logs: kubectl logs -n webhook-system -l app=webhook --tail=50 # Test with dry-run (triggers webhooks with sideEffects: None): kubectl apply -f deploy.yaml --dry-run=server # If rejected → webhook denied it # Temporarily disable a broken webhook: kubectl delete validatingwebhookconfiguration policy-validator # ⚠️ Removes all enforcement — re-apply when fixed
failurePolicy: Fail match the symptom (all requests blocked)? Quick fix for the exam: delete the webhook config, fix the issue, then re-apply.
Summary
| Concept | Key Point |
|---|---|
| Webhook | External HTTPS service called by API server during admission |
| MutatingWebhookConfiguration | Can modify objects (JSON Patch response) |
| ValidatingWebhookConfiguration | Can only accept or reject |
| AdmissionReview | JSON protocol: API server sends request, webhook returns response |
| failurePolicy: Fail | Reject all if webhook unreachable — safe but can lock cluster |
| failurePolicy: Ignore | Allow all if webhook unreachable — available but bypasses policy |
| namespaceSelector | Filter which namespaces trigger the webhook (always exclude kube-system) |
| TLS required | caBundle must match the webhook's serving cert (use cert-manager) |
| Bootstrap deadlock | Webhook matching its own namespace can prevent self-recovery |
| timeoutSeconds | Max wait time (default 10s, recommended 3-5s for production) |
📝 Quiz: Dynamic Admission Webhooks
Q1: A ValidatingWebhookConfiguration has failurePolicy: Fail. The webhook Pod is OOMKilled and not restarting. What happens to new Deployments?
Fail policy, if the API server can't reach the webhook, it treats the admission as denied. No new Deployments, Pods, or other matching resources can be created until the webhook is back or the configuration is deleted.Q2: A MutatingWebhook wants to add a sidecar container to a Pod. What does its response look like?
"allowed": true, "patchType": "JSONPatch", and a base64-encoded "patch" field containing a JSON Patch array:[{"op":"add","path":"/spec/containers/-","value":{"name":"sidecar","image":"envoy:latest"}}]The /containers/- path appends to the array. The API server applies this patch to the Pod spec before persisting.Q3: Your webhook matches all Pod creates including kube-system. The webhook Pod crashes. Why can't it recover?
namespaceSelector matching kubernetes.io/metadata.name NotIn [webhook-system].Q4: Why must webhooks use HTTPS? What specifies the CA?
caBundle field of the webhook configuration — the API server uses it to verify the webhook's TLS certificate.Q5: Two mutating webhooks both want to modify the same Pod. Webhook A runs first and adds a label. Webhook B has reinvocationPolicy: IfNeeded. What happens?
reinvocationPolicy: IfNeeded, if B's modification changes something that A's rules match, A is called again on the twice-modified object. This ensures ordering doesn't cause missed mutations. With Never, A would not be re-invoked.Q6: You need a webhook to validate Pods in production and staging but NOT in dev. How do you configure this?
namespaceSelector in the webhook configuration:namespaceSelector:
matchExpressions:
- key: environment
operator: In
values: ["production", "staging"]Label the namespaces accordingly (kubectl label ns production environment=production). The dev namespace won't have the matching label, so the webhook won't be called for dev Pods.