22.4 Validating Admission Webhooks: Rejecting Invalid Requests
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
failurePolicyfield in yourValidatingWebhookConfiguration.Failmeans the request is rejected (a “fail-closed” behavior).Ignoremeans the request is allowed through (a “fail-open” behavior). ChoosingFailis safer but can cause cluster-wide outages if your webhook is buggy. ChoosingIgnoreis 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 sometimesDELETEoperations. Your validation logic must account for this. Check thereq.Operationfield. A rule that makes sense for a new Pod might break a crucialkubectl rolloutcommand because it’s an UPDATE. - The Object Is Not What You Think: During an UPDATE request, the
req.Object.Rawis the new object the user is trying to apply. The old existing object is inreq.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.