KubernetesLXI · Admission ControlAdmission control
ValidatingAdmissionPolicy — declarative policy in CEL
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
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:
- No privileged containers. The expression above.
- 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"
- 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"
- 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:
- Identify the policy. The webhook’s handler logic.
- Express in CEL. Translate the handler into one or more expressions.
- Create the policy. Deploy the
ValidatingAdmissionPolicyandBinding. - Run in
Warnmode. SetvalidationActions: [Warn]and verify the messages match the webhook’s rejections. - Switch to
Deny. Once verified, setvalidationActions: [Deny]. - 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
- 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). - Type mismatch. The expression accesses a field
that does not exist; the evaluation errors. The
fix is to use
has()to check before accessing. - Policy too broad. The expression applies to
every Pod, including system Pods. The fix is to
scope via the binding’s
matchResources. - Policy not enforced. The binding is missing; the policy exists but is not applied. The fix is to create the binding.
Production failure modes
- Policy applies cluster-wide unintentionally.
The binding’s
matchResourcesis empty; the policy applies to every Pod, including kube-system. The fix is to scope vianamespaceSelectororobjectSelector. - Warn mode for too long. A policy runs in
Warnmode indefinitely; rejections never happen. The fix is a deadline for the switch toDeny. - No audit of policy hits. The policy’s
rejections are not logged. The fix is to enable
audit logging at
RequestResponselevel.
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
Q1. Where is the CEL expression in a `ValidatingAdmissionPolicy` evaluated?
Q2. `validationActions: [Warn]` accepts the request but logs a warning; `validationActions: [Deny]` rejects the request.
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.
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.