Migrating from Traditional Policies to CEL

Complete guide for migrating from pattern/deny-based policies to CEL-based ValidatingPolicy

Migrating from Traditional Policies to CEL-based ValidatingPolicy

This guide helps you migrate from traditional Kyverno ClusterPolicy/Policy resources using validate.pattern or validate.deny to the new CEL-based ValidatingPolicy introduced in Kyverno v1.14.

Why Migrate to CEL-based Policies?

CEL-based ValidatingPolicy offers significant advantages over traditional policies:

  • Performance: 25% average latency improvement and up to 80% CPU reduction
  • Kubernetes Native: Can generate native ValidatingAdmissionPolicies automatically
  • Expressiveness: More powerful and flexible validation logic
  • Future-Ready: CEL is the strategic direction for Kubernetes policy validation

Understanding the Differences

Traditional ClusterPolicy Structure

 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: require-labels
 5spec:
 6  validationFailureAction: Enforce
 7  rules:
 8  - name: check-labels
 9    match:
10      any:
11      - resources:
12          kinds:
13          - Pod
14    validate:
15      message: "Required labels missing"
16      pattern:
17        metadata:
18          labels:
19            app: "?*"
20            version: "?*"

New ValidatingPolicy Structure

 1apiVersion: kyverno.io/v1alpha1
 2kind: ValidatingPolicy
 3metadata:
 4  name: require-labels
 5spec:
 6  validationActions: [Enforce]
 7  rules:
 8  - name: check-labels
 9    match:
10      any:
11      - resources:
12          kinds:
13          - Pod
14    validate:
15      cel:
16        expressions:
17        - expression: "has(object.metadata.labels.app) && has(object.metadata.labels.version)"
18          message: "Required labels 'app' and 'version' are missing"

Step-by-Step Migration Process

Step 1: Identify Migration Candidates

Start with policies that use:

  • Simple validate.pattern rules
  • Basic validate.deny conditions
  • Resource field validation
  • Label/annotation requirements

Not suitable for immediate migration:

  • Policies using mutate, generate, or verifyImages rules
  • Complex JMESPath expressions requiring external data
  • Policies requiring Kyverno-specific features like policy exceptions

Step 2: Convert Validation Logic

Pattern-based to CEL Conversion

Traditional Pattern:

1validate:
2  pattern:
3    spec:
4      containers:
5      - name: "*"
6        image: "!*:latest"

CEL Equivalent:

1validate:
2  cel:
3    expressions:
4    - expression: "object.spec.containers.all(container, !container.image.endsWith(':latest'))"
5      message: "Container images must not use 'latest' tag"

Deny-based to CEL Conversion

Traditional Deny:

1validate:
2  deny:
3    conditions:
4      all:
5      - key: "{{ request.object.spec.replicas }}"
6        operator: GreaterThan
7        value: 10
8  message: "Replica count cannot exceed 10"

CEL Equivalent:

1validate:
2  cel:
3    expressions:
4    - expression: "object.spec.replicas <= 10"
5      message: "Replica count cannot exceed 10"

Step 3: Handle Variable Translation

Built-in Variable Mapping

Traditional KyvernoCEL EquivalentDescription
{{ request.object }}objectThe resource being validated
{{ request.oldObject }}oldObjectPrevious version (for updates)
{{ request.operation }}request.operationOperation type (CREATE, UPDATE, DELETE)
{{ serviceAccountName }}request.userInfo.usernameService account name
{{ request.namespace }}object.metadata.namespaceResource namespace

Example Variable Migration

Traditional:

 1validate:
 2  deny:
 3    conditions:
 4      any:
 5      - key: "{{ request.operation }}"
 6        operator: Equals
 7        value: "CREATE"
 8      - key: "{{ request.object.metadata.namespace }}"
 9        operator: Equals
10        value: "kube-system"

CEL:

1validate:
2  cel:
3    expressions:
4    - expression: "!(request.operation == 'CREATE' && object.metadata.namespace == 'kube-system')"
5      message: "Cannot create resources in kube-system namespace"

Common Migration Patterns

1. Required Fields Validation

Traditional:

1validate:
2  pattern:
3    metadata:
4      labels:
5        app: "?*"
6        environment: "production|staging|development"

CEL:

1validate:
2  cel:
3    expressions:
4    - expression: "has(object.metadata.labels.app)"
5      message: "Label 'app' is required"
6    - expression: "object.metadata.labels.environment in ['production', 'staging', 'development']"
7      message: "Label 'environment' must be one of: production, staging, development"

2. Resource Limits Validation

Traditional:

1validate:
2  pattern:
3    spec:
4      containers:
5      - name: "*"
6        resources:
7          limits:
8            memory: "?*"
9            cpu: "?*"

CEL:

1validate:
2  cel:
3    expressions:
4    - expression: |
5        object.spec.containers.all(container,
6          has(container.resources.limits.memory) && has(container.resources.limits.cpu)
7        )
8      message: "All containers must specify CPU and memory limits"

3. Conditional Validation

Traditional:

 1validate:
 2  deny:
 3    conditions:
 4      all:
 5      - key: "{{ request.object.spec.containers[].securityContext.privileged || `false` }}"
 6        operator: Equals
 7        value: true
 8      - key: "{{ request.object.metadata.namespace }}"
 9        operator: NotEquals
10        value: "kube-system"

CEL:

1validate:
2  cel:
3    expressions:
4    - expression: |
5        !(object.spec.containers.exists(container, 
6          has(container.securityContext.privileged) && 
7          container.securityContext.privileged == true
8        ) && object.metadata.namespace != 'kube-system')
9      message: "Privileged containers not allowed outside kube-system namespace"

Advanced Migration Scenarios

Working with Lists and Arrays

Traditional (using foreach):

1validate:
2  foreach:
3  - list: "request.object.spec.containers"
4    deny:
5      conditions:
6        any:
7        - key: "{{ element.image }}"
8          operator: AnyIn
9          value: ["nginx:latest", "redis:latest"]

CEL:

1validate:
2  cel:
3    expressions:
4    - expression: "!object.spec.containers.exists(container, container.image in ['nginx:latest', 'redis:latest'])"
5      message: "Containers cannot use latest tags for nginx or redis"

Complex Object Validation

Traditional:

 1validate:
 2  pattern:
 3    spec:
 4      template:
 5        spec:
 6          containers:
 7          - name: "*"
 8            env:
 9            - name: "DATABASE_URL"
10              valueFrom:
11                secretKeyRef:
12                  name: "?*"

CEL:

 1validate:
 2  cel:
 3    expressions:
 4    - expression: |
 5        object.spec.template.spec.containers.all(container,
 6          !container.env.exists(envVar, 
 7            envVar.name == 'DATABASE_URL' && 
 8            (!has(envVar.valueFrom) || !has(envVar.valueFrom.secretKeyRef))
 9          )
10        )
11      message: "DATABASE_URL must be sourced from a secret"

Testing Your Migration

1. Validate CEL Expressions

Use the Kyverno CLI to test your CEL expressions:

1# Test with a sample resource
2kyverno apply validating-policy.yaml --resource test-pod.yaml

2. Side-by-Side Testing

Deploy both policies in different namespaces temporarily:

1# Test traditional policy in namespace 'old-policy-test'
2# Test CEL policy in namespace 'cel-policy-test'
3# Compare results with identical resources

3. Use Policy Reports

Monitor policy reports to ensure equivalent behavior:

1kubectl get polr -n test-namespace -o yaml

Performance Considerations

CEL Expression Optimization

Avoid:

1# Inefficient: Multiple separate validations
2expressions:
3- expression: "has(object.metadata.labels.app)"
4- expression: "has(object.metadata.labels.version)"  
5- expression: "has(object.metadata.labels.environment)"

Prefer:

1# Efficient: Combined validation
2expressions:
3- expression: |
4    ['app', 'version', 'environment'].all(label, 
5      has(object.metadata.labels[label])
6    )
7  message: "Required labels: app, version, environment"

ValidatingAdmissionPolicy Generation

Enable automatic generation for optimal performance:

1apiVersion: kyverno.io/v1alpha1
2kind: ValidatingPolicy
3metadata:
4  name: efficient-policy
5  annotations:
6    policies.kyverno.io/generate-validating-admission-policy: "true"
7spec:
8  # ... policy rules

Migration Checklist

  • Identify suitable policies for CEL migration
  • Convert validation logic from pattern/deny to CEL expressions
  • Update variable references to CEL syntax
  • Test CEL expressions with sample resources
  • Validate behavior matches original policy
  • Enable ValidatingAdmissionPolicy generation if appropriate
  • Monitor performance improvements
  • Update documentation and runbooks
  • Train team on CEL syntax and debugging

Troubleshooting Common Issues

CEL Expression Errors

Error: unknown field 'nonexistent' Solution: Use has() function to check field existence:

1expression: "has(object.spec.nonexistent) && object.spec.nonexistent == 'value'"

Error: index out of bounds Solution: Check array length before accessing:

1expression: "size(object.spec.containers) > 0 && object.spec.containers[0].image != 'bad'"

Variable Migration Issues

Issue: Traditional variables not working in CEL Solution: Use CEL built-in variables or resource library:

1# Instead of {{ request.object.metadata.name }}
2expression: "object.metadata.name"
3
4# For external data, use resource library
5expression: "resource.get('v1', 'ConfigMap', 'default', 'my-config') != null"

Next Steps

After successfully migrating to ValidatingPolicy:

  1. Explore ImageValidatingPolicy for supply chain security
  2. Consider MutatingPolicy for resource modifications (v1.15+)
  3. Implement policy exceptions using CEL expressions
  4. Set up monitoring for policy performance and compliance

Additional Resources


Note: This migration guide focuses on ValidatingPolicy. For mutate, generate, or verifyImages rules, continue using ClusterPolicy until MutatingPolicy, GeneratingPolicy, and ImageValidatingPolicy meet your specific requirements.