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

API Server Admission stage AdmissionReview JSON HTTPS POST Webhook Service (your code in a Pod) Receives: AdmissionReview Returns: allow/deny/patch Response Result: allowed: true/false + optional patch MutatingWebhookConfiguration / ValidatingWebhookConfiguration

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"}]
  }
}
The webhook is just an HTTPS endpoint that receives JSON and returns JSON. You can write it in any language (Go, Python, Node.js). It's deployed as a regular K8s Deployment + Service. The API server calls it via the Service's ClusterIP. TLS is mandatory — the API server verifies the webhook's certificate.

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

FieldPurposeImpact
rulesWhat API requests trigger the webhookToo broad = performance hit; too narrow = gaps
failurePolicyWhat happens if webhook is unreachableFail = reject all (safe but blocks cluster). Ignore = allow all (available but unsafe)
namespaceSelectorOnly intercept requests from matching namespacesExempt kube-system to avoid bootstrap loops
timeoutSecondsMax time to wait for webhook responseLow = fast failures. High = tolerant of slow webhooks.
sideEffectsWhether webhook has side effects beyond the admission decisionMust be None to work with dry-run
caBundleCA certificate to validate webhook's TLS certAPI server won't call webhook without valid TLS

failurePolicy — The Critical Decision

PolicyWebhook Down →Use When
FailALL matching requests are rejectedSecurity-critical policies (can't allow unvalidated requests)
IgnoreALL 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

PolicyBehavior
Never (default)Webhook called once. Other mutators' changes not visible.
IfNeededRe-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

ApproachHowPros/Cons
cert-managerAnnotate the webhook config; cert-manager injects CA + issues certs✅ Automatic rotation. Most common approach.
Self-signed + caBundleGenerate self-signed cert, base64 CA into webhook configSimple but manual rotation
K8s CAUse CertificateSigningRequest to get a cert from the cluster CANo 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
Always use cert-manager for webhook TLS in production. Manual cert management is error-prone — an expired webhook cert with 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
CKS exam: if you're asked why Pods can't be created and you see a webhook error, check: (1) Is the webhook Service reachable? (2) Is the caBundle correct? (3) Is the webhook Pod running? (4) Does failurePolicy: Fail match the symptom (all requests blocked)? Quick fix for the exam: delete the webhook config, fix the issue, then re-apply.

Summary

ConceptKey Point
WebhookExternal HTTPS service called by API server during admission
MutatingWebhookConfigurationCan modify objects (JSON Patch response)
ValidatingWebhookConfigurationCan only accept or reject
AdmissionReviewJSON protocol: API server sends request, webhook returns response
failurePolicy: FailReject all if webhook unreachable — safe but can lock cluster
failurePolicy: IgnoreAllow all if webhook unreachable — available but bypasses policy
namespaceSelectorFilter which namespaces trigger the webhook (always exclude kube-system)
TLS requiredcaBundle must match the webhook's serving cert (use cert-manager)
Bootstrap deadlockWebhook matching its own namespace can prevent self-recovery
timeoutSecondsMax 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?

All matching API requests are rejected. With 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?

The response includes "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?

Bootstrap deadlock. The webhook Pod needs to be recreated, but creating Pods requires the webhook (which is down, and failurePolicy: Fail rejects). The webhook can't start because starting it requires the webhook to be running. Fix: exclude the webhook's namespace with namespaceSelector matching kubernetes.io/metadata.name NotIn [webhook-system].

Q4: Why must webhooks use HTTPS? What specifies the CA?

The API server sends sensitive data (full object specs, including Secrets) to the webhook. HTTPS ensures this data is encrypted in transit and the API server is talking to the real webhook (not a MITM). The CA is specified in the 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?

Webhook A runs and adds a label. Webhook B runs on the modified object. Since B has 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?

Use 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.