PostgreSQLInstance Claim asking for storageGB: 99999, the Kubernetes API will accept it (because it is a valid integer). Crossplane will then happily attempt to provision a 100TB database in AWS, bankrupting your company. We must build a mathematical firewall to stop this before it reaches the Control Plane. We need a Validating Webhook.1. How Kubernetes Webhooks Work#
A Kubernetes Validating Webhook is a standard HTTP server that sits in front of etcd.
When you run kubectl apply -f claim.yaml:
- The Kubernetes API server parses the YAML.
- It sends an HTTP
POSTrequest (containing the YAML data) to your Webhook server. - Your server runs arbitrary Go logic (e.g.,
if storageGB > 1000 { return false }). - If your server returns HTTP 200 (Allow), the API server saves the Claim to the database, and Crossplane begins provisioning.
- If your server returns HTTP 403 (Deny), the API server instantly drops the request, and the developer sees an error in their terminal.
This guarantees that Crossplane never sees invalid data.
2. Writing the Webhook in Go#
Because a Webhook is just an HTTP server that accepts a specific JSON schema (an AdmissionReview object), we can write it using the standard Go net/http library.
Create a file named webhook.go:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Define the structure of the Developer's Claim
type PostgreSQLClaim struct {
Spec struct {
Parameters struct {
StorageGB int `json:"storageGB"`
Env string `json:"environment"`
} `json:"parameters"`
} `json:"spec"`
}
func handleValidate(w http.ResponseWriter, r *http.Request) {
// 1. Read the HTTP Request Body from the Kubernetes API Server
body, _ := ioutil.ReadAll(r.Body)
// 2. Unmarshal the AdmissionReview object
var review admissionv1.AdmissionReview
json.Unmarshal(body, &review)
// 3. Extract the raw Crossplane Claim from the request
var claim PostgreSQLClaim
json.Unmarshal(review.Request.Object.Raw, &claim)
// 4. THE FIREWALL LOGIC
allowed := true
message := "Claim is valid."
// Rule A: Max Storage Size
if claim.Spec.Parameters.StorageGB > 500 {
allowed = false
message = "Security Violation: Storage cannot exceed 500GB."
}
// Rule B: Enforce increments of 10
if claim.Spec.Parameters.StorageGB%10 != 0 {
allowed = false
message = "Compliance Violation: Storage must be an increment of 10."
}
// 5. Build the Response
responseReview := admissionv1.AdmissionReview{
TypeMeta: review.TypeMeta,
Response: &admissionv1.AdmissionResponse{
UID: review.Request.UID,
Allowed: allowed,
Result: &metav1.Status{
Message: message,
},
},
}
// 6. Send the HTTP Response back to the API Server
respBytes, _ := json.Marshal(responseReview)
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
}
func main() {
http.HandleFunc("/validate", handleValidate)
// Webhooks MUST run over HTTPS! You must mount TLS certificates.
fmt.Println("Starting Webhook Server on :8443")
http.ListenAndServeTLS(":8443", "/tls/tls.crt", "/tls/tls.key", nil)
}3. Deploying the Webhook#
Step 1: Containerize and Run#
You must compile this Go code, build a Docker image, and deploy it as a standard Kubernetes Deployment and Service inside your cluster. (Ensure the Service is named validation-webhook on port 443).
Step 2: Configure the API Server#
Once your Go HTTP server is running, you must instruct the Kubernetes API Server to forward all Crossplane Claims to it.
You do this using a ValidatingWebhookConfiguration.
Create webhook-config.yaml:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: crossplane-claim-validator
webhooks:
- name: validate.acmecorp.com
rules:
- apiGroups: ["database.acmecorp.com"]
apiVersions: ["v1alpha1"]
# Intercept operations on our custom Crossplane Claim!
operations: ["CREATE", "UPDATE"]
resources: ["postgresqlinstances"]
clientConfig:
service:
name: validation-webhook
namespace: platform-system
path: "/validate"
# You MUST provide the CA Bundle that signed the TLS cert of your Go server!
caBundle: "LS0tLS1CRUdJTiBDRV..."
admissionReviewVersions: ["v1"]
sideEffects: NoneApply this to the cluster:
kubectl apply -f webhook-config.yaml4. The Developer Experience#
Now, let’s pretend to be the Application Developer. We submit a bad Claim:
# bad-claim.yaml
apiVersion: database.acmecorp.com/v1alpha1
kind: PostgreSQLInstance
metadata:
name: rogue-db
spec:
parameters:
storageGB: 5000
environment: prodWhen they run kubectl apply:
kubectl apply -f bad-claim.yamlExpected Terminal Output:
Error from server: error when creating "bad-claim.yaml": admission webhook "validate.acmecorp.com" denied the request: Security Violation: Storage cannot exceed 500GB.The YAML is instantly rejected. etcd never stores the object. Crossplane never sees the object. AWS is perfectly safe.
You have successfully built an impenetrable governance layer.
Troubleshooting & Common Errors#
Internal error occurred: failed calling webhook: Post "https://...": x509: certificate signed by unknown authority- Root Cause: Kubernetes requires Webhooks to use strict TLS encryption. The API server rejected your Go server’s SSL certificate because it doesn’t trust the CA.
- Solution: Ensure the
caBundlein yourValidatingWebhookConfigurationexactly matches the CA that generated thetls.crtmounted to your Go server. (Many teams usecert-managerto automate this).
The Webhook ignores the Claim entirely
- Root Cause: The
apiGroupsorresourcesarrays in the webhook config do not match the XRD definitions. - Solution: Double check your plurals. If your XRD is
postgresqlinstances, ensure the webhook config sayspostgresqlinstances.
- Root Cause: The
Conclusion & Next Steps#
Webhooks are the ultimate expression of Platform Engineering security. By combining the strict OpenAPI schemas of Crossplane XRDs with the dynamic, programmable validation of Go Webhooks, you can safely hand infrastructure provisioning keys to every developer in the company.
However, writing YAML files—even simple Claims—is still not the ideal developer experience. Developers want a beautiful Web UI with dropdown menus and buttons.
In the Final Episode (Ep 15): Building an IDP with Backstage, we will complete the Platform Engineering journey. We will integrate Crossplane with Spotify’s Backstage, allowing developers to click a button in their browser to instantly provision AWS infrastructure.

