What is Validating Admission Policy in Kubernetes? (CEL-Based Policy Without OPA)
Kubernetes 1.30 made Validating Admission Policy GA. It lets you enforce cluster policies using CEL expressions — no OPA, no Gatekeeper, no webhook needed. Here's how it works and when to use it.
For years, if you wanted to enforce policies in a Kubernetes cluster — "all pods must have resource limits," "no containers should run as root," "all images must come from our private registry" — you had two options:
- OPA/Gatekeeper — powerful, flexible, but requires deploying and maintaining a separate control plane component
- Kyverno — Kubernetes-native, easier than OPA, but still an external tool to install and manage
Kubernetes 1.30 (released April 2024, now stable) added a third option built directly into Kubernetes: Validating Admission Policy using CEL (Common Expression Language).
No external tools. No webhooks. No Rego. Just a CRD and an expression.
How Kubernetes Admission Actually Works
Before understanding Validating Admission Policy, understand the flow:
kubectl apply -f pod.yaml
│
▼
Kubernetes API Server
│
├── Authentication & Authorization (RBAC)
│
├── Admission Controllers
│ ├── Built-in controllers (NamespaceLifecycle, ResourceQuota, etc.)
│ ├── Mutating Webhooks (modify the resource before it's saved)
│ └── Validating Webhooks (accept or reject the resource)
│
└── etcd (resource is persisted)
Every resource you create goes through admission controllers. Validating Admission Policy plugs into the validating stage — it evaluates expressions against the incoming resource and decides: accept it or reject it.
The difference from webhooks: instead of calling an external HTTP server to evaluate the policy, Kubernetes evaluates a CEL expression directly inside the API server. Faster, simpler, no external dependencies.
Your First Validating Admission Policy
Let's enforce a real policy: "All pods must have CPU and memory limits set."
This is a common requirement — without limits, a single misbehaving pod can consume all node resources.
Step 1: Create the ValidatingAdmissionPolicy
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-resource-limits
spec:
# What this policy does when violated
failurePolicy: Fail # Reject the resource
# What resources this policy applies to
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
# The actual validation logic — written in CEL
validations:
- expression: >
object.spec.containers.all(c,
has(c.resources) &&
has(c.resources.limits) &&
has(c.resources.limits.cpu) &&
has(c.resources.limits.memory)
)
message: "All containers must have CPU and memory limits set"
reason: Invalid
# Also check init containers
- expression: >
!has(object.spec.initContainers) ||
object.spec.initContainers.all(c,
has(c.resources) &&
has(c.resources.limits) &&
has(c.resources.limits.cpu) &&
has(c.resources.limits.memory)
)
message: "All init containers must have CPU and memory limits set"Step 2: Bind the Policy to a Namespace or Cluster
A ValidatingAdmissionPolicy alone does nothing — it's a definition. You bind it using a ValidatingAdmissionPolicyBinding:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-resource-limits-binding
spec:
policyName: require-resource-limits # References the policy above
# Where to enforce it
matchResources:
namespaceSelector:
matchLabels:
enforce-policies: "true" # Only enforce in labeled namespaces
# What to do when the policy is violated
validationActions:
- Deny # Reject the resource
# Also available: Warn (allow but warn), Audit (log violation but allow)Label your namespace:
kubectl label namespace production enforce-policies=trueNow any pod created in the production namespace without resource limits will be rejected.
Understanding CEL Expressions
CEL (Common Expression Language) was originally designed at Google for policy evaluation. It's used in Firebase Security Rules, Kubernetes, and increasingly in other tools.
The basics:
# Access a field
object.spec.replicas
# Check if a field exists (ALWAYS use has() before accessing optional fields)
has(object.spec.containers)
# Check a condition on all items in a list
object.spec.containers.all(c, has(c.resources))
# Check if any item meets a condition
object.spec.containers.exists(c, c.name == "sidecar")
# Count items in a list
object.spec.containers.size() > 0
# String operations
object.metadata.name.startsWith("prod-")
object.metadata.name.matches("^[a-z][a-z0-9-]*$")
# Math
object.spec.replicas <= 10
# Null safety
object.spec.?securityContext.?runAsNonRoot.orValue(false) == true
Real Policy Examples
Policy: No Root Containers
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: no-root-containers
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.runAsUser) ||
c.securityContext.runAsUser != 0
)
message: "Containers must not run as root (uid 0)"
- expression: >
!has(object.spec.securityContext) ||
!has(object.spec.securityContext.runAsUser) ||
object.spec.securityContext.runAsUser != 0
message: "Pod security context must not set runAsUser to 0"Policy: Images Must Come From Approved Registries
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: approved-registries
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >
object.spec.containers.all(c,
c.image.startsWith("your-registry.io/") ||
c.image.startsWith("gcr.io/your-project/")
)
message: "Images must come from approved registries: your-registry.io or gcr.io/your-project"Policy: Replica Count Limit (with Parameters)
# First, define a parameter schema
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: replica-limit
spec:
failurePolicy: Fail
paramKind:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= params.maxReplicas"
messageExpression: "'Replicas must not exceed ' + string(params.maxReplicas)"
---
# Bind with different limits per namespace
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: replica-limit-staging
spec:
policyName: replica-limit
paramRef:
name: staging-replica-limits
parameterNotFoundAction: Deny
matchResources:
namespaceSelector:
matchLabels:
env: staging
validationActions: [Deny]Audit Mode: Test Before Enforcing
One of the best features: you can run policies in Audit mode first. They log violations to the API server audit log without actually blocking anything.
validationActions:
- Audit # Log violations but don't block
# Change to Deny when you're confidentCheck what would have been blocked:
kubectl get events --field-selector reason=FailedAdmission -AWhen to Use This vs OPA/Kyverno
Use Validating Admission Policy (built-in CEL) when:
- Your policies are straightforward and expressible in CEL
- You want zero external dependencies (no Gatekeeper, no Kyverno to maintain)
- You're running Kubernetes 1.26+ (alpha), 1.28+ (beta), 1.30+ (GA)
- You need high-performance policy evaluation (no webhook round-trip)
Stick with OPA/Gatekeeper when:
- You need complex Rego logic that CEL can't express
- You need rich policy libraries (OPA has thousands of built-in rules)
- You need audit reporting and violation dashboards
- Your organization already standardized on OPA
Stick with Kyverno when:
- You need mutation (modifying resources, not just validating them) — CEL VAP doesn't do mutation
- You need policy generation (auto-creating NetworkPolicies per namespace, etc.)
- You prefer Kubernetes-native YAML over CEL expressions
- You want more mature tooling around exceptions and policy reports
The Bottom Line
Validating Admission Policy fills the gap between "I want basic enforcement without external tools" and "I need a full policy engine." For the 80% use case — enforce resource limits, block root containers, restrict registries — it's now the right default choice.
Save OPA and Kyverno for when you genuinely need their additional capabilities. Don't install a tool heavier than your problem requires.
Validate your Kubernetes YAML before applying: YAML Validator
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
How to Set Up HashiCorp Vault for Secrets Management from Scratch (2026)
HashiCorp Vault is the industry standard for secrets management. This step-by-step guide shows you how to install Vault, configure it, and integrate it with Kubernetes.
What is a Service Mesh? Explained Simply (No Jargon)
Service mesh sounds complicated but the concept is simple. Here's what it actually does, why teams use it, and whether you need one — explained without the buzzwords.
What is an Admission Webhook in Kubernetes Explained
Admission webhooks intercept every Kubernetes API request before it's persisted. Learn how mutating and validating webhooks work, with real examples from OPA, Istio, and custom webhooks.