Kubernetes v1.36 shipped on April 22, 2026 with a headline feature called Declarative Validation reaching General Availability, and the name alone has already caused a small wave of "great, now I can delete my CRD validating webhook" posts. Most of them are wrong about what actually graduated. Declarative Validation (KEP-5073) is about the ~50 built-in API types — Pod, Service, Node, and friends — moving their internal, handwritten Go validation code to CEL rules declared on struct tags. It does not touch CustomResourceDefinitions at all. If you run a Cluster API fleet, the webhook you can actually delete graduated two releases earlier, under a completely different feature, and it's not the one this month's headline is about.
Here's the precise map: three separate CEL-based validation mechanisms hit GA across three different Kubernetes releases, they get conflated constantly, and each one draws its boundary in a different place. Sorting out which is which is the difference between correctly retiring a webhook deployment and shipping a policy gap because you assumed CEL covered a check it structurally can't.
Three Mechanisms, Three GA Dates, One Shared Blind Spot
| Mechanism | KEP / feature | GA version | What it validates | Cross-object / stateful checks? |
|---|---|---|---|---|
| CRD Validation Rules | KEP-2876, x-kubernetes-validations | 1.29 (Dec 2023) | A CustomResourceDefinition's own schema — fields on the object being written | No |
| ValidatingAdmissionPolicy (VAP) | KEP-3488 | 1.30 (Apr 2024) | Any resource, built-in or custom, as a cluster-wide policy | No |
| Declarative Validation | KEP-5073, validation-gen | 1.36 (Apr 2026) | Built-in native types only (Pod, Service, …) — replaces handwritten Go validation internals | No |
The row that matters for a self-hosted Cluster API fleet is the first one, not the last one. Cluster API's own CRDs — Cluster, Machine, KubeadmControlPlane, and infrastructure types like HetznerCluster — have carried x-kubernetes-validations rules in their OpenAPI schemas since around the 1.29 cycle, for exactly the invariants a webhook used to enforce: field immutability after creation, required-field combinations, enum-like constraints on string fields. That webhook was already gone, or never needed writing, well before this year's release. If you're still running a custom validating webhook whose entire job is "reject a KubeadmControlPlane with an invalid replica count," that's a schema rule you can delete a webhook deployment for today, independent of anything in 1.36.
What Kubernetes 1.36's Declarative Validation actually changes is upstream of anything you configure. It runs inside the API server's own request-handling path — both on your management cluster and every workload cluster CAPI provisions — replacing handwritten validate.go functions for built-in types with CEL expressions attached to Go struct tags via validation-gen. During the migration, the feature gate DeclarativeValidationBeta (default true in 1.36) runs both systems side by side for shadowed fields: the handwritten result is still what's authoritative, but the declarative result is compared against it, and any discrepancy increments the declarative_validation_mismatch_total metric and gets logged. That's a correctness and maintainability change for the Kubernetes project itself — fewer handwritten validation bugs, a documented path toward eventually publishing validation rules via OpenAPI for tooling like Kubebuilder to consume — not a lever an operator flips. You will not delete a webhook because of it. You'll benefit from it the same way you benefit from any apiserver bug fix: passively, and only if your fleet is running 1.36 kubelets and control planes.
The Webhook You Can Actually Delete: A Worked Example
Say your fleet runs a policy every HetznerCluster object must satisfy: its controllerImage reference must come from an approved region-mirrored registry, and it must not target a Hetzner location outside your contracted set (say, only fsn1, nbg1, hel1 are on your invoiced capacity). Before VAP existed, enforcing that meant standing up a real webhook: a Deployment running your validation server, a Service in front of it, a ValidatingWebhookConfiguration pointing at that service, and a cert-manager Certificate + Issuer pair to keep its TLS serving cert rotated — because failurePolicy: Fail on a webhook whose certificate has silently expired doesn't degrade gracefully, it freezes every create/update for the resources it matches, including the pods you'd need to fix the webhook itself.
Since ValidatingAdmissionPolicy went GA in 1.30, that whole deployment collapses into a CEL expression the kube-apiserver evaluates in-process, no network hop, no TLS cert to rotate:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: hetznercluster-approved-locations
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["infrastructure.cluster.x-k8s.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE"]
resources: ["hetznerclusters"]
validations:
- expression: >
object.spec.controllerImage.startsWith('registry.fsn1.internal/') &&
object.spec.location in ['fsn1', 'nbg1', 'hel1']
message: "HetznerCluster must use the region-mirrored registry and an approved location"apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: hetznercluster-approved-locations-binding
spec:
policyName: hetznercluster-approved-locations
validationActions: ["Deny"]Apply those two objects, delete the old webhook's Deployment, Service, ValidatingWebhookConfiguration, and Certificate — four pieces of infrastructure you were keeping alive, monitoring, and patching, replaced by two objects the kube-apiserver already knows how to serve. That's the concrete "no extra webhook sidecar" win the headline promised, and it's been available for two full releases; 1.36 didn't add it, but it's the mechanism actually doing the work when people say "Kubernetes CEL validation let us delete a webhook."
Where the Webhook Doesn't Go Away
All three mechanisms in that table share one documented limit: CEL validation is scoped to the object being admitted. The self variable in a x-kubernetes-validations rule and the object/oldObject variables in a VAP expression only ever see the fields of the resource currently being written — there is no supported way to reach out and read a different object, query an external system, or check anything that depends on cluster state beyond the request payload. Kubernetes' own CRD validation-rules documentation states this plainly: "no cross-object or stateful validation rules are supported." That's not a gap the 1.36 release closes either — Declarative Validation inherits the same constraint, because it's still CEL underneath.
A self-hosted PaaS's own tenant-facing admission surface runs headlong into that limit. Take a bex.yml-backed App custom resource: a tenant submits a manifest declaring a custom domain, a git repository URL, and a secret reference for build credentials. Three checks that manifest needs before the API server accepts it are all inherently stateful:
- Domain uniqueness — is
app.example.comalready claimed by a different tenant'sAppobject? Answering that requires listing every otherAppin the cluster (or querying an index), not just reading the object in front of you. - Referenced-secret existence — does the
Secretnamed inspec.buildCredentialsRefactually exist in that namespace, and does the calling tenant have RBAC to read it? That's a live lookup against the API server's own storage, not a schema check. - Quota against a live counter — has this org already hit its plan's app-count or build-minutes ceiling this cycle? That number lives in an external billing/usage system a CEL expression has no path to reach.
None of those can be expressed as a schema-only rule, no matter how the CEL is written, because each one depends on state the object being validated doesn't carry. It isn't that this admission check hasn't been modernized yet — the class of check it performs is structurally outside what declarative, in-process CEL validation was ever designed to answer. That webhook keeps its Deployment, its Service, and its cert-manager-issued certificate — with failurePolicy: Fail scoped narrowly to just the apps.bex.co resource group (never to core resources like Pod or Node) so a webhook outage can't freeze the whole cluster, and its own namespace excluded from any policy that would otherwise create a circular dependency on itself.
A Fast Audit for Your Own Fleet
Before adding — or keeping — a webhook, run the check against what it's actually doing:
- Is the rule visible entirely on the object being written? (a required field, an enum, an immutability constraint, a numeric range) →
x-kubernetes-validationson your CRD, GA since 1.29. - Is it a cluster-wide policy that applies across resource types or needs a
Deny/Audit/Warnaction model? (an approved-location allowlist, "no privileged containers," a required label convention) →ValidatingAdmissionPolicy, GA since 1.30. - Does answering it require reading a different object, an external system, or a live counter? (uniqueness, existence, quota, anything "as of right now, elsewhere") → you still need a real webhook, and no Kubernetes release is going to remove that requirement, because the constraint is architectural, not a maturity gap.
- Is it internal to a built-in type's own field validation? → that's Declarative Validation's territory as of 1.36, and you don't configure it either way.
Run that audit against every ValidatingWebhookConfiguration in a management cluster and most fleets find at least one that's been doing job #1 or #2 the hard way since before VAP existed. The ones doing job #3 aren't going anywhere, and pretending otherwise — deleting a webhook because "Kubernetes 1.36 handles validation now" without checking which category it falls into — is how a policy gap ships silently on the next tenant.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Cluster API–managed fleet underneath. Its own tenant-facing admission logic follows exactly this split: schema rules live in the App CRD, cross-tenant checks live in a real webhook. Star the repo on GitHub or deploy your first app today.



