Skip to content
SRE & DevOpsHow-To Published Updated 5 min readViews unavailable

Kubernetes ValidatingAdmissionPolicy: CEL Policy Without a Webhook

How to design, bind, stage, observe, and safely enforce in-process Kubernetes admission rules with ValidatingAdmissionPolicy and CEL.

Kubernetes ValidatingAdmissionPolicy runs declarative Common Expression Language checks inside the API server. It can reject or audit matching create and update requests without operating a separate admission webhook service. The feature is stable from Kubernetes 1.30.

Removing a webhook hop eliminates certificates, network availability, service deployment, and callback latency for suitable rules. It does not make policy changes harmless. A bad in-process expression can still block every matching write, so scope and staged enforcement are part of the design.

Separate logic from activation

A useful policy normally has two Kubernetes objects. ValidatingAdmissionPolicy defines match constraints, variables, validations, failure behavior, and optional parameter type. ValidatingAdmissionPolicyBinding selects where that logic applies and which validation actions occur.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: deployment-replica-limit.example.com
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: "object.spec.replicas <= 20"
      message: "Deployments may request at most 20 replicas"
      reason: Invalid

Without a binding, this policy has no effect. That is useful for installing logic before activation. The binding can restrict namespaces and objects further and choose Audit, Warn, or Deny actions.

Keep broad resource matching in the policy only when every binding should share it. Put rollout-specific namespace selection in bindings so the same reviewed logic can move from a test namespace to wider enforcement without copying expressions.

Write CEL against real API shapes

CEL evaluates the admitted object, the old object on updates, request metadata, parameters, namespace information where available, and authorizer helpers. Optional fields and union types need explicit handling. An expression that assumes a missing field exists can produce an evaluation error, whose result then depends on failurePolicy.

Use has() or presence-safe expressions, and test create, update, status subresource, deletion, and server-side apply. A rule intended for Pods may also see controller-created Pods; decide whether the workload controller, the resulting Pod, or both are the enforcement point.

Variables can name repeated expressions and make the validation readable. Match conditions can exclude system identities or special operations, but every exception becomes part of the security policy. Prefer a narrow resource and namespace selector over a long expression that begins by exempting most requests.

Parameterize organization-specific values

paramKind lets a policy reference a native object or CRD containing values such as allowed registries, replica ceilings, or label domains. The binding selects a parameter object by name or selector.

Define what happens when parameters are absent. parameterNotFoundAction can deny or allow, and the correct choice depends on whether the parameter is mandatory security policy or optional local customization. Validate the parameter CRD itself so a malformed list cannot quietly broaden admission.

Parameterization avoids duplicating logic, but it creates a second authorization surface. Restrict who can change parameter objects and audit those writes as policy changes.

Roll out audit before denial

Begin with a binding whose validation actions include Audit, optionally Warn, but not Deny. Exercise normal deployment tools, controllers, backup restores, operators, and emergency workflows. Inspect API audit annotations and client warnings to identify legitimate requests that the rule would reject.

Then add Deny to a canary namespace. Keep a pretested rollback that deletes or narrows the binding, not the policy logic. Because bindings activate policy, reverting one object is faster and less error-prone than editing a complex CEL expression during an outage.

Choose failurePolicy: Fail for a security invariant only after expressions and parameter availability are proven. Ignore improves write availability when policy evaluation fails, but it permits requests without the intended validation. Document that tradeoff rather than inheriting it accidentally.

Know when a webhook is still required

ValidatingAdmissionPolicy does not mutate objects and cannot perform arbitrary network lookups. A rule that needs external vulnerability intelligence, an asynchronous approval system, or custom code may still require a webhook or a controller that reconciles state after admission.

Prefer CEL for deterministic validation over the request and cluster-resident parameters. It keeps policy evaluation close to the API schema and removes an entire networked service from the critical write path. The best migration target is a stable webhook rule whose inputs can be expressed completely in CEL.

Treat type checking and observability as release gates

Creating a policy proves that its CEL syntax parses, but it does not prove that every field reference is valid. Kubernetes reports schema-based warnings under status.typeChecking.expressionWarnings. Check that status explicitly after every policy change. Type checking currently skips wildcard matches, checks only a bounded number of matched types, and does not cover CRD schemas, so an empty warning list is useful evidence rather than a complete test suite.

kubectl get validatingadmissionpolicy \
  deployment-replica-limit.example.com \
  -o jsonpath='{.status.typeChecking.expressionWarnings}'

Runtime evidence should identify both the failed rule and the selected action. Use messageExpression for a single-line explanation containing safe request values, and keep a static message as a fallback if the expression itself fails. Add auditAnnotations when operators need structured context in API audit events. Those annotations are namespaced with the policy name, which prevents an unqualified key from becoming the incident interface.

Remember that Deny and Warn cannot be combined in one binding. A practical progression is [Warn, Audit] during observation, followed by [Deny, Audit] for enforcement. Inspect warnings, audit events, and status.typeChecking in CI before promoting the binding. That makes the policy object, its runtime behavior, and its operational evidence one reviewed release instead of three unrelated assumptions.

Success means more than seeing the object in kubectl get. Submit known-good and known-bad requests as each relevant identity, confirm audit and warning behavior, verify the denial reason, simulate missing parameters, and prove the rollback while the rule is active.

Related:

Sources:

Comments