How to Create Admission Webhooks
Build validating and mutating admission webhooks to enforce policies and modify resources. Implement custom admission controllers for Kubernetes.
How to Create Admission Webhooks
Admission webhooks intercept requests to the Kubernetes API before persistence, allowing you to validate or mutate resources. Validating webhooks reject non-compliant resources, while mutating webhooks modify resources automatically.
Webhook Types
- Validating Admission Webhook: Accepts or rejects requests
- Mutating Admission Webhook: Modifies requests before validation
Create Webhook Server (Go)
// main.go
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
http.HandleFunc("/validate", validateHandler)
http.HandleFunc("/mutate", mutateHandler)
http.HandleFunc("/health", healthHandler)
fmt.Println("Starting webhook server on :8443")
err := http.ListenAndServeTLS(":8443", "/certs/tls.crt", "/certs/tls.key", nil)
if err != nil {
panic(err)
}
}
func validateHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var admissionReview admissionv1.AdmissionReview
json.Unmarshal(body, &admissionReview)
var pod corev1.Pod
json.Unmarshal(admissionReview.Request.Object.Raw, &pod)
allowed := true
message := "Pod validation passed"
// Validate: Require resource limits
for _, container := range pod.Spec.Containers {
if container.Resources.Limits == nil {
allowed = false
message = fmt.Sprintf("Container %s must have resource limits", container.Name)
break
}
}
response := admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
APIVersion: "admission.k8s.io/v1",
Kind: "AdmissionReview",
},
Response: &admissionv1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: allowed,
Result: &metav1.Status{
Message: message,
},
},
}
respBytes, _ := json.Marshal(response)
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
}
func mutateHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var admissionReview admissionv1.AdmissionReview
json.Unmarshal(body, &admissionReview)
var pod corev1.Pod
json.Unmarshal(admissionReview.Request.Object.Raw, &pod)
// Mutation: Add default labels
patches := []map[string]interface{}{}
if pod.Labels == nil {
patches = append(patches, map[string]interface{}{
"op": "add",
"path": "/metadata/labels",
"value": map[string]string{},
})
}
if _, exists := pod.Labels["managed-by"]; !exists {
patches = append(patches, map[string]interface{}{
"op": "add",
"path": "/metadata/labels/managed-by",
"value": "admission-webhook",
})
}
patchBytes, _ := json.Marshal(patches)
patchType := admissionv1.PatchTypeJSONPatch
response := admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
APIVersion: "admission.k8s.io/v1",
Kind: "AdmissionReview",
},
Response: &admissionv1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: true,
PatchType: &patchType,
Patch: patchBytes,
},
}
respBytes, _ := json.Marshal(response)
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}Create Webhook Server (Python)
# webhook.py
from flask import Flask, request, jsonify
import json
import base64
app = Flask(__name__)
@app.route('/validate', methods=['POST'])
def validate():
admission_review = request.get_json()
pod = json.loads(
admission_review['request']['object']
) if isinstance(admission_review['request']['object'], str) else admission_review['request']['object']
allowed = True
message = "Validation passed"
# Validate: Check for required labels
required_labels = ['app', 'team']
labels = pod.get('metadata', {}).get('labels', {})
for label in required_labels:
if label not in labels:
allowed = False
message = f"Missing required label: {label}"
break
# Validate: No privileged containers
containers = pod.get('spec', {}).get('containers', [])
for container in containers:
security_context = container.get('securityContext', {})
if security_context.get('privileged', False):
allowed = False
message = f"Privileged containers not allowed: {container['name']}"
break
return jsonify({
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": admission_review['request']['uid'],
"allowed": allowed,
"status": {"message": message}
}
})
@app.route('/mutate', methods=['POST'])
def mutate():
admission_review = request.get_json()
pod = admission_review['request']['object']
patches = []
# Add default annotations
if 'annotations' not in pod.get('metadata', {}):
patches.append({
"op": "add",
"path": "/metadata/annotations",
"value": {}
})
patches.append({
"op": "add",
"path": "/metadata/annotations/webhook.kubernetes.io~1mutated",
"value": "true"
})
# Add resource requests if missing
containers = pod.get('spec', {}).get('containers', [])
for i, container in enumerate(containers):
if 'resources' not in container:
patches.append({
"op": "add",
"path": f"/spec/containers/{i}/resources",
"value": {
"requests": {"memory": "64Mi", "cpu": "50m"},
"limits": {"memory": "128Mi", "cpu": "100m"}
}
})
patch_base64 = base64.b64encode(json.dumps(patches).encode()).decode()
return jsonify({
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": admission_review['request']['uid'],
"allowed": True,
"patchType": "JSONPatch",
"patch": patch_base64
}
})
@app.route('/health', methods=['GET'])
def health():
return "OK", 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8443, ssl_context=('/certs/tls.crt', '/certs/tls.key'))Generate TLS Certificates
#!/bin/bash
# generate-certs.sh
SERVICE_NAME=webhook-service
NAMESPACE=webhook-system
SECRET_NAME=webhook-tls
# Generate CA
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -days 365 -out ca.crt -subj "/CN=Admission Webhook CA"
# Generate server certificate
cat > server.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${SERVICE_NAME}
DNS.2 = ${SERVICE_NAME}.${NAMESPACE}
DNS.3 = ${SERVICE_NAME}.${NAMESPACE}.svc
DNS.4 = ${SERVICE_NAME}.${NAMESPACE}.svc.cluster.local
EOF
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=${SERVICE_NAME}.${NAMESPACE}.svc" -config server.conf
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -extensions v3_req -extfile server.conf
# Create Kubernetes secret
kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret tls ${SECRET_NAME} \
--cert=server.crt \
--key=server.key \
-n ${NAMESPACE} \
--dry-run=client -o yaml | kubectl apply -f -
# Get CA bundle for webhook config
CA_BUNDLE=$(cat ca.crt | base64 | tr -d '\n')
echo "CA_BUNDLE: ${CA_BUNDLE}"Deploy Webhook Server
# webhook-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: admission-webhook
namespace: webhook-system
spec:
replicas: 2
selector:
matchLabels:
app: admission-webhook
template:
metadata:
labels:
app: admission-webhook
spec:
containers:
- name: webhook
image: myregistry/admission-webhook:v1
ports:
- containerPort: 8443
volumeMounts:
- name: tls-certs
mountPath: /certs
readOnly: true
readinessProbe:
httpGet:
path: /health
port: 8443
scheme: HTTPS
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: tls-certs
secret:
secretName: webhook-tls
---
apiVersion: v1
kind: Service
metadata:
name: webhook-service
namespace: webhook-system
spec:
selector:
app: admission-webhook
ports:
- port: 443
targetPort: 8443Register Validating Webhook
# validating-webhook-config.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-validation-webhook
webhooks:
- name: pod-validator.example.com
clientConfig:
service:
name: webhook-service
namespace: webhook-system
path: /validate
caBundle: <BASE64_ENCODED_CA_CERT>
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail # or Ignore
namespaceSelector:
matchExpressions:
- key: webhook-enabled
operator: In
values: ["true"]Register Mutating Webhook
# mutating-webhook-config.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: pod-mutation-webhook
webhooks:
- name: pod-mutator.example.com
clientConfig:
service:
name: webhook-service
namespace: webhook-system
path: /mutate
caBundle: <BASE64_ENCODED_CA_CERT>
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Ignore
reinvocationPolicy: Never
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "webhook-system"]Using cert-manager for Certificates
# cert-manager-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: webhook-cert
namespace: webhook-system
spec:
secretName: webhook-tls
dnsNames:
- webhook-service
- webhook-service.webhook-system
- webhook-service.webhook-system.svc
- webhook-service.webhook-system.svc.cluster.local
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}Test Webhooks
# Test validating webhook - should fail without labels
kubectl run test-pod --image=nginx
# Test with required labels - should succeed
kubectl run test-pod --image=nginx --labels="app=test,team=platform"
# Check mutation
kubectl get pod test-pod -o jsonpath='{.metadata.annotations}'
# Debug webhook
kubectl logs -n webhook-system -l app=admission-webhook
kubectl get events --field-selector reason=FailedCreateSummary
Admission webhooks provide powerful customization for the Kubernetes API. Mutating webhooks run first to modify resources, then validating webhooks check compliance. Always use TLS, implement health checks, set appropriate failurePolicy, and exclude system namespaces to prevent cluster lockout. Use cert-manager to automate certificate management.
📘 Go Further with Kubernetes Recipes
Love this recipe? There’s so much more! This is just one of 100+ hands-on recipes in our comprehensive Kubernetes Recipes book.
Inside the book, you’ll master:
- ✅ Production-ready deployment strategies
- ✅ Advanced networking and security patterns
- ✅ Observability, monitoring, and troubleshooting
- ✅ Real-world best practices from industry experts
“The practical, recipe-based approach made complex Kubernetes concepts finally click for me.”
👉 Get Your Copy Now — Start building production-grade Kubernetes skills today!
📘 Get All 100+ Recipes in One Book
Stop searching — get every production-ready pattern with detailed explanations, best practices, and copy-paste YAML.
Want More Kubernetes Recipes?
This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.