Policy-as-code for Kubernetes — written in YAML, not a new language. You'll install Kyverno, write policies that validate, mutate, and generate resources, verify image signatures, run policies in CI, and roll them out safely from audit to enforce. Hands-on throughout.
The journey:
| You need | Why |
|---|---|
A Kubernetes cluster + kubectl |
Kyverno runs as an admission controller |
| Helm (easiest install) | Deploys Kyverno |
| Basic manifest familiarity | Policies match on pod/Deployment fields |
Kyverno needs no new language — if you can read Kubernetes YAML, you can write policies. Confirm your cluster: kubectl get nodes.
Admission control is the checkpoint between kubectl apply and the resource being stored. A policy engine hooks in there to reject, modify, or react to resources against your rules — enforcing standards no reviewer can consistently catch: "no :latest images," "every pod has resource limits," "only our registries," "required owner labels."
Kyverno's differentiator vs OPA/Gatekeeper: policies are Kubernetes resources in YAML with familiar match/pattern syntax, so there's no Rego to learn. It does four things — validate, mutate, generate, and verifyImages — covering most platform-guardrail needs.
helm repo add kyverno https://kyverno.github.io/kyverno
helm repo update
helm install kyverno kyverno/kyverno --namespace kyverno --create-namespace
kubectl get pods -n kyverno # admission/background/cleanup controllers
Kyverno registers as a ValidatingWebhookConfiguration/MutatingWebhookConfiguration, so it sees resources as they're created or updated. Policies come as ClusterPolicy (cluster-wide) or Policy (namespaced) CRDs.
A validate rule checks a resource against a pattern and blocks (or audits) violations. Require every pod to set resource limits:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-limits
spec:
validationFailureAction: Audit # start in Audit (see Part 4)
rules:
- name: require-container-limits
match:
any:
- resources: { kinds: [Pod] }
validate:
message: "Every container must set resources.limits."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*" # ?* = any non-empty value
cpu: "?*"
kubectl apply -f require-limits.yaml
kubectl run nginx --image=nginx # a pod with no limits → policy report flags it
kubectl get policyreport -A # violations show up here
The pattern is an "overlay" the resource must match; ?* means "must be present and non-empty." Kyverno reports violations to PolicyReports you can query and dashboard.
validationFailureAction controls what a violation does:
Audit — allow the resource, record a violation in PolicyReports. Use this first, everywhere.Enforce — reject the resource at admission.The safe rollout: ship as Audit, watch PolicyReports for what would break, fix the offending workloads, then flip to Enforce. Flipping to Enforce blindly will block deploys and page you.
kubectl get clusterpolicyreport -o wide # cluster-wide violation counts
You can also scope policies with match/exclude (skip kube-system, target only certain namespaces) so guardrails don't break platform components.
A mutate rule modifies resources on admission — apply sane defaults instead of rejecting. Add a default securityContext so workloads are hardened by default:
spec:
rules:
- name: default-securitycontext
match:
any:
- resources: { kinds: [Pod] }
mutate:
patchStrategicMerge:
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
Mutation is powerful for platform ergonomics: inject labels, sidecars, imagePullSecrets, or default resources so teams get compliant defaults without editing every manifest. Prefer mutate (fix it) over validate (reject it) where a safe default exists.
A generate rule creates new resources when a trigger appears — e.g. give every new namespace a default-deny NetworkPolicy and a resource quota automatically:
spec:
rules:
- name: default-netpol
match:
any:
- resources: { kinds: [Namespace] }
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: default-deny
namespace: "{{request.object.metadata.name}}"
synchronize: true # keep it in sync; recreate if deleted
data:
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
generate turns "every namespace should have X" into an enforced guarantee, with synchronize: true re-creating the resource if someone deletes it. This is how platform teams bootstrap namespaces consistently.
verifyImages blocks unsigned or untrusted images at admission — supply-chain security enforced in the cluster:
spec:
rules:
- name: verify-signature
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences: ["ghcr.io/acme/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/acme/*"
issuer: "https://token.actions.githubusercontent.com"
Combined with signing in CI (cosign, keyless via OIDC), this guarantees only images your pipeline built and signed can run — closing the "someone deployed a random image" gap. Pair it with the least-privilege and provenance ideas from the GitHub Actions tutorial.
Catch policy violations at PR time, not at admission, with the Kyverno CLI:
# Test that a policy gives the expected result on sample resources
kyverno apply require-limits.yaml --resource bad-pod.yaml
kyverno test . # runs kyverno-test.yaml assertions in the repo
Wire kyverno apply/kyverno test into CI so a manifest that would be rejected fails the pull request. This shifts policy left — developers see the violation in their PR with the exact message, long before the cluster rejects it.
Audit, read PolicyReports, then flip to Enforce.kube-system, kyverno) so guardrails don't break the platform.generate + synchronize to guarantee per-namespace defaults (NetworkPolicy, quotas).verifyImages + signing.failurePolicy deliberately).Enforce. You'll block deploys cluster-wide. Audit first, always.kube-system or Kyverno itself.failurePolicy: Fail without HA. If the webhook is down, every admission fails — cluster-wide outage.kyverno test.synchronize on generate. A deleted generated resource won't come back without it.require-limits policy in Audit, create a bad pod, and find it in kubectl get policyreport.Enforce, and confirm a non-compliant pod is now rejected with your message.runAsNonRoot: true and verify a plain pod gets it.kyverno apply against a good and a bad manifest and see the pass/fail.Self-check:
generate + synchronize guarantee?You now have the loop: install → validate → audit-then-enforce → mutate → generate → verify images → test in CI. That's Kubernetes policy-as-code in production.