Admission Control
Validating Admission Policy
In tree CEL based admission policies that need no external webhook.
ValidatingAdmissionPolicy runs policy inside the API server using CEL (Common Expression Language) expressions. There is no webhook, no extra deployment, and no network hop that can fail or add latency. It is GA since Kubernetes 1.30.
Like Gatekeeper, it splits logic from placement:
- ValidatingAdmissionPolicy: what to check, as CEL expressions over the incoming
object. - ValidatingAdmissionPolicyBinding: where to apply it and what to do on failure (
Deny,Warn, orAudit).
Write A Policy
Reject privileged containers in one CEL expression:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: disallow-privileged
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >-
object.spec.containers.all(c,
!has(c.securityContext) ||
!has(c.securityContext.privileged) ||
c.securityContext.privileged == false)
message: "Privileged containers are not allowed."
The expression must hold for the request to pass. has() guards optional fields, and all() quantifies over every container.
Bind It
Apply the policy only to namespaces labeled for it:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: disallow-privileged-binding
spec:
policyName: disallow-privileged
validationActions: ["Deny"]
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: lab
Test It
kubectl apply -f policy.yaml -f binding.yaml
kubectl run priv --image=busybox:1.36 --restart=Never -n lab \
--overrides='{"spec":{"containers":[{"name":"priv","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'
Expected output:
The pods "priv" is invalid: : ValidatingAdmissionPolicy 'disallow-privileged'
with binding 'disallow-privileged-binding' denied request:
Privileged containers are not allowed.
An unprivileged pod is admitted normally.
Warn And Audit First
validationActions supports the same staged rollout as the policy engines:
validationActions: ["Warn", "Audit"]
Warn returns a warning to the client, Audit writes an audit log annotation, and neither blocks the request. Switch to ["Deny"] once violations are gone.
Where It Fits
| Approach | Strength | Cost |
|---|---|---|
| Pod Security Admission | Zero setup, standard levels | Fixed rules only |
| ValidatingAdmissionPolicy | In-tree, fast, no add-on to operate | Validation only, CEL has expression limits |
| Kyverno / Gatekeeper | Mutation, generation, reports, big policy libraries | An extra privileged component to run and upgrade |
A reasonable progression: PSA for the pod security baseline, ValidatingAdmissionPolicy for a handful of custom guardrails, and a policy engine only once you need mutation, generation, or a large rule set with reporting.
Clean Up
kubectl delete validatingadmissionpolicybinding disallow-privileged-binding
kubectl delete validatingadmissionpolicy disallow-privileged
Practical Guidance
- Prefer this over a custom webhook for simple validations; the availability failure mode of webhooks disappears entirely.
- Keep expressions small and name each check with a clear
message; CEL one-liners get unreadable fast. - Use
namespaceSelectoron the binding so platform namespaces likekube-systemare excluded deliberately, not accidentally. - Roll out with
WarnandAuditbeforeDeny, exactly as with PSA and the policy engines. - Use
variablesin the policy spec to factor repeated sub-expressions when policies grow.