🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

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.

DevOpsBoysJun 13, 20265 min read
Share:Tweet

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:

  1. OPA/Gatekeeper — powerful, flexible, but requires deploying and maintaining a separate control plane component
  2. 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

yaml
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:

yaml
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:

bash
kubectl label namespace production enforce-policies=true

Now 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

yaml
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

yaml
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)

yaml
# 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.

yaml
validationActions:
- Audit   # Log violations but don't block
# Change to Deny when you're confident

Check what would have been blocked:

bash
kubectl get events --field-selector reason=FailedAdmission -A

When 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

Browse fixes
Newsletter

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

Comments