ServerlessBase Blog
  • Kubernetes Mutating and Validating Webhooks

    A comprehensive guide to understanding and implementing Kubernetes admission webhooks for controlling resource creation and modification

    Kubernetes Mutating and Validating Webhooks

    You've probably deployed a Kubernetes cluster and noticed that some resources get modified automatically when you create them. Maybe a label gets added, a default value gets set, or a validation check fails. This magic happens through Kubernetes admission webhooks.

    Admission webhooks are HTTP callbacks that receive admission requests and can either allow or deny them. They run after authentication and authorization but before the object is persisted to the API server. Understanding how they work is essential for building secure, automated Kubernetes clusters.

    What Are Admission Webhooks?

    Think of admission webhooks as gatekeepers standing between the API server and the cluster. When you submit a request to create or modify a resource, the API server first checks authentication and authorization. If those pass, it sends the request to registered admission webhooks.

    Each webhook can perform one of two actions:

    • Mutating: Modify the request before it's processed
    • Validating: Check if the request meets certain criteria

    A single webhook can do both, or you can have separate webhooks for each purpose. The API server calls them in the order they're registered, and all must succeed for the request to be processed.

    How Webhooks Work: The Request Flow

    The admission process follows a specific sequence. When you submit a request to the Kubernetes API server, it goes through these stages:

    1. Authentication: Verify the user's identity
    2. Authorization: Check if the user has permission
    3. Admission: Call registered webhooks
    4. Persistence: Save the object to etcd

    The admission phase is where mutating and validating webhooks operate. The API server sends a serialized AdmissionReview object to each webhook, and the webhook responds with an AdmissionResponse.

    # Example AdmissionReview request sent to webhook
    apiVersion: admission.k8s.io/v1
    kind: AdmissionReview
    request:
      uid: "12345678-1234-1234-1234-123456789012"
      kind:
        group: ""
        kind: "Pod"
        version: "v1"
      operation: "CREATE"
      object:
        metadata:
          name: "my-pod"
        spec:
          containers:
          - name: "app"
            image: "nginx:latest"
      dryRun: false
      options:
        admissionReviewVersions: ["v1"]
        kind: "CreateOptions"
        apiVersion: "admission.k8s.io/v1"

    The webhook receives this JSON, processes it, and returns an AdmissionResponse. If the webhook is mutating, it can include a patch field with a JSON patch that modifies the request object.

    Mutating Webhooks: Automatically Applying Defaults

    Mutating webhooks modify the request before it's processed. This is useful for applying defaults, injecting sidecar containers, or setting labels automatically.

    Consider a scenario where you want to ensure all pods have a specific security context. Instead of manually adding it to every pod definition, you can create a mutating webhook that injects it automatically.

    # Example mutating webhook configuration
    apiVersion: admissionregistration.k8s.io/v1
    kind: MutatingWebhookConfiguration
    metadata:
      name: "pod-security-context-mutator"
    webhooks:
    - name: "pod-security-context.example.com"
      rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
      admissionReviewVersions: ["v1"]
      clientConfig:
        service:
          name: "webhook-service"
          namespace: "default"
          path: "/mutate"
        caBundle: LS0tLS1CRUdJTi...
      sideEffects: None
      reinvocationPolicy: Never

    The webhook service implements the /mutate endpoint that receives the AdmissionReview and returns a patch. JSON patches use the RFC 6902 format, which specifies operations like add, replace, and remove.

    // Example webhook handler that adds a security context
    const express = require('express');
    const app = express();
     
    app.use(express.json());
     
    app.post('/mutate', async (req, res) => {
      const admissionReview = req.body;
     
      if (admissionReview.request.kind.kind === 'Pod') {
        const patch = [
          {
            op: 'add',
            path: '/spec/containers/0/securityContext',
            value: {
              runAsNonRoot: true,
              runAsUser: 1000,
              allowPrivilegeEscalation: false,
              capabilities: {
                drop: ['ALL']
              }
            }
          }
        ];
     
        const admissionResponse = {
          uid: admissionReview.request.uid,
          allowed: true,
          patchType: 'JSONPatch',
          patch: Buffer.from(JSON.stringify(patch)).toString('base64')
        };
     
        return res.json({
          apiVersion: 'admission.k8s.io/v1',
          kind: 'AdmissionReview',
          response: admissionResponse
        });
      }
     
      // Allow the request to proceed without modifications
      const admissionResponse = {
        uid: admissionReview.request.uid,
        allowed: true
      };
     
      return res.json({
        apiVersion: 'admission.k8s.io/v1',
        kind: 'AdmissionReview',
        response: admissionResponse
      });
    });
     
    app.listen(8443, () => {
      console.log('Webhook server running on port 8443');
    });

    This webhook adds a security context to the first container in the pod. The sideEffects: None field indicates the webhook doesn't modify external state, which is important for performance and idempotency.

    Validating Webhooks: Enforcing Policies

    Validating webhooks check if a request meets certain criteria and reject it if it doesn't. This is where you enforce security policies, resource quotas, and business rules.

    A common use case is enforcing that pods don't run with privileged containers. This prevents accidental privilege escalation and reduces the attack surface.

    # Example validating webhook configuration
    apiVersion: admissionregistration.k8s.io/v1
    kind: ValidatingWebhookConfiguration
    metadata:
      name: "privileged-pod-validator"
    webhooks:
    - name: "privileged-pod.example.com"
      rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
      admissionReviewVersions: ["v1"]
      clientConfig:
        service:
          name: "webhook-service"
          namespace: "default"
          path: "/validate"
        caBundle: LS0tLS1CRUdJTi...
      failurePolicy: Fail
      sideEffects: None

    The failurePolicy: Fail setting means the webhook will reject the request if it fails validation. You can also set it to Ignore to log warnings instead of rejecting the request.

    // Example validating webhook handler
    const express = require('express');
    const app = express();
     
    app.use(express.json());
     
    app.post('/validate', async (req, res) => {
      const admissionReview = req.body;
     
      if (admissionReview.request.kind.kind === 'Pod') {
        const pod = admissionReview.request.object;
     
        // Check if any container is privileged
        for (const container of pod.spec.containers) {
          if (container.securityContext?.privileged) {
            const admissionResponse = {
              uid: admissionReview.request.uid,
              allowed: false,
              status: {
                message: `Pod ${pod.metadata.name} contains a privileged container`,
                code: 403
              }
            };
     
            return res.json({
              apiVersion: 'admission.k8s.io/v1',
              kind: 'AdmissionReview',
              response: admissionResponse
            });
          }
        }
      }
     
      // Allow the request to proceed
      const admissionResponse = {
        uid: admissionReview.request.uid,
        allowed: true
      };
     
      return res.json({
        apiVersion: 'admission.k8s.io/v1',
        kind: 'AdmissionReview',
        response: admissionResponse
      });
    });
     
    app.listen(8443, () => {
      console.log('Webhook server running on port 8443');
    });

    This webhook rejects any pod that has privileged: true in its security context. The error message is returned to the user, making it clear why the request was denied.

    Mutating vs Validating: When to Use Each

    The choice between mutating and validating webhooks depends on what you're trying to achieve. Here's a comparison:

    AspectMutating WebhooksValidating Webhooks
    Primary PurposeApply defaults, inject configurationsEnforce policies, validate rules
    Request ImpactModifies the request objectOnly checks and rejects
    User ExperienceSeamless, automaticExplicit rejection with error messages
    Use CasesAdd labels, set defaults, inject sidecarsSecurity policies, resource constraints, compliance rules
    Can Be CombinedYes, with separate webhooksYes, with separate webhooks

    You'll often use both types together. For example, a mutating webhook might add a default resource limit, and a validating webhook might ensure that limit doesn't exceed a maximum value.

    Webhook Failure Policies

    The failurePolicy setting determines what happens when a webhook fails. This can occur due to network issues, webhook service unavailability, or webhook implementation errors.

    Failure PolicyBehavior
    FailReject the request immediately
    IgnoreLog a warning and allow the request to proceed

    The default is Fail, which is generally safer for security-critical webhooks. However, Ignore can be useful for non-critical validations where you want to avoid blocking legitimate requests during temporary issues.

    # Example with Ignore policy for non-critical validation
    apiVersion: admissionregistration.k8s.io/v1
    kind: ValidatingWebhookConfiguration
    metadata:
      name: "non-critical-validator"
    webhooks:
    - name: "non-critical.example.com"
      failurePolicy: Ignore
      rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
      admissionReviewVersions: ["v1"]
      clientConfig:
        service:
          name: "webhook-service"
          namespace: "default"
          path: "/validate"
        caBundle: LS0tLS1CRUdJTi...

    In this example, if the webhook is unavailable, pods can still be created. The webhook will log a warning, but the request won't be blocked.

    Webhook Reinvocation Policy

    The reinvocationPolicy setting controls whether the webhook is called multiple times during the admission process. This is important for webhooks that need to see the results of other webhooks.

    Reinvocation PolicyBehavior
    NeverWebhook is only called once
    IfNeededWebhook is called again if other mutating webhooks modify the request

    The default is Never, which is sufficient for most use cases. IfNeeded is useful when you have multiple mutating webhooks and want to ensure your webhook sees the final state of the request.

    # Example with IfNeeded reinvocation
    apiVersion: admissionregistration.k8s.io/v1
    kind: MutatingWebhookConfiguration
    metadata:
      name: "reinvocable-mutator"
    webhooks:
    - name: "reinvocable.example.com"
      reinvocationPolicy: IfNeeded
      rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
      admissionReviewVersions: ["v1"]
      clientConfig:
        service:
          name: "webhook-service"
          namespace: "default"
          path: "/mutate"
        caBundle: LS0tLS1CRUdJTi...

    With IfNeeded, the webhook is called again if another mutating webhook modifies the request. This allows you to apply additional changes based on the final state.

    Webhook Service Implementation

    The webhook service needs to be accessible from the Kubernetes API server. You can deploy it as a Deployment with a Service, or use an external HTTP server.

    For development, you can use a simple HTTP server. For production, consider using a framework like Express.js or implementing a more robust service with proper error handling and metrics.

    // Complete webhook service with error handling
    const express = require('express');
    const app = express();
     
    app.use(express.json({ limit: '1mb' }));
     
    app.post('/mutate', async (req, res) => {
      try {
        const admissionReview = req.body;
     
        if (!admissionReview.request) {
          return res.status(400).json({ error: 'Invalid request' });
        }
     
        const response = await processAdmissionRequest(admissionReview);
     
        return res.json(response);
      } catch (error) {
        console.error('Webhook error:', error);
     
        // Return an error response to the API server
        const admissionResponse = {
          uid: req.body.request?.uid,
          allowed: false,
          status: {
            message: `Webhook error: ${error.message}`,
            code: 500
          }
        };
     
        return res.json({
          apiVersion: 'admission.k8s.io/v1',
          kind: 'AdmissionReview',
          response: admissionResponse
        });
      }
    });
     
    async function processAdmissionRequest(admissionReview) {
      const { request } = admissionReview;
     
      if (request.kind.kind === 'Pod') {
        const patch = await generatePodPatch(request.object);
     
        return {
          apiVersion: 'admission.k8s.io/v1',
          kind: 'AdmissionReview',
          response: {
            uid: request.uid,
            allowed: true,
            patchType: 'JSONPatch',
            patch: Buffer.from(JSON.stringify(patch)).toString('base64')
          }
        };
      }
     
      return {
        apiVersion: 'admission.k8s.io/v1',
        kind: 'AdmissionReview',
        response: {
          uid: request.uid,
          allowed: true
        }
      };
    }
     
    async function generatePodPatch(pod) {
      const patch = [];
     
      for (const container of pod.spec.containers) {
        if (!container.securityContext) {
          patch.push({
            op: 'add',
            path: '/spec/containers/-/securityContext',
            value: {
              runAsNonRoot: true,
              runAsUser: 1000,
              allowPrivilegeEscalation: false,
              capabilities: {
                drop: ['ALL']
              }
            }
          });
        }
      }
     
      return patch;
    }
     
    app.listen(8443, () => {
      console.log('Webhook server running on port 8443');
    });

    This implementation includes error handling, proper request validation, and a clean separation of concerns. The webhook service should be deployed with appropriate resource limits and monitoring.

    Testing Webhooks Locally

    Testing webhooks can be challenging because they need to communicate with the Kubernetes API server. The kubectl apply --dry-run=client command can help you test your webhook configuration without actually creating resources.

    # Test webhook configuration
    kubectl apply --dry-run=client -f mutating-webhook.yaml
     
    # Test webhook configuration
    kubectl apply --dry-run=client -f validating-webhook.yaml

    For testing the webhook logic itself, you can use tools like curl to send AdmissionReview objects to your webhook service.

    # Send a test request to the webhook
    curl -X POST https://webhook-service.default.svc:8443/mutate \
      -H "Content-Type: application/json" \
      -d @test-request.json

    The test-request.json file contains a serialized AdmissionReview object that mimics what the API server sends to your webhook.

    Common Use Cases

    1. Security Policy Enforcement

    Webhooks are excellent for enforcing security policies across your cluster. You can validate that pods don't use privileged containers, require specific security contexts, or check for sensitive configurations.

    2. Resource Quota Enforcement

    While Kubernetes has built-in resource quotas, webhooks can provide more complex validation and automatic adjustments. For example, you could automatically scale down pods that exceed their resource limits.

    3. Configuration Management

    Webhooks can ensure consistent configuration across your cluster. You might automatically add labels to resources, set default values, or inject configuration files.

    4. Compliance and Auditing

    Webhooks can log all admission requests for auditing purposes. This helps you track who created or modified resources and when, which is important for compliance requirements.

    5. Integration with External Systems

    Webhooks can integrate with external systems like CI/CD pipelines, monitoring tools, or compliance frameworks. For example, you could validate that a pod's image comes from a trusted registry.

    Best Practices

    1. Keep Webhooks Stateless

    Webhooks should be stateless to ensure they can be scaled horizontally. Avoid storing request-specific data in memory or databases.

    2. Use Proper Error Handling

    Always handle errors gracefully and return meaningful error messages to the API server. This helps users understand why their requests were rejected.

    3. Implement Rate Limiting

    Protect your webhook service from abuse by implementing rate limiting. This prevents denial-of-service attacks and ensures reliable operation.

    4. Monitor Webhook Performance

    Monitor webhook response times and success rates. Slow webhooks can impact cluster performance, so optimize them for speed.

    5. Test Thoroughly

    Test your webhooks in various scenarios, including edge cases and error conditions. Use integration tests to verify they work correctly with the Kubernetes API server.

    Conclusion

    Kubernetes mutating and validating webhooks are powerful tools for controlling resource creation and modification in your cluster. They provide a flexible way to enforce policies, apply defaults, and integrate with external systems.

    By understanding how webhooks work and following best practices, you can build robust admission control mechanisms that improve security, consistency, and compliance across your Kubernetes deployments.

    Platforms like ServerlessBase simplify the deployment and management of webhooks by handling the infrastructure and configuration automatically, allowing you to focus on implementing the webhook logic itself.

    Next Steps

    Now that you understand webhooks, consider exploring related Kubernetes concepts:

    Leave comment