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
| Kind | What it does | Example |
|---|---|---|
| validate | Allow or deny the request | Require runAsNonRoot |
| mutate | Patch the object before persist | Add env: prod label |
| generate | Create related objects | Default 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)
- No privileged pods outside
kube-system - Images only from your registry (
ghcr.io/acme,*.dkr.ecr.*) resources.requestsset on every container- No
latesttag - Required labels:
app,owner - 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
- Enforce on day one across the whole cluster.
- Matching Pods created by a Deployment but not the Pod template — deny the Pod, the Deployment looks “fine” and ReplicaSet loops.
- Mutating fields Argo CD owns without
ignoreDifferences. - Exceptions with
*names. - 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.
