Kubernetes ships a built-in ValidatingAdmissionPolicy resource whose rules are written in CEL (Common Expression Language) and evaluated inside the API server. How does it work, and when would you pick it over an external validating admission webhook?
answer
- policy = logic, binding = scope + action
- CEL in-process, no webhook pod, no certs
- object / oldObject / request / params / authorizer
- validationActions: Deny, Warn, Audit
- can't mutate, can't call out
basics
~20 sValidatingAdmissionPolicy holds CEL expressions the API server evaluates in-process on incoming objects. A ValidatingAdmissionPolicyBinding scopes it to namespaces or resources and picks the action: Deny, Warn, or Audit. No external service means no serving certificates, no network hop, no availability risk. But CEL cannot mutate or call out.
solid answer
~50 sA **ValidatingAdmissionPolicy** declares `matchConstraints` (which resources and verbs it applies to) and `validations` — CEL expressions over `object`, `oldObject`, `request`, `params` and `authorizer`. A separate **ValidatingAdmissionPolicyBinding** attaches it to a scope (namespace selector, optional `paramRef` to a parameter object) and sets `validationActions`: `Deny`, `Warn`, `Audit`, or a combination. Because CEL runs inside kube-apiserver there is no webhook Deployment to keep healthy, no TLS certificate to rotate, and no `failurePolicy` availability trade-off — the check costs a bounded in-process expression instead of a TLS round trip on every write. The cost is expressiveness. CEL is deliberately non-Turing-complete, sandboxed and budget-limited: it cannot query a registry, verify an image signature, read arbitrary cluster state (only the `params` object it is bound to), or mutate the request. So: field-level invariants (required labels, banned fields, resource limits present, immutable spec fields) belong in ValidatingAdmissionPolicy; anything needing external data, signature verification, or generated side-effect resources stays in Kyverno, Gatekeeper, or a hand-written webhook. Most clusters run both.
code
yaml · 29 linesapiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-cpu-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: >-
object.spec.template.spec.containers.all(c,
has(c.resources) && has(c.resources.limits) && has(c.resources.limits.cpu))
message: "every container must set resources.limits.cpu"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-cpu-limits-prod
spec:
policyName: require-cpu-limits
validationActions: ["Deny"]
matchResources:
namespaceSelector:
matchLabels:
env: prodgo deeper
Know that Kubernetes can enforce custom rules on objects at creation time, that ValidatingAdmissionPolicy is the built-in way to do it, and that it only rejects — it never edits.
Explain the policy/binding split, the CEL variables available, and the Deny/Warn/Audit actions, and give one concrete rule you'd express this way.
Lead with the operational argument: no webhook pod, no certificate rotation, no failurePolicy dilemma, lower write latency — and be precise about the class of rules that still require a webhook.
Frame it as a policy-architecture split: cheap invariants in-tree and centrally versioned, external engines only where external data or mutation is unavoidable, with a stated migration path and version floor across the fleet.
## The problem it solves Before ValidatingAdmissionPolicy, every custom cluster rule — "pods must set resource limits", "Ingress hosts must end in our domain" — required a **validating admission webhook**: a pod you deploy, expose as a Service, serve TLS from, register with a `ValidatingWebhookConfiguration`, and keep online. Every matching API write then makes a synchronous HTTPS call to it. That is a lot of operational surface for what is often a one-line field check, and it puts your own workload on the critical path of the control plane. ## What the API does Two objects, deliberately split: **ValidatingAdmissionPolicy** — the *logic*, written once and reusable. It contains: - `matchConstraints`: resource rules (apiGroups, apiVersions, resources, operations) selecting which requests it sees. - `validations`: a list of CEL `expression`s that must evaluate to `true`, each with a `message` or `messageExpression` returned on failure. - optional `variables` (named sub-expressions), `auditAnnotations`, and `matchConditions` (CEL predicates that cheaply skip the policy). - optional `paramKind`: the type of a parameter object the expressions can read as `params`. **ValidatingAdmissionPolicyBinding** — the *scoping and enforcement decision*: which policy, which namespaces (`matchResources.namespaceSelector`), which parameter instance (`paramRef`), and `validationActions`. That split is the interesting design point: one policy, many bindings. The same "must have resource limits" policy can Deny in production namespaces and only Audit in sandbox namespaces, or read different threshold values per team through `paramRef`. ## The CEL environment Inside an expression you get: - `object` — the incoming object (null on DELETE), - `oldObject` — the existing object (null on CREATE), which makes update-only rules and immutability checks possible, - `request` — the AdmissionRequest metadata (operation, userInfo, namespace, dryRun), - `params` — the bound parameter object, - `authorizer` — lets a policy ask "is this user allowed to do X?", which is how you write break-glass exemptions without hardcoding usernames. CEL has no loops that can run unbounded, no I/O, and every expression is cost-estimated at write time and runtime-budgeted, so a bad policy cannot hang the API server. Compilation errors are reported on the policy object's status, not discovered at request time. ## Actions `validationActions` accepts any combination of: - **Deny** — request is rejected with a 4xx and the message. - **Warn** — the message is returned as a `Warning:` header, so `kubectl` prints it but the write succeeds. - **Audit** — the failure is recorded as an annotation in the API audit log only. That trio is what makes safe rollout possible: ship with `[Warn, Audit]`, watch the audit stream to find who would break, then flip to `[Deny]`. ## Choosing it versus a webhook Pick ValidatingAdmissionPolicy when the rule is a **pure function of the object being submitted** (plus a params object). You get: no extra deployment, no cert rotation, lower latency, no chance that your policy component being down either blocks all writes or silently disables enforcement. Pick a webhook (or a policy engine that uses one) when you need to: mutate the object; verify an image signature or query a registry; consult other cluster resources for referential checks ("no two Ingresses may claim the same host"); generate companion resources; or reuse an existing Rego/Kyverno policy library. Note also that the built-in policies still run *inside* the same admission phase and cannot see the results of webhooks that run after them, so ordering-sensitive rules still need care. ## Version reality ValidatingAdmissionPolicy is beta from Kubernetes 1.28 and GA in 1.30; on older clusters it may be absent or behind a feature gate. A CEL-based mutating counterpart (MutatingAdmissionPolicy) arrived later and is much newer, so for mutation most clusters still rely on webhooks. Check the cluster version before promising a design that depends on it.
- How would you let a specific break-glass identity bypass a ValidatingAdmissionPolicy without hardcoding a username?Use the `authorizer` variable in a matchCondition or validation: ask whether the requesting user is authorized on a sentinel resource, e.g. `authorizer.group('policy.example.com').resource('exemptions').check('use').allowed()`. Then grant that permission through RBAC to whoever needs the exemption. The policy stays static while the exemption is managed as an auditable RBAC binding rather than a policy edit.
- What stops someone writing a CEL expression that hangs the API server?CEL in Kubernetes is non-Turing-complete — no unbounded loops, no I/O — and every expression is statically cost-estimated when the policy is created plus runtime-budgeted per request. A policy whose estimated cost is too high is rejected outright, and an expression exceeding its runtime budget fails rather than spinning. Compile errors surface on the policy's status, not at admission time.
A webhook is phoning an outside inspector on every delivery; ValidatingAdmissionPolicy is a checklist taped to the receiving desk — faster, always available, but it can only check what's in front of it.
saying these in an interview costs you the question
- Claiming ValidatingAdmissionPolicy can mutate or default fields — it validates only.
- Thinking the CEL can read other objects in the cluster; it sees only the request plus a bound params object.
- Forgetting the binding: a policy with no ValidatingAdmissionPolicyBinding enforces nothing.
- Saying it replaces Kyverno/Gatekeeper entirely, ignoring mutation, generation and image verification.
- Assuming it is available on any cluster version without checking (pre-1.28 clusters lack it).