ValidatingAdmissionPolicy

A ValidatingAdmissionPolicy is an in-process, declarative validating admission mechanism: instead of calling out to an external webhook, the API server evaluates a Common Expression Language (CEL) predicate against the object directly inside kube-apiserver (Kubernetes — Validating Admission Policy). It exists to solve the operational fragility of validating webhooks — no network hop, no external Deployment to keep alive, no TLS certificate to rotate, and crucially it cannot wedge the cluster the way a failurePolicy: Fail webhook can. A policy is split across three resources: a ValidatingAdmissionPolicy (the CEL logic), a ValidatingAdmissionPolicyBinding (which resources it applies to and what to do on failure), and an optional parameter resource. ValidatingAdmissionPolicy reached GA (stable) in Kubernetes 1.30.

Mental Model

The defining trade of ValidatingAdmissionPolicy is giving up arbitrary logic to gain operational safety. A validating webhook can run any code — but it is an external service in the critical path of every write, and an unreachable one with failurePolicy: Fail can deadlock the cluster (see ValidatingAdmissionWebhook). ValidatingAdmissionPolicy moves the policy inside the apiserver as a CEL expression. CEL is a deliberately non-Turing-complete, side-effect-free, bounded-cost expression language — it always terminates, cannot loop unboundedly, and cannot make network calls. That restriction is the feature: a CEL policy cannot hang, cannot be unreachable, and adds microseconds rather than a round-trip.

The three-resource split is a deliberate separation of concerns: the policy is the reusable logic (written once, by a platform team), the binding is the scoping-and-enforcement decision (written per environment — Warn in staging, Deny in production), and the parameter resource is the tunable data (the actual numeric limit, supplied without rewriting the CEL).

flowchart TD
    P[ValidatingAdmissionPolicy<br/>spec.matchConstraints<br/>spec.validations CEL<br/>spec.paramKind] -->|policyName| B[ValidatingAdmissionPolicyBinding<br/>spec.matchResources<br/>spec.validationActions<br/>spec.paramRef]
    PR[Parameter resource<br/>ConfigMap or CRD<br/>e.g. maxReplicas: 5] -->|paramRef| B
    B --> E{Request matches?<br/>matchConstraints AND<br/>matchResources}
    E -->|yes| C[Evaluate CEL validations<br/>in-process, no network]
    E -->|no| S[Skip]
    C -->|expression false| A[validationActions:<br/>Deny / Warn / Audit]
    C -->|expression true| OK[Admit]
    style C fill:#e8f0ff

The three-resource model. The insight: the policy is reusable logic, the binding turns it on for a scope and picks the enforcement action, and the parameter resource supplies the data — so one CEL policy can be enforced strictly in prod and as a warning in staging by shipping two bindings, no policy edit needed.

Mechanical Walk-through

  1. A platform team writes a ValidatingAdmissionPolicy. Its spec.matchConstraints declares which API operations/resources the policy can apply to (a coarse filter). Its spec.validations is a list of CEL expressions; each must evaluate to a boolean — true means the object passes, false means it fails. Optionally spec.paramKind declares the GVK of a parameter resource the CEL can reference as params.
  2. The policy on its own is inert. It does nothing until a ValidatingAdmissionPolicyBinding references it by policyName.
  3. The binding’s spec.matchResources narrows scope further (namespace/object label selectors, matchConditions). Its spec.validationActions lists what happens when a validation fails. Its optional spec.paramRef points at the concrete parameter object.
  4. On a matching create/update/delete, the compiled-in ValidatingAdmissionPolicy admission plugin (see Admission Controllers) evaluates every validations expression in-process. CEL expressions see object (the incoming object), oldObject (for updates), request (the admission attributes), params (the bound parameter resource), namespaceObject, and authorizer (for authorization sub-checks).
  5. If any expression is false, the binding’s validationActions decide the outcome:
    • Deny — reject the request with a 403.
    • Warn — admit, but return a Warning: header that kubectl prints.
    • Audit — admit, but record the failure as an annotation in the audit log. Audit and Warn may be combined; Deny may be combined with Audit; Deny + Warn together is rejected as redundant.
  6. The policy’s spec.failurePolicy (Fail / Ignore) governs what happens if the expression itself errors (e.g. a CEL type error or a missing field). Note this is “policy evaluation failed,” not “webhook unreachable” — there is no network, so the catastrophic webhook-down case simply does not exist.

Configuration / API Surface

# 1. The policy: reusable CEL logic
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: replica-limit.example.com
spec:
  failurePolicy: Fail                       # behaviour if the CEL expression errors
  paramKind:                                # optional: bind a parameter resource
    apiVersion: rules.example.com/v1
    kind: ReplicaLimit
  matchConstraints:                         # coarse filter: which requests this policy can see
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
  - expression: "object.spec.replicas <= params.maxReplicas"
    message: "Deployment replica count exceeds the configured maximum."
    reason: Invalid
---
# 2. The binding: scope + enforcement action + parameter wiring
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: replica-limit-prod.example.com
spec:
  policyName: replica-limit.example.com
  validationActions: [Deny]                 # Deny | Warn | Audit (Warn+Audit combinable)
  paramRef:
    name: prod-replica-limit                # the ReplicaLimit object holding maxReplicas
    parameterNotFoundAction: Deny
  matchResources:                           # fine filter: only production namespaces
    namespaceSelector:
      matchLabels:
        environment: production
---
# 3. The parameter resource: the tunable data
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
  name: prod-replica-limit
maxReplicas: 5

Field-by-field:

  • matchConstraints vs matchResources — the policy’s matchConstraints is the upper bound of what it can match; the binding’s matchResources narrows within that. A request must satisfy both.
  • validations[].expression — a CEL boolean. object, oldObject, params, request, namespaceObject, authorizer are the available variables. validations[].messageExpression (a CEL string) can build a dynamic failure message.
  • paramKind / paramRefparamKind declares the type in the policy; paramRef binds a concrete instance in the binding. The same policy enforced against different limits in different environments needs only different parameter objects.
  • validationActions — the enforcement decision lives in the binding, not the policy. This is what lets a single policy be a non-blocking Warn while a team adopts it, then flip to Deny.
  • failurePolicy — applies when the CEL expression cannot be evaluated (type error, missing field). Distinct from a webhook’s failurePolicy, which covers an unreachable service.

Failure Modes

  • CEL evaluation error under failurePolicy: Fail. If an expression references a field that may be absent (object.spec.foo) and the field is missing, evaluation errors and — with failurePolicy: Fail — the request is rejected. The fix is defensive CEL: has(object.spec.foo) && object.spec.foo == ....
  • Cannot read external data. CEL has no network access. A policy that needs “is this image signature valid against our Rekor log?” or “is this team within its cloud-cost budget?” cannot be expressed as a ValidatingAdmissionPolicy — that still requires a webhook.
  • Expressiveness ceiling. CEL is not a general programming language. Complex multi-object reasoning, stateful checks, or anything requiring iteration beyond CEL’s bounded comprehensions hits a wall. The symptom is a policy that becomes an unreadable one-line CEL expression — at that point a webhook (or splitting the policy) is the honest answer.
  • matchConditions cost. Excessively complex matchConditions CEL is evaluated on every candidate request; an expensive match expression taxes the apiserver even for requests the policy ultimately skips.
  • Silent no-op. A policy with no binding does nothing. A common confusion: applying the ValidatingAdmissionPolicy and expecting enforcement — the ValidatingAdmissionPolicyBinding is mandatory.

Alternatives and When to Choose Them

  • vs ValidatingAdmissionWebhook. ValidatingAdmissionPolicy wins on operations: in-process, no certs, no extra Deployment, cannot wedge the cluster, lower latency. The webhook wins on capability: arbitrary code, external data, stateful checks. The current Kubernetes direction is CEL-based policies absorbing the simple webhook cases while webhooks retain the genuinely complex ones.
  • vs built-in plugins. PodSecurity, ResourceQuota, LimitRanger are still the right tool for the specific things they do. ValidatingAdmissionPolicy is for custom invariants those plugins do not cover.
  • vs OPA Gatekeeper / Kyverno. Both policy engines are webhook-based; both are now adding CEL or generating ValidatingAdmissionPolicy objects under the hood. For a brand-new cluster with modest policy needs, native ValidatingAdmissionPolicy avoids running a policy-engine Deployment at all.
  • vs MutatingAdmissionPolicy. ValidatingAdmissionPolicy can only reject; its sibling MutatingAdmissionPolicy can modify. If the goal is “default a missing field” rather than “reject a bad value,” the mutating policy is correct.

Production Notes

  • ValidatingAdmissionPolicy reached GA in Kubernetes 1.30 (Kubernetes 1.30 release); it had been alpha in 1.26 and beta in 1.28. The compiled-in ValidatingAdmissionPolicy admission plugin is default-on in current Kubernetes.
  • Adoption pattern: ship the binding with validationActions: [Audit, Warn] first, watch the audit log and kubectl warnings for how many existing workloads would violate the policy, fix or exempt them, then flip the binding to [Deny]. The binding-level action makes this a one-line change with no policy edit.
  • Type-checking: the apiserver type-checks CEL expressions against the resource schema when the policy is created, surfacing many mistakes (typos in field names, type mismatches) at apply time rather than at admission time — a real advantage over webhooks, where such bugs only show up under live traffic.

See Also