Kyverno vs Gatekeeper
Choosing an engine per constraint, not per fashion.
In short. Kyverno vs Gatekeeper in Policy-as-code at scale: Choosing an engine per constraint, not per fashion.
Kubernetes will happily run whatever YAML you feed it: a container as root, an image from a stranger's registry, a Deployment with no resource limits. A *policy engine* is the inspector at the door — software that checks every resource against rules you write and rejects the ones that fail. Kyverno and Gatekeeper are the two dominant inspectors, and they differ the way two building inspectors might: one reads your blueprints in the language blueprints are already written in (YAML), the other requires the building code be translated into a dedicated legal language (Rego) that is harder to learn but can express almost anything.
Both engines plug into the same socket: the *admission webhook*, an HTTP callback the Kubernetes API server makes before persisting any object, giving an external service veto power. Gatekeeper wraps OPA — the general-purpose engine from the first lesson — in a Kubernetes controller, so policies are Rego programs. Kyverno was built for Kubernetes only: policies are themselves Kubernetes resources, written declaratively in YAML. The failure both prevent is identical: one misconfigured manifest, out of thousands, quietly reaching production and becoming an incident.
How each engine works internally
Kyverno runs as a set of purpose-split controllers: an admission controller that answers the webhook, a background controller that applies rules to resources that already exist, and a reports controller that records results. A ClusterPolicy holds rules of four types — validate, mutate, generate, and verifyImages — matched to resources with declarative selectors. Validation rules compare a pattern (or a CEL expression) against the incoming object; results for existing resources land in per-namespace PolicyReport custom resources.
Gatekeeper embeds OPA as a library inside a single controller-manager. You author a ConstraintTemplate containing Rego plus a parameter schema, and Gatekeeper compiles it into a brand-new CRD. Each *constraint* — an instance of that CRD — binds the logic to specific kinds and namespaces with concrete parameters. A separate audit loop replays every constraint against existing cluster state (every 60 seconds by default) and writes violations into each constraint's status field.
One guardrail, two dialects
The fastest way to feel the difference is the same rule in both engines. Require every Pod to carry a team label — the tag your on-call rotation uses to route pages.
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 of intent, and the pattern block reads like the Pod it validates. The Gatekeeper version splits into a template (the logic) and a constraint (the binding):
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 trade in miniature: Kyverno's declarative patterns bottom out fast, while Gatekeeper hands you a full language — which, as the Rego lessons showed, pays off when logic is genuinely program-shaped: loops over nested structures, set arithmetic, comparisons across fields. Kyverno narrows the gap with CEL expressions and API-call contexts, but deeply conditional logic still reads more naturally in Rego.
Enforce it, then test it offline
Install either engine with Helm — three webhook replicas is the production floor, because the webhook now sits on the critical path of every matched API request — then try to violate the policy. The denial message tells you which engine answered:
# 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 throwing manifests at a live cluster. Both projects ship CLIs that evaluate policies against local files — this is what belongs in your CI pipeline:
# 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 engines diverge hardest in what happens to violations after the fact. Kyverno writes PolicyReport resources into every namespace; at 3,000 namespaces times dozens of policies, that is real etcd pressure — scope reports deliberately and give the reports controller room. Gatekeeper goes the other way: audit findings live in each constraint's status, truncated at constraintViolationsLimit (default 20). At scale that status is a sample, not an inventory, so export violations through metrics or the audit export channel instead of reading them off the object.
Both engines are also converging on the same escape hatch: ValidatingAdmissionPolicy, Kubernetes' built-in CEL validation that runs inside the API server with no webhook hop. Each engine can generate these from its own policy format, keeping simple checks in-process and reserving the webhook for logic CEL cannot express. Until you get there, remember that every webhook call adds a network round trip to every matched request — scope match blocks tightly instead of matching all kinds and filtering inside the policy.
failurePolicy: Ignore: if its pods are down, every violating resource is admitted silently — an enforcement gap with no error to alert on. Kyverno defaults to Fail: an outage blocks admissions instead, which is right for security policies but can stall cluster recovery if system namespaces are not excluded. Choose a side explicitly per policy, run at least three webhook replicas behind a PodDisruptionBudget, and alert on webhook error rate and latency either way.Choosing an engine
The honest decision tree is short. Choose 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), and verifyImages checks Sigstore signatures — capabilities Gatekeeper largely lacks, since its mutation is limited to simple assignment CRDs and it has no generation at all. Choose Gatekeeper when Rego is already your organization's policy language across CI and 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 cluster-wide, so RBAC on those resources must be as tight as RBAC on RBAC itself. Pin the engine version, set resource limits on its controllers, and watch admission latency percentiles — a slow policy engine taxes every deploy in the cluster.
Picking the engine is the easy part. The policies it enforces are living code: they need versioning, staged rollout from audit to enforce, documented exemptions, and eventual retirement — without anyone bricking a Friday deploy. That operational discipline, the policy lifecycle, is where we go next.