# Migrate a Kyverno ClusterPolicy to a CEL ValidatingPolicy

> After the upgrade to Kyverno 1.20 a ClusterPolicy says Ready and blocks nothing. Rewrite it in CEL, audit, add an exception, then enforce.

Level: Advanced · Topic: Security · About 18 minutes
Try it in the browser: https://k8wiz.com/playground/migrate-a-kyverno-clusterpolicy-to-a-validatingpolicy

The playground is a simulated Kubernetes cluster that runs in the browser. kubectl output and errors follow the real ones.

## The situation

The cluster was upgraded to Kyverno 1.20 last week. The `require-labels` ClusterPolicy is still there and `kubectl get cpol` still says it is Ready, but 1.20 no longer runs `kyverno.io` policies. `payments` went out in the `apps` namespace without a `version` label and nothing stopped it.

Bring the rule back as a `ValidatingPolicy` from `policies.kyverno.io/v1`. Start in Audit mode, read the policy reports, fix what would break, then switch to Deny.

The old policy excluded the `tools` namespace, where a vendor's agent runs with labels you can't change. Keep that exemption with a `PolicyException`. The legacy manifest is in the editor as `require-labels-cpol.yaml`.

## Objectives

1. Check that the legacy ClusterPolicy is still listed as Ready (`kubectl get clusterpolicies`. Then try `kubectl run test --image=nginx:1.29`: it is admitted)
2. Create the ValidatingPolicy require-labels, in Audit mode
3. Read the policy reports to see which Pods fail (`kubectl get policyreports -A`)
4. payments passes: its Pods carry app and version
5. A PolicyException exempts Pods in tools, and only those
6. The policy denies: a Pod without the labels is rejected in apps (`validationActions: [Deny]`)
7. Delete the legacy ClusterPolicy
8. web, payments and vendor-agent keep running

## Hints

1. A validate rule becomes a `ValidatingPolicy`. `match` turns into `matchConstraints.resourceRules`, the same shape a Kubernetes ValidatingAdmissionPolicy uses, and `validationFailureAction: Enforce` turns into `validationActions: [Deny]`. Use `[Audit]` first.
2. The pattern `app: "?*"` becomes CEL. A missing label must not make the expression error, so use optional access: `['app', 'version'].all(label, object.metadata.?labels[?label].orValue('') != '')`.
3. `kubectl get policyreports -A` has one report per scanned Pod, with PASS and FAIL counts. `kubectl describe policyreport -n apps` shows the message for each failure.
4. kubectl patch deployment payments --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"version":"1.4"}}}}}'
5. A `PolicyException` names the policy in `policyRefs` (with `kind: ValidatingPolicy`) and narrows itself with CEL `matchConditions`, such as `object.metadata.namespace == 'tools'`. Without a condition it would exempt every Pod in the cluster.
6. Once the reports show no failure left: `kubectl patch validatingpolicy require-labels --type=merge -p '{"spec":{"validationActions":["Deny"]}}'`, then `kubectl delete clusterpolicy require-labels`.

## Solution

```bash
kubectl get clusterpolicies
```
```bash
cat <<'EOF' | kubectl apply -f -
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-labels
spec:
  validationActions:
  - Audit
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: >-
      ['app', 'version'].all(label, object.metadata.?labels[?label].orValue('') != '')
    message: "Pods need the labels app and version"
EOF
```
```bash
sleep 6
```
```bash
kubectl get policyreports -A
```
```bash
kubectl patch deployment payments --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"version":"1.4"}}}}}'
```
```bash
cat <<'EOF' | kubectl apply -f -
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
  name: vendor-tools
  namespace: tools
spec:
  policyRefs:
  - name: require-labels
    kind: ValidatingPolicy
  matchConditions:
  - name: only-tools
    expression: "object.metadata.namespace == 'tools'"
EOF
```
```bash
kubectl rollout status deployment/payments
```
```bash
kubectl patch validatingpolicy require-labels --type=merge -p '{"spec":{"validationActions":["Deny"]}}'
```
```bash
kubectl delete clusterpolicy require-labels
```

## What it teaches

- Kyverno 1.20 stores `ClusterPolicy` and the other `kyverno.io` kinds but no longer runs them. Their status is not updated either, so a policy can read Ready and enforce nothing.
- The replacements each do one job: `ValidatingPolicy`, `MutatingPolicy`, `GeneratingPolicy`. They match with `matchConstraints` and express rules in CEL.
- CEL doesn't forgive missing fields the way patterns did. `object.metadata.?labels[?'app'].orValue('')` returns a default where `object.metadata.labels.app` would error.
- `validationActions: [Audit]` records results in PolicyReports without blocking, which shows what `Deny` would break before you turn it on.
- A `PolicyException` replaces `exclude` blocks you can't express as a selector. It points at a policy by kind and name and scopes itself with `matchConditions`.
