k8wiz
All graphs

Kyverno graph

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.

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.

Interactive graph

Click a resource to see what it does, or follow the setup step by step.

You write itCreated by a controllerselects by labelcreatesreferences

How it works

All 9 resources and what they do

Namespace team-av1
The trigger for the GeneratingPolicy, and the source of the team label the MutatingPolicy copies onto Pods.
GeneratingPolicy default-denypolicies.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-denynetworking.k8s.io/v1
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-labelpolicies.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-requestspolicies.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-agentpolicies.kyverno.io/v1
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 checkoutapps/v1
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 checkout-7c9d8-…v1
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>wgpolicyk8s.io/v1alpha2
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.

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

Everything above that is marked "You write it", in one file. The rest comes from add-ons and controllers.

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

Keep going

More graphs