Kyverno
Verified against Official docs — kyverno.io/docs/kyverno-cli, kyverno.io/docs/policy-types/cluster-policy, · official docs
What it is and where it fits 🎯#
Kyverno is a Kubernetes-native policy engine — it validates, mutates, and generates cluster resources at
admission time, the same architectural slot OPA/Gatekeeper occupies (both covered in
Container & Kubernetes Security's admission
control material). The difference that matters day to day: Kyverno policies are written in plain
Kubernetes YAML, not Rego — a ClusterPolicy custom resource with a match/validate block reads like
any other Kubernetes manifest a platform team already writes, with no separate policy language to learn.
Kyverno also does something OPA/Gatekeeper structurally can't do as naturally: it can mutate a
non-compliant resource to fix it automatically (inject a missing resource limit, add a required label)
instead of only rejecting it outright — and it can generate a whole new resource in response to
another one being created (auto-creating a default NetworkPolicy whenever a new Namespace appears).
Kyverno sits alongside OPA and Conftest (both covered in this series) as a third way to do
policy-as-code, but at a different layer: OPA/Conftest most commonly validate config files in a CI
pipeline (a Terraform plan, a raw Kubernetes YAML before it's ever applied); Kyverno validates/mutates
live admission requests as they hit the Kubernetes API server, and — via verifyImages below — can
also enforce container image signature policy at that exact same admission boundary.
Where Kyverno sits in the admission-control pipeline#
All four rule types (validate, mutate, generate, verifyImages) run through the exact same
admission-webhook mechanism — the difference is entirely in what the policy declares it should do once
matched.
Installation#
# Install the Kyverno controller into a cluster (Helm — the officially recommended method)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
# ...or the raw manifest, useful for quick local testing (kind, minikube)
kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.13.0/install.yaml
kubectl -n kyverno wait --for=condition=available --timeout=180s deployment/kyverno-admission-controller
# The Kyverno CLI — for offline policy testing, no cluster required
brew install kyverno # Homebrew
kubectl krew install kyverno # as a kubectl plugin: kubectl kyverno ...
go install github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno@latest
kyverno versionCore concepts#
| Concept | What it means |
|---|---|
ClusterPolicy | Cluster-wide policy resource — applies to matching resources in every namespace |
Policy | The namespaced equivalent — only applies within the namespace it's created in |
validate rule | Accepts or rejects a resource based on a pattern/condition — the "gatekeeper" behavior |
mutate rule | Patches a resource before it's persisted — Kyverno's standout capability versus Rego-based tools |
generate rule | Creates a brand-new resource in reaction to another resource being created (or a source resource changing) |
verifyImages rule | Verifies a container image's Cosign signature/attestations before allowing the Pod to admit |
validationFailureAction | Audit (log only, don't block — for rolling out a new policy safely) vs Enforce (actually reject) |
Writing and testing a validate policy#
# disallow-latest-tag.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
rules:
- name: require-image-tag
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Images must not use the ':latest' tag — pin an explicit version."
pattern:
spec:
containers:
- image: "!*:latest"kubectl apply -f disallow-latest-tag.yaml
# Offline dry-run against a resource file, no cluster admission needed — fast local feedback loop
kyverno apply disallow-latest-tag.yaml --resource notifier-deployment.yaml
kubectl apply -f notifier-deployment.yaml # if it uses :latest, this is now REJECTED at admissionSample rejection message a developer actually sees:
Error from server: error when creating "notifier-deployment.yaml": admission webhook
"validate.kyverno.svc-fail" denied the request:
resource Pod/default/notifier violates policy disallow-latest-tag:
require-image-tag: 'validation error: Images must not use the '\'':latest'\'' tag —
pin an explicit version. rule require-image-tag failed at path /spec/containers/0/image/'
Tip
Start every new policy with validationFailureAction: Audit, not Enforce. Audit mode logs every
would-be violation without blocking anything — it's how you find out a policy would have broken half
the cluster's existing deployments before flipping it to Enforce and actually blocking traffic.
Writing a mutate policy — Kyverno's standout feature#
# add-default-resource-limits.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-resource-limits
spec:
rules:
- name: default-limits
match:
any:
- resources:
kinds: ["Pod"]
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
resources:
limits:
=(memory): "256Mi"
=(cpu): "500m"kyverno apply add-default-resource-limits.yaml --resource notifier-deployment.yaml
# shows the RESULTING mutated resource — inspect this before applying to a real clusterThe =(memory) syntax means "set this value only if the field isn't already present" — a Deployment that
already declares its own limits is left untouched; one that doesn't gets a sane default injected
automatically, rather than being rejected and sent back to the developer to fix by hand.
Writing a generate policy#
# generate-default-networkpolicy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-default-networkpolicy
spec:
rules:
- name: default-deny-new-namespace
match:
any:
- resources:
kinds: ["Namespace"]
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"]Every new Namespace created after this policy is active automatically gets a default-deny
NetworkPolicy the moment it exists — directly reusing this series' network-segmentation principle from
Part 3, but enforced structurally rather than depending on every team remembering to add one themselves.
verifyImages — enforcing Cosign signatures at admission#
# require-signed-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: check-image-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "registry.example.com/checkout-service/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/my-org/checkout-service/.github/workflows/*"
issuer: "https://token.actions.githubusercontent.com"This is the same keyless (Fulcio/Rekor-backed) verification model as cosign verify (its own cheat
sheet) — Kyverno just enforces it as an admission gate instead of a manual CI step, so an image that was
never signed by the expected GitHub Actions workflow identity is rejected before a Pod running it can
ever start, regardless of whether the CI pipeline itself was bypassed some other way.
Testing policies with the Kyverno CLI test framework#
# kyverno-test.yaml
name: disallow-latest-tag-test
policies:
- disallow-latest-tag.yaml
resources:
- notifier-deployment.yaml
results:
- policy: disallow-latest-tag
rule: require-image-tag
resource: notifier
kind: Pod
result: failkyverno test . # runs every kyverno-test.yaml under the current directory
kyverno test . -v # verbose, per-rule resultsRunning kyverno test in CI, against every policy in a platform repo, before those policies are ever
promoted to Enforce on a real cluster, is the direct policy-engine equivalent of the "write a denied
test case, not just an allowed one" discipline this series' OPA cheat sheet also emphasizes.
Real-world scenario: rolling out policy without an outage#
- Write the policy with
validationFailureAction: Auditand apply it. - Run
kubectl get policyreport -A(orclusterpolicyreport) after a day of real traffic to see every resource that would have been rejected. - Triage the audit findings — fix genuinely non-compliant workloads, or add a documented
excludeblock for a legitimate exception, before flipping anything toEnforce. - Flip
validationFailureAction: Enforceonly once the audit report is clean (or the remaining violations are explicitly excluded), and monitor admission-controller logs immediately after.
Warning
Flipping straight to Enforce on a brand-new policy against a live production cluster is how an
entire team's deploy pipeline gets blocked at 2am. Audit mode exists specifically to de-risk this —
skipping it to "save time" is the single most common way a policy rollout turns into an incident.
CI/CD integration recipe — pre-flight policy testing before merge#
# .github/workflows/kyverno-policy-test.yml
name: Kyverno policy tests
on: [pull_request]
jobs:
kyverno-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Kyverno CLI
run: |
curl -LO https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_x86_64.tar.gz
tar -xzf kyverno-cli_linux_x86_64.tar.gz && sudo mv kyverno /usr/local/bin/
- name: Run policy tests
run: kyverno test ./policies/Common pitfalls#
- Skipping
Auditmode for a new policy. See the WARNING above. - Confusing
mutate's=(field)conditional-set syntax with an unconditional overwrite.=(memory)only sets a value if absent; omitting the=()wrapper unconditionally overwrites whatever was there, including a value the developer deliberately set. - Forgetting
synchronize: trueon ageneraterule when the generated resource should stay in sync with future changes to its source — without it, the generated resource is created once and then left alone even if the policy's template later changes. - Assuming
kyverno apply(the CLI dry-run) and real cluster admission behave identically for every rule type.generaterules in particular have cluster-side effects the offline CLI can't fully simulate — always confirm behavior against a real (non-production) cluster before enforcing broadly.
When to reach for something else#
For validating config files before they're ever applied — a Terraform plan, a raw Kubernetes manifest in CI, a Dockerfile — reach for Conftest (its own cheat sheet), which is a more ergonomic front end over the same policy-as-code idea for that specific job. For hand-authoring or debugging the underlying Rego a Gatekeeper-based cluster already relies on, reach for OPA directly. If a team is already fully standardized on OPA/Gatekeeper for admission control and doesn't specifically need Kyverno's mutate/generate capabilities, there's limited reason to run both policy engines side by side — pick one per cluster to avoid two independent, potentially-conflicting sources of admission decisions.