Kubernetes

Kyverno Policies That Teams Will Actually Keep Enabled

Validate, mutate, and generate with Kyverno without turning every deploy into a 403 — policy types, failure actions, exceptions, and a starter set for platform clusters.

2026-09-07 · 4 min read

Admission policy fails in two ways: it is so loose it does nothing, or so tight that someone adds kubectl --validate=false folklore and the policy is quietly deleted.

Kyverno sits in the admission chain (and can also audit existing objects). The job is to encode the 10 rules you actually care about, with exceptions that expire.

Three verbs

KindWhat it doesExample
validateAllow or deny the requestRequire runAsNonRoot
mutatePatch the object before persistAdd env: prod label
generateCreate related objectsDefault NetworkPolicy in new namespaces

Start with validate + Audit. Switch a rule to Enforce after a week of PolicyReport noise you have actually cleaned up.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-non-root
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: run-as-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["prod", "staging"]
      validate:
        message: "containers must set securityContext.runAsNonRoot: true"
        pattern:
          spec:
            securityContext:
              runAsNonRoot: true
            containers:
              - securityContext:
                  runAsNonRoot: true

background: true reports pods that already exist. Admission alone does not fix last year’s Deployments.

Match narrowly

A ClusterPolicy that matches every Pod in every namespace will fight kube-system. Exclude it.

exclude:
  any:
    - resources:
        namespaces:
          - kube-system
          - kyverno
          - external-secrets

Or match only namespaces with policy.acme.io/enforce: "true". Platform teams label namespaces as they onboard; they do not surprise a vendor Helm chart on day one.

Mutate is a contract

Mutation that adds a label is fine. Mutation that rewrites image or resources will fight Helm and Argo CD: Git says one thing, the live object says another, the app flaps OutOfSync.

If you mutate, either:

  • commit the mutated form in Git (Kyverno as a CLI in CI, not only admission), or
  • tell Argo CD to ignore the exact fields (ignoreDifferences).

Prefer validate for anything an engineer should have written themselves.

Exceptions must die

apiVersion: kyverno.io/v2
kind: PolicyException
metadata:
  name: ingress-root-for-unifi
  namespace: ingress
spec:
  exceptions:
    - policyName: require-non-root
      ruleNames: ["run-as-non-root"]
  match:
    any:
      - resources:
          kinds: ["Pod"]
          namespaces: ["ingress"]
          names: ["ingress-nginx-controller*"]

Put a expires label or a ticket in metadata.annotations. Review exceptions monthly. A permanent exception is a deleted policy.

Generate default-deny carefully

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: default-deny-netpol
spec:
  rules:
    - name: deny-all
      match:
        any:
          - resources:
              kinds: ["Namespace"]
              selector:
                matchLabels:
                  netpol.acme.io/default-deny: "true"
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-all
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        data:
          spec:
            podSelector: {}
            policyTypes: ["Ingress", "Egress"]

synchronize: true means Kyverno will fight anyone who deletes the NetPol. That is what you want for a default-deny. It is not what you want if a team must punch a hole and Kyverno reverts their allow policy — generate only the deny, let teams add extra NetPols.

A starter Enforce set (after Audit is clean)

  1. No privileged pods outside kube-system
  2. Images only from your registry (ghcr.io/acme, *.dkr.ecr.*)
  3. resources.requests set on every container
  4. No latest tag
  5. Required labels: app, owner
  6. HostNetwork / hostPID denied

Leave “must have NetworkPolicy” as Audit until default-deny generate is rolled out. Otherwise you block every new namespace.

Testing policies

kyverno apply require-non-root.yaml --resource pod-good.yaml
kyverno apply require-non-root.yaml --resource pod-bad.yaml

Put pod-good.yaml / pod-bad.yaml next to the policy. CI should fail if a bad fixture is allowed or a good fixture is denied. Policies without fixtures rot.

Common pitfalls

  1. Enforce on day one across the whole cluster.
  2. Matching Pods created by a Deployment but not the Pod template — deny the Pod, the Deployment looks “fine” and ReplicaSet loops.
  3. Mutating fields Argo CD owns without ignoreDifferences.
  4. Exceptions with * names.
  5. No PolicyReport dashboard. Audit with nowhere to look is theatre.

Kyverno is not a replacement for policy-as-code on Terraform plans. Use both: Kyverno for cluster runtime, OPA/conftest for the apply that creates the cluster.

Platform evaluation

See OrchesTerra on your infrastructure

Request access to generate architecture from a repository, review the plan, and run governed reconciliation across AWS, Azure, GCP, and OCI.