Kyverno vs Gatekeeper
Choosing an engine per constraint, not per fashion.
Kubernetes will run whatever YAML (a plain-text format for describing configuration) you hand it. A container running as root. An image pulled from a stranger's registry. A Deployment with no memory limit at all. Nothing in the platform says no, because nothing was ever asked to. A *policy engine* is the building inspector you post at the door: software that reads every resource on its way in, checks it against rules you wrote, and turns away the ones that fail. Kyverno and Gatekeeper are the two inspectors most teams hire, and they differ the way two inspectors might. One reads your blueprints in the language blueprints already come in (YAML). The other wants the building code translated into a specialist legal language called Rego, harder to learn, able to argue almost anything.
Both engines plug into the same socket. That socket is the *admission webhook*, a callback the Kubernetes API server (the front door that every kubectl command and every controller talks to) makes over HTTP before it writes any object to storage. It hands an outside service a veto. Gatekeeper wraps OPA (Open Policy Agent, the general-purpose engine from the first lesson) inside a Kubernetes controller, so your policies are Rego programs. Kyverno was built for Kubernetes and nothing else: its policies are Kubernetes resources themselves, written as declarative YAML. The failure they both exist to stop is the same one. Out of thousands of manifests (the YAML files that describe your resources), a single misconfigured one slips into production, and six weeks later it is an incident.
What each engine is doing inside the cluster
Kyverno splits the work across purpose-built controllers, like a shop with a doorman, a floor walker and a bookkeeper. The admission controller answers the webhook in real time. The background controller walks resources that already exist and applies the same rules to them. The reports controller writes down what was found. The policy itself lives in a ClusterPolicy object, which carries rules of four kinds: validate, mutate, generate, and verifyImages. Each rule picks its targets with declarative selectors. A validation rule compares a pattern (or a CEL expression, CEL being the Common Expression Language, a small condition language built into Kubernetes) against the object arriving at the door. For resources already running, the verdicts land in PolicyReport custom resources, one set per namespace (a namespace is a folder-like partition inside the cluster).
Gatekeeper takes the opposite shape: one controller-manager with OPA compiled in as a library. You write a ConstraintTemplate holding the Rego plus a schema for its parameters, and Gatekeeper turns that template into a brand-new CRD (Custom Resource Definition, the way you teach the Kubernetes API a new object type). A *constraint* is one instance of that new type. It binds the logic to particular kinds and namespaces with concrete parameter values, so a single template can back twenty constraints. A separate audit loop replays every constraint against what is already running, once every 60 seconds by default, and records violations in each constraint's status field.
One guardrail, two dialects
The fastest way to feel the difference is to write the same rule twice. Here is the rule: every Pod (the smallest thing Kubernetes runs, one or more containers scheduled together) must carry a team label. That label is the tag your on-call rotation reads to decide whose phone rings at 3am.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata:name: require-team-labelspec:rules:- name: check-teammatch:any:- resources:kinds:- Podvalidate:failureAction: Enforcemessage: "label 'team' is required for on-call routing"pattern:metadata:labels:team: "?*" # any non-empty value
Under twenty lines, and the pattern block is shaped like the Pod it is checking. You could read it aloud to a colleague who has never seen Kyverno and they would follow it. The Gatekeeper version arrives in two pieces: a template that holds the logic, and a constraint that binds it to something.
apiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:name: k8srequiredlabelsspec:crd:spec:names:kind: K8sRequiredLabelsvalidation:openAPIV3Schema:type: objectproperties:labels:type: arrayitems:type: stringtargets:- target: admission.k8s.gatekeeper.shrego: |package k8srequiredlabelsviolation[{"msg": msg}] {required := input.parameters.labels[_]not input.review.object.metadata.labels[required]msg := sprintf("missing required label: %v", [required])}---apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequiredLabelsmetadata:name: pods-must-have-teamspec:enforcementAction: denymatch:kinds:- apiGroups: [""]kinds: ["Pod"]parameters:labels: ["team"]
Same guardrail, roughly triple the YAML, plus a Rego program. That is the whole trade in miniature. Kyverno's declarative patterns get you there fast and then hit a ceiling. Gatekeeper hands you a real programming language, which, as the Rego lessons showed, earns its keep the moment the logic turns program-shaped: looping over nested structures, doing set arithmetic, comparing one field against another. Kyverno has closed part of that gap with CEL expressions and API-call contexts. Deeply conditional logic still reads better in Rego.
Enforce it, then test it offline
Install either engine with Helm (the package manager for Kubernetes). Three webhook replicas is the production floor, because the webhook now sits on the critical path of every matched API request, and one replica is one bad node away from trouble. Then go break your own policy on purpose. The denial message tells you which engine answered the door.
# Kyvernohelm repo add kyverno https://kyverno.github.io/kyverno/helm install kyverno kyverno/kyverno -n kyverno --create-namespace \--set admissionController.replicas=3# Gatekeeperhelm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/chartshelm install gatekeeper gatekeeper/gatekeeper -n gatekeeper-system \--create-namespace --set replicas=3kubectl apply -f kyverno-policy.yamlkubectl run nginx --image=nginx:1.29# -> Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:# -># -> resource Pod/default/nginx was blocked due to the following policies# -># -> require-team-label:# -> check-team: 'validation error: label ''team'' is required for on-call routing.# -> rule check-team failed at path /metadata/labels/team/'# Same experiment with Gatekeeper enforcing:kubectl run nginx --image=nginx:1.29# -> Error from server (Forbidden): admission webhook "validation.gatekeeper.sh"# -> denied the request: [pods-must-have-team] missing required label: team
Never iterate on policies by lobbing manifests at a live cluster to see what sticks. Both projects ship a command-line tool that evaluates policies against files on disk, and that is the piece you wire into CI (continuous integration, the pipeline that runs on every change).
# Kyverno CLI: evaluate a policy against a manifestkyverno apply kyverno-policy.yaml --resource bad-pod.yaml# -> Applying 1 policy rule(s) to 1 resource(s)...# -># -> policy require-team-label -> resource default/Pod/nginx failed:# -> 1. check-team: validation error: label 'team' is required for on-call routing.# -># -> pass: 0, fail: 1, warn: 0, error: 0, skip: 0# Gatekeeper's gator: same idea for templates + constraintsgator test -f gatekeeper-policy.yaml -f bad-pod.yaml# -> ["pods-must-have-team"] Message: "missing required label: team"
What breaks at scale
The two engines part company hardest over what happens to a violation after it has been found. Kyverno writes PolicyReport resources into every namespace. At 3,000 namespaces times dozens of policies, that is real pressure on etcd (the key-value database where the cluster keeps all of its state), so scope your reports deliberately and give the reports controller room to breathe. Gatekeeper goes the other way and parks audit findings in each constraint's status, cut off at constraintViolationsLimit (default 20). Past that line the status is a sample, not an inventory. Export violations through metrics or the audit export channel instead of reading a number off the object and believing it.
Both engines are also converging on the same escape hatch: ValidatingAdmissionPolicy, the CEL validation Kubernetes now has built in, which runs inside the API server with no network hop to a webhook at all. Each engine can generate one of these from its own policy format, so cheap checks stay in-process and the webhook is saved for logic CEL cannot express. Until you get there, remember that every webhook call adds a round trip to every request it matches. Scope your match blocks tightly rather than matching all kinds and filtering inside the policy.
failurePolicy: Ignore. If its pods are down, every violating resource sails straight through, silently, with no error anywhere for you to alert on. Kyverno ships with Fail, so an outage blocks admissions instead. That is the right instinct for security policies, and it can also stall a cluster recovery if you have not excluded the system namespaces. Pick a side deliberately, policy by policy. Run at least three webhook replicas behind a PodDisruptionBudget (a rule that stops Kubernetes draining too many of them at once), and alert on webhook error rate and latency whichever side you picked.Choosing an engine
The honest decision tree is short. Pick Kyverno when your platform team thinks in YAML and you need more than validation: mutate rules inject defaults, generate rules create companion resources (a NetworkPolicy for every new namespace, say), and verifyImages checks Sigstore signatures, Sigstore being the project that signs container images so you can prove where they came from. Gatekeeper largely cannot do those things. Its mutation is limited to a handful of simple assignment CRDs, and it has no generation at all. Pick Gatekeeper when Rego is already your organization's policy language across CI and your services, and you want one testable codebase enforced everywhere.
Whichever you pick, treat the policy CRDs as crown jewels. Anyone who can edit a ClusterPolicy or a constraint can switch off enforcement across the entire cluster, so RBAC (role-based access control, the permission system that decides who may touch what) on those objects has to be as tight as RBAC on RBAC itself. Pin the engine version. Set resource limits on its controllers. Watch the admission latency percentiles, because a slow policy engine puts a tax on every deploy in the cluster.
Picking the engine is the easy part. The policies it enforces are living code. They need versioning, a staged rollout from audit mode to enforce mode, exemptions written down somewhere a stranger can find them, and a clean way to retire, all without anyone bricking a Friday afternoon deploy. That operational discipline is the policy lifecycle, and it is where we go next.
Try this
Run these in a lab cluster or something else you can throw away, so you see the real shape of the output instead of a screenshot from someone's blog post.
kubectl get validatingadmissioncontrollers 2>/dev/null | headkubectl get crd | grep -E 'kyverno|gatekeeper' || true
…gatekeeper……kyverno…
Takeaway
Kyverno when you want YAML-native validate, mutate and generate. Gatekeeper with OPA when Rego's expressiveness and shared policy libraries are what you actually need. Engine fashion is not a strategy.
Next: ConstraintTemplates that scale without copy-pasting Rego for every constraint.