Skip to main content
RunBook Academy

KubernetesLXI · Admission ControlAdmission control

ValidatingAdmissionPolicy — declarative policy in CEL

Advanced⏱ ~14 minkubectl

What you'll learn

  • Write a ValidatingAdmissionPolicy using CEL expressions
  • Bind the policy to resources via ValidatingAdmissionPolicyBinding
  • Configure the failurePolicy and audit annotations
  • Migrate from a webhook-based policy to a CEL-based policy

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

ValidatingAdmissionPolicy (introduced in 1.26 as alpha, 1.30 as beta) is the declarative alternative to admission webhooks. A policy is a CEL expression that the API server evaluates without calling out to a service. Policies are faster, more reliable, and easier to audit than webhooks. This lesson covers the shape of a policy, the bindings, the common patterns, and the migration from webhooks.

The shape of a policy

A ValidatingAdmissionPolicy has three fields: spec.validations (the CEL expressions), spec.rules (which resources and operations the policy applies to), and spec.failurePolicy (Fail or Ignore).

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: no-privileged
spec:
  failurePolicy: Fail
  rules:
  - operations: ["CREATE", "UPDATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  validations:
  - expression: "object.spec.containers.all(c, !has(c.securityContext.privileged) || c.securityContext.privileged == false)"
    message: "privileged containers are not allowed"

The CEL expression iterates over every container in the Pod; if any container has securityContext.privileged == true, the policy fails. The message field is returned to the user when the policy fails.

Bindings

A ValidatingAdmissionPolicyBinding scopes the policy to specific resources or namespaces:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: no-privileged-prod
spec:
  policyName: no-privileged
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        pod-security.kubernetes.io/enforce: restricted

This binding applies the no-privileged policy only to namespaces with the pod-security.kubernetes.io/enforce: restricted label. Other namespaces are unaffected.

The validationActions field is [Deny] (the default) or [Warn]. Deny rejects the request; Warn accepts the request but logs a warning. Use Warn for advisory policies; Deny for enforcement.

flowchart LR
    A[Request] --> B{Binding matches?}
    B -->|yes| C[Evaluate CEL]
    B -->|no| D[Skip]
    C -->|pass| E[Allow]
    C -->|fail| F[validationActions]
    F -->|Deny| G[Reject]
    F -->|Warn| H[Allow with warning]

Common policies

Four policies recur:

  1. No privileged containers. The expression above.
  2. Image registry whitelist. Restrict images to a specific registry:
validations:
- expression: "object.spec.containers.all(c, c.image.startsWith('registry.example.com/'))"
  message: "images must come from registry.example.com"
  1. Required labels. Enforce that every workload has an app.kubernetes.io/name:
validations:
- expression: "has(object.metadata.labels) && 'app.kubernetes.io/name' in object.metadata.labels"
  message: "the app.kubernetes.io/name label is required"
  1. Resource limits. Every container must have CPU and memory limits:
validations:
- expression: "object.spec.containers.all(c, has(c.resources.limits) && has(c.resources.limits.memory) && has(c.resources.limits.cpu))"
  message: "every container must have CPU and memory limits"

These four policies cover the most common operational requirements.

The failurePolicy and audit annotations

spec.failurePolicy controls what happens when the CEL evaluation errors (e.g., type mismatch):

spec:
  failurePolicy: Fail  # reject on error

Fail is the safe default for security policies; Ignore allows the request through if the evaluation errors. Use Ignore only for advisory policies.

The metadata.annotations can mark a policy as audit-only:

metadata:
  annotations:
    policy.admission.example.com/audit: "true"

The annotation is convention; tools can read it to classify the policy.

Migration from webhooks

Migrating a webhook policy to CEL:

  1. Identify the policy. The webhook’s handler logic.
  2. Express in CEL. Translate the handler into one or more expressions.
  3. Create the policy. Deploy the ValidatingAdmissionPolicy and Binding.
  4. Run in Warn mode. Set validationActions: [Warn] and verify the messages match the webhook’s rejections.
  5. Switch to Deny. Once verified, set validationActions: [Deny].
  6. Disable the webhook. Remove the ValidatingAdmissionWebhookConfiguration.
# 1. List existing webhooks
kubectl get validatingwebhookconfigurations

# 2. Run the new policy in Warn
kubectl apply -f no-privileged-policy.yaml

# 3. Test with a workload that should be rejected
kubectl apply -f privileged-pod.yaml
# Should be accepted (Warn) but with a warning event

# 4. Switch to Deny
kubectl edit validatingadmissionpolicybinding no-privileged-prod
# Change validationActions to [Deny]

# 5. Disable the webhook
kubectl delete validatingwebhookconfiguration no-privileged-webhook

Common failure modes

  1. CEL syntax error. The expression is invalid; the policy fails to register. The fix is to test the expression in kubectl ce-l (the kubectl CEL evaluator).
  2. Type mismatch. The expression accesses a field that does not exist; the evaluation errors. The fix is to use has() to check before accessing.
  3. Policy too broad. The expression applies to every Pod, including system Pods. The fix is to scope via the binding’s matchResources.
  4. Policy not enforced. The binding is missing; the policy exists but is not applied. The fix is to create the binding.

Production failure modes

  1. Policy applies cluster-wide unintentionally. The binding’s matchResources is empty; the policy applies to every Pod, including kube-system. The fix is to scope via namespaceSelector or objectSelector.
  2. Warn mode for too long. A policy runs in Warn mode indefinitely; rejections never happen. The fix is a deadline for the switch to Deny.
  3. No audit of policy hits. The policy’s rejections are not logged. The fix is to enable audit logging at RequestResponse level.

Cross-course references

  • The Observability course covers the audit log entries for policy rejections.
  • The Linux course covers the file permissions for the policy’s metadata.annotations.

Quiz

Knowledge check · 4 questions

  1. Q1. Where is the CEL expression in a `ValidatingAdmissionPolicy` evaluated?

  2. Q2. `validationActions: [Warn]` accepts the request but logs a warning; `validationActions: [Deny]` rejects the request.

  3. Q3. Your cluster has a ValidatingAdmissionWebhook that rejects any Pod with a container image not from `registry.example.com`. The webhook service has had two outages this month, each blocking all Pod creations. The team wants to migrate to a CEL-based policy. Walk the migration.

    The webhook has `failurePolicy: Fail`, `operations: CREATE,UPDATE`, `resources: pods`. The handler logic is: reject if `image` does not start with `registry.example.com/`. The team wants a CEL equivalent that does not depend on a webhook service.

  4. Q4. Name three common ValidatingAdmissionPolicy patterns and the CEL expression for each.

Passing score: 75%. Answers are checked in this browser.

Production discipline

ValidatingAdmissionPolicy is the right primitive for declarative policy. A defensible admission programme expresses the security policies in CEL, binds them to the appropriate namespaces, runs in Warn mode during rollout, and switches to Deny once verified. Webhooks remain for the policies that CEL cannot express (cross-object validation, external lookups). A cluster whose policies are CEL-based is auditable; a cluster whose policies are webhooks-only is at the risk of webhook outages. The discipline is to prefer CEL where possible, webhooks where necessary, and to audit every policy’s bindings.