Alright, let’s get our hands dirty with Validating Admission Webhooks. This is where you graduate from just letting things happen in your cluster to actually enforcing the rules of your own little Kubernetes kingdom. Think of it as the bouncer at the club door, checking IDs and turning away anyone who doesn’t meet the dress code. The API server will send an admission request to a webhook you define, and your job is to craft a response that says either “yeah, this is cool, let it in” or “absolutely not, and here’s why.”

The power here is immense, and so is the potential for you to accidentally break your entire cluster. No pressure.

The Absolute Basics: What You’re Actually Building

At its core, a Validating Admission Webhook is just an HTTPS endpoint that accepts a AdmissionReview request and returns an AdmissionReview response. Kubernetes does all the heavy lifting of watching the API and sending you the object data; you just have to write the logic to say “yes” or “no.” You’re not modifying anything here—that’s the Mutating Webhook’s job. You’re purely the judge, not the lawyer.

Here’s the skeletal structure of what your code needs to handle. This example is in Go, but the principles are language-agnostic.

package main

import (
	"encoding/json"
	"net/http"
	"log"

	admissionv1 "k8s.io/api/admission/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func validateHandler(w http.ResponseWriter, r *http.Request) {
	// 1. Read and parse the AdmissionReview request
	var admissionReviewReq admissionv1.AdmissionReview
	if err := json.NewDecoder(r.Body).Decode(&admissionReviewReq); err != nil {
		http.Error(w, "Failed to decode request", http.StatusBadRequest)
		return
	}

	// The actual object the user is trying to create/update is buried in the request
	req := admissionReviewReq.Request
	var pod corev1.Pod
	if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
		http.Error(w, "Failed to unmarshal object", http.StatusBadRequest)
		return
	}

	// 2. Your actual validation logic goes here
	allowed := true
	message := ""
	if pod.Spec.RestartPolicy == "Always" && len(pod.Spec.Containers) > 1 {
		allowed = false
		message = "My cluster, my rules. No pods with multiple containers and an 'Always' restart policy."
	}

	// 3. Build the AdmissionReview response
	admissionReviewResponse := admissionv1.AdmissionReview{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "admission.k8s.io/v1",
			Kind:       "AdmissionReview",
		},
		Response: &admissionv1.AdmissionResponse{
			UID:     req.UID, // This MUST be echoed back, or the API server will get very confused.
			Allowed: allowed,
			Result: &metav1.Status{
				Message: message,
			},
		},
	}

	// 4. Send the response back
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(admissionReviewResponse)
}

The Devil’s in the Details: Common Pitfalls

This seems simple, right? Ha. Here’s where everyone, without exception, gets bitten.

  • Failure Modes: What happens if your webhook endpoint is down? The API server will be stuck. You must configure the failurePolicy field in your ValidatingWebhookConfiguration.Fail means the request is rejected (a “fail-closed” behavior). Ignore means the request is allowed through (a “fail-open” behavior). Choosing Fail is safer but can cause cluster-wide outages if your webhook is buggy. Choosing Ignore is riskier but more resilient. There is no good choice, only trade-offs.
  • It’s Not Just for CREATE: Remember, your webhook gets called for CREATE, UPDATE, and sometimes DELETE operations. Your validation logic must account for this. Check the req.Operation field. A rule that makes sense for a new Pod might break a crucial kubectl rollout command because it’s an UPDATE.
  • The Object Is Not What You Think: During an UPDATE request, the req.Object.Raw is the new object the user is trying to apply. The old existing object is in req.OldObject.Raw. If you need to compare the new state to the old state (e.g., “you can’t change this field”), you must unmarshal both.

The Configuration: Your ValidatingWebhookConfiguration

Writing the code is only half the battle. You have to tell Kubernetes when to call it. This is done via a ValidatingWebhookConfiguration resource, and it’s hilariously easy to misconfigure. The most important parts are the rules (which operations on which resources trigger the webhook) and the namespaceSelector (which namespaces this applies to).

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "my-custom-pod-validator.example.com"
webhooks:
- name: "my-custom-pod-validator.example.com"
  objectSelector: {} # Apply to all objects
  rules:
  - operations: ["CREATE", "UPDATE"] # Watch for these operations
    apiGroups: [""]                  # core API group
    apiVersions: ["v1"]              # v1 API version
    resources: ["pods"]              # on the 'pods' resource
  clientConfig:
    service:
      namespace: "webhooks"           # namespace of your webhook service
      name: "webhook-service"        # name of your service
      path: "/validate"              # path your endpoint listens on
      port: 443
    caBundle: <CA_BUNDLE>            # This is a whole other topic of pain.
  admissionReviewVersions: ["v1"]    # Must include "v1"
  sideEffects: None                  # This is crucial. Unless your webhook has side effects, set this to 'None'.
  failurePolicy: Fail                # The big choice: Fail or Ignore

Pro Tip: For the love of all that is holy, use a namespaceSelector to exclude the kube-system and your webhook’s own namespace initially. Otherwise, you might prevent critical system pods or your own webhook pod from starting, creating a bricked cluster that can’t self-heal. I’m not joking. This is a rite of passage.