# Kyverno CEL policies: ValidatingPolicy, MutatingPolicy, GeneratingPolicy and exceptions

> How Kyverno 1.20's policy types act on one team namespace: a MutatingPolicy sets defaults, a ValidatingPolicy rejects Pods, a GeneratingPolicy adds a NetworkPolicy, a PolicyException exempts one tool and PolicyReports record the results.

Interactive graph: https://k8wiz.com/graphs/kyverno-validating-mutating-generating-policies

Kyverno 1.20 runs policies from the `policies.kyverno.io` group only. Each kind does one job: `MutatingPolicy` changes an object on its way in, `ValidatingPolicy` accepts or rejects it, and `GeneratingPolicy` creates other objects when a trigger appears. All three select objects with `matchConstraints` and express their logic in CEL.

This graph follows one namespace, `team-a`, from the moment it is created to a Pod being admitted into it, and shows where each policy acts and what it leaves behind.

## Resources

### Namespace team-a

you write it, v1

The trigger for the GeneratingPolicy, and the source of the `team` label the MutatingPolicy copies onto Pods.

### GeneratingPolicy default-deny

you write it, policies.kyverno.io/v1

When a Namespace is created, the background controller evaluates `generate` and creates what `generator.Apply` lists, here a default-deny NetworkPolicy. `generateExisting` covers namespaces older than the policy, and `synchronize` puts a deleted copy back.

### NetworkPolicy default-deny

created by a controller, networking.k8s.io/v1, namespace team-a

Generated by Kyverno, with labels that name the policy and the trigger. It selects every Pod and allows no ingress, so the team has to add policies for the traffic it wants.

### MutatingPolicy add-team-label

you write it, policies.kyverno.io/v1

Runs in the mutating webhook, before validation. It copies the namespace's `team` label onto each new Pod with an `ApplyConfiguration` patch, and the match condition skips namespaces that have no such label.

### ValidatingPolicy require-requests

you write it, policies.kyverno.io/v1

Runs in the validating webhook, after every mutation. Each expression must be true or the request is denied with the message. `Audit` in place of `Deny` only records the result.

### PolicyException vendor-agent

you write it, policies.kyverno.io/v1, namespace team-a

Exempts the vendor's agent, whose chart sets no requests. It names the policy by kind and name, and its match conditions keep the exemption to Pods whose name starts with `vendor-agent`.

### Deployment checkout

you write it, apps/v1, namespace team-a

The team's workload. The policies match Pods, so the Deployment itself is accepted, and it is its ReplicaSet that sees the denial when a Pod breaks a rule.

### Pod ×2 checkout-7c9d8-…

created by a controller, v1, namespace team-a

Created by the ReplicaSet. On the way in they gain `team: payments` from the MutatingPolicy, then pass the ValidatingPolicy because both requests are set.

### PolicyReport <pod uid>

created by a controller, wgpolicyk8s.io/v1alpha2, namespace team-a

One per scanned object, written by the reports controller. It lists each policy's result as `pass`, `fail` or `skip`, and `kubectl get policyreports -A` sums them up.

## How the resources connect

- GeneratingPolicy default-deny → Namespace team-a: matchConstraints (selects)
- GeneratingPolicy default-deny → NetworkPolicy default-deny: generator.Apply (creates)
- MutatingPolicy add-team-label → Namespace team-a: namespaceObject (references)
- Deployment checkout → Pod ×2 checkout-7c9d8-…: ReplicaSet creates (creates)
- MutatingPolicy add-team-label → Pod ×2 checkout-7c9d8-…: adds team label (selects)
- ValidatingPolicy require-requests → Pod ×2 checkout-7c9d8-…: validates (selects)
- PolicyException vendor-agent → ValidatingPolicy require-requests: policyRefs (references)
- NetworkPolicy default-deny → Pod ×2 checkout-7c9d8-…: podSelector: {} (selects)
- PolicyReport <pod uid> → Pod ×2 checkout-7c9d8-…: scope (references)
- PolicyReport <pod uid> → ValidatingPolicy require-requests: results (references)

## How it works

1. **A namespace triggers generation.** Creating `team-a` matches the GeneratingPolicy. A few seconds later the background controller creates `default-deny` in it. Delete that NetworkPolicy and `synchronize` recreates it.
2. **The Deployment creates Pods.** The Deployment is admitted untouched, since the policies match `pods`. Its ReplicaSet sends one create request per Pod, and each request goes through Kyverno's webhooks.
3. **Mutation comes first.** The MutatingPolicy reads the Namespace through `namespaceObject` and merges `team: payments` into the Pod's labels. Validation then sees the Pod as it will be stored.
4. **Validation accepts or denies.** The ValidatingPolicy checks every container for requests. A Pod without them is rejected, and the ReplicaSet reports `FailedCreate` with the policy's message in its events.
5. **An exception skips one tool.** For a Pod named `vendor-agent-…`, Kyverno finds the PolicyException before it runs the validations and skips the policy. The report shows `skip` for that Pod.
6. **Reports show the state of what already runs.** Admission only sees new requests. The background scan evaluates the ValidatingPolicy against existing Pods and writes a PolicyReport for each, which is how you see what `Deny` would break before you turn it on.

## Common mistakes

- Kyverno 1.20 stores `ClusterPolicy` and the other `kyverno.io` kinds and no longer runs them. `kubectl get cpol` can still say Ready while nothing is enforced.
- CEL errors on a missing field. `object.metadata.labels.team` fails on a Pod with no labels, where `object.metadata.?labels[?'team'].orValue('')` returns a default.
- A policy that matches Pods doesn't reject the Deployment that would create them. Look at the ReplicaSet's events, or `kubectl describe replicaset`, for the denial.
- A PolicyException without `matchConditions` exempts every object the policy matches, in every namespace.
- With `synchronize` on, deleting the GeneratingPolicy deletes what it generated. Turn it off first if the generated objects have to stay.

## The manifests you write

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: team-a
  labels:
    team: payments
---
apiVersion: policies.kyverno.io/v1
kind: GeneratingPolicy
metadata:
  name: default-deny
spec:
  evaluation:
    generateExisting:
      enabled: true
    synchronize:
      enabled: true
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["namespaces"]
  variables:
    - name: downstream
      expression: >-
        [{
          "apiVersion": dyn("networking.k8s.io/v1"),
          "kind": dyn("NetworkPolicy"),
          "metadata": dyn({"name": "default-deny"}),
          "spec": dyn({"podSelector": dyn({}), "policyTypes": dyn(["Ingress"])})
        }]
  generate:
    - expression: generator.Apply(object.metadata.name, variables.downstream)
---
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: add-team-label
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
  matchConditions:
    - name: namespace-has-team
      expression: namespaceObject.metadata.?labels[?'team'].hasValue()
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: >-
          Object{metadata: Object.metadata{labels: {"team": namespaceObject.metadata.labels["team"]}}}
---
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-requests
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          c.?resources.?requests[?'cpu'].hasValue() &&
          c.?resources.?requests[?'memory'].hasValue())
      message: Every container needs CPU and memory requests
---
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
  name: vendor-agent
  namespace: team-a
spec:
  policyRefs:
    - name: require-requests
      kind: ValidatingPolicy
  matchConditions:
    - name: only-the-agent
      expression: object.metadata.name.startsWith('vendor-agent')
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: team-a
spec:
  replicas: 2
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: ghcr.io/acme/checkout:2.4.1
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
```
