Kyverno vs. Gatekeeper in 2026: Both Roads Lead to CEL — Pick Your Migration Tax
Sources
- Kyverno docs — Policy Types overview and deprecation schedule (v1.19)
- Kyverno docs — Migrating to CEL Policies (ClusterPolicy field-by-field mapping)
- Kyverno v1.19.1 release (Sep 10, 2026) — security backports
- Kyverno GHSA-5qq8-67g6-4h2w — Privilege escalation to cluster admin via Policy apiCall urlPath (CVSS 9.9, patched 1.19.1)
- Kyverno GHSA-q825-p383-r9v5 — Legacy apiCall executor bypasses SSRF blocklist (CVSS 7.6, patched 1.19.1)
- Gatekeeper docs — Integration with Kubernetes Validating Admission Policy (K8sNativeValidation engine)
- Gatekeeper v3.23.1 release (Aug 27, 2026)
- CNCF — Kyverno graduation announcement (Mar 16, 2026)
- Gatekeeper docs — Mutation (Assign, AssignMetadata, ModifySet, AssignImage mutators)
- Kyverno docs — Evaluating Policy Engines
Every Kubernetes policy-engine comparison written before 2026 is now obsolete in one specific way: the Rego vs. YAML war is over, and CEL won. Kyverno deprecated its entire legacy ClusterPolicy API in v1.19 (removed in v1.20) in favor of CEL-based types. Gatekeeper added a K8sNativeValidation engine that compiles ConstraintTemplates into native Kubernetes ValidatingAdmissionPolicy resources, bypassing its own webhook. Both vendors read the same tea leaves: the Kubernetes API server itself is becoming the policy engine, and the surviving products are thin management layers around it.
This guide is the decision matrix for platform teams picking a policy engine in late 2026 — with the stuff vendor comparisons omit: what each tool's fresh security record actually says, what migrating an existing fleet costs, and who should skip both. Version baseline: Kyverno v1.19.1 (chart 3.9.1, released Sep 10, 2026) and Gatekeeper v3.23.1 (chart 3.23.1, released Aug 27, 2026), the current stable lines as of September 19, 2026.
TL;DR
- Pick Kyverno if: your platform team wants one tool for admission control plus mutation, generation, image verification, and cleanup — and you are willing to absorb a mandatory ClusterPolicy→CEL migration before v1.20 removes the old API. The policy language is now CEL, which your engineers already know from VAP and CRD validation rules.
- Pick Gatekeeper if: you already have Rego expertise, need referential policies (inspect the whole cluster state, not just the admitted object), or want a hardened separation between policy authoring (ConstraintTemplate) and policy instantiation (Constraint) that maps well to platform teams handing parameterized guardrails to app teams.
- The 2026 twist neither vendor advertises: the in-tree ValidatingAdmissionPolicy is now stable in K8s 1.30+ and free. If your policies fit in a single CEL expression, you can skip webhooks entirely. Both engines exist to manage what VAP cannot do — and that gap is shrinking every release.
- Security record, not marketing: Kyverno shipped five advisories in one day (Sep 10, 2026), including a CVSS 9.9 privilege escalation from namespace tenant to cluster admin. All patched in 1.19.1. Gatekeeper's last advisory predates this discussion by a year. Read the details below before you trust either webhook with your admission path.
- Who should skip both: clusters where a handful of static policies suffice — use native ValidatingAdmissionPolicy with no webhook at all. And teams without the operational capacity to babysit webhook HA and failure modes: a misconfigured admission webhook is a self-inflicted cluster outage.
The Convergence Nobody's Decision Matrix Mentions
Both engines started from opposite philosophies and are now walking toward each other. Kyverno began as "policies should look like the resources they govern" — declarative YAML patterns, no new language. Gatekeeper began as "policies are code" — full Rego, arbitrary computation, referential queries. Where they landed:
2021 2026
──── ────
KYVERNO KYVERNO
YAML patterns, JMESPath CEL expressions (policies.kyverno.io/v1)
"no new language" ValidatingPolicy / MutatingPolicy /
auto-generation, per-rule GeneratingPolicy / ImageValidatingPolicy
actions ClusterPolicy DEPRECATED (v1.19)
→ removed in v1.20
│
GATEKEEPER ▼
Rego, whole-cluster state → GATEKEEPER
ConstraintTemplate + K8sNativeValidation engine: CEL in
Constraint (authoring vs ConstraintTemplates
instantiation split) --default-create-vap-for-templates=true:
compiles templates into native
ValidatingAdmissionPolicy objects,
enforced in-process by kube-apiserver
(no webhook hop for simple policies)
│
▼
KUBE-APISERVER (the actual winner)
in-tree ValidatingAdmissionPolicy, GA since K8s 1.30
CEL expressions, no webhook, fail-closed by default
The strategic reading: both tools are repositioning as policy distribution and lifecycle layers on top of an engine that Kubernetes now ships natively. Kyverno's advantage is breadth (mutate/generate/cleanup/image-verify in one product, one RBAC model, one webhook fleet). Gatekeeper's advantage is depth (Rego for the policies that genuinely need cluster-wide referential logic, plus a two-layer authoring model that large orgs need for safe delegation).
Head-to-Head: The Table That Matters
| Dimension | Kyverno 1.19.x | Gatekeeper 3.23.x |
|---|---|---|
| Policy language | CEL (new types); legacy pattern/deny YAML deprecated | Rego (default) + CEL via K8sNativeValidation engine |
| Native VAP generation | No — own webhooks, CEL evaluated by Kyverno | Yes — K8sNativeValidation engine, beta since v3.20, on by default |
| Mutation | MutatingPolicy (CEL, stable v1.18); patches/strategic merge lineage | Assign / AssignMetadata / ModifySet / AssignImage mutators (stable since v3.10) |
| Generation | GeneratingPolicy — create/clone resources on triggers, with namespace targeting | Not built-in; sync data via Config — a different problem |
| Image verification | ImageValidatingPolicy — cosign/Notary signatures + attestations, stable | External-data providers or Rego workarounds; no first-class type |
| Referential policies | Limited — GlobalContextEntry caches external/cluster data | Yes — Rego queries full synced state; the canonical use case |
| Authoring model | One CRD per policy; policy = the object | Two layers: ConstraintTemplate (author) + Constraint (instantiate) |
| Test tooling | kyverno test + Chainsaw (E2E) |
gator test / gator verify (offline) |
| Webhook failure policy | spec.failurePolicy per policy; configurable |
validatingWebhookFailurePolicy + separate CEL default |
| Reporting | PolicyReport / ClusterPolicyReport (wgpolicyk8s.io/v1alpha2) | Constraint status + audit; violation events |
| CNCF status | Graduated (Mar 16, 2026) | Part of OPA (Graduated) |
What Rego still buys you
- Referential validation: "deny this Deployment if another Deployment in the namespace already claims port X." Rego queries Gatekeeper's synced cache of cluster state. Kyverno's GlobalContextEntry approximates this for external APIs, but in-cluster referential checks are Rego's home turf.
- Arbitrary computation: overlap detection, aggregation across objects, policy libraries like
gatekeeper-librarywith hundreds of battle-tested templates. - One language, many runtimes: the same Rego runs in Gatekeeper admission, CI (gator), and standalone OPA for HTTP APIs. If your org has cross-domain policy needs beyond Kubernetes, Rego is a career skill; CEL is a Kubernetes skill.
The September 2026 Security Ledger (Read This Before Trusting Either Webhook)
Both engines sit on your admission path with powerful ServiceAccounts. Their security records are not footnotes — they are the due-diligence section of your design doc. On September 10, 2026, Kyverno published five advisories simultaneously, all patched in v1.19.1:
| Advisory | Severity / CVSS | What it actually was |
|---|---|---|
GHSA-5qq8-67g6-4h2w |
Critical / 9.9 | Namespace tenant → cluster admin. A tenant with create on namespaced Policies crafts an apiCall with a percent-encoded urlPath (%2e%2e segments) that escapes the per-namespace clamp; the admission-controller SA then POSTs a MutatingWebhookConfiguration cluster-wide or drops a PolicyException into the protected kyverno namespace. Verified live on kind v1.31 with the unmodified default chart. |
GHSA-c5qq-7g2q-cpqp |
High / 7.7 | Same percent-encoding bypass class, namespaced Policy apiCall — cross-namespace object access. |
GHSA-q825-p383-r9v5 |
High / 7.6 | The legacy apiCall executor and GlobalContextEntry ignored the SSRF blocklist (169.254.169.254, loopback, metadata endpoints) that April 2026's fix applied only to the CEL http path — plus unconditional SA-token attachment to arbitrary outbound hosts. |
GHSA-59v6-2x73-wfg4 |
High / 7.7 | globalcontext.Lib CEL library not namespace-confined: tenant-authored NamespacedValidatingPolicy reads cross-namespace GlobalContextEntry data. |
GHSA-5cjf-wwfg-pj4c |
High / 7.7 | ImageValidatingPolicy exceptions ignored PolicyException.spec.images/allowedValues — exceptions fully bypassed image signature verification instead of partially exempting. |
The pattern across all five is identical and worth internalizing before you grant either engine cluster-wide powers: features bolted on beside the new CEL path kept the old, unaudited executor code alive. The apiCall SSRF blocklist was "fixed" in April but only wired into the CEL http executor — the legacy path every non-CEL service call flows through kept the hole. This is the tax of breadth: every additional surface (background controllers, image verification, external API calls) multiplies the audit surface. Gatekeeper's smaller, older surface has been quieter lately — its last repo advisory dates to 2021-era issues, with OPA core advisories (2022–2025) not touching the admission path. That is not proof of superiority; it is a smaller blast radius per feature.
The Same Policy, Written Three Ways
Require app and version labels on Deployments. First, what neither engine wants you to notice you can have for free — the in-tree ValidatingAdmissionPolicy (K8s 1.30+, no webhook, no controller, CEL evaluated inside kube-apiserver):
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-app-version-labels
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: >
has(object.metadata.labels) &&
'app' in object.metadata.labels &&
'version' in object.metadata.labels
message: "Deployments must carry 'app' and 'version' labels."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-app-version-labels-binding
spec:
policyName: require-app-version-labels
validationActions: ["Deny"]
Now the same rule in Kyverno 1.19's ValidatingPolicy — note it is the same CEL, wrapped in Kyverno's lifecycle (background scanning, reports, exceptions) instead of raw API machinery:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-app-version-labels
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validationActions: ["Deny"]
validations:
- expression: >
['app', 'version'].all(label,
object.metadata.?labels[label].orValue('') != ''
)
message: "Pods must carry non-empty 'app' and 'version' labels."
And in Gatekeeper, using the K8sNativeValidation engine — the ConstraintTemplate defines the schema and the CEL source; each Constraint instantiates it with parameters. Note the two-layer authoring model:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: object
properties:
key:
type: string
allowedRegex:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
code:
- engine: K8sNativeValidation
source:
validations:
- expression: '[object, oldObject].exists(obj, obj != null && has(obj.metadata) && variables.params.labels.all(entry, has(obj.metadata.labels) && entry.key in obj.metadata.labels))'
messageExpression: '"missing required label, requires all of: " + variables.params.labels.map(entry, entry.key).join(", ")'
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-version
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels:
- key: app
allowedRegex: ^.+$
- key: version
allowedRegex: ^.+$
Three observations from actually writing these out. First: the CEL in options 0, 1, and the Gatekeeper CEL engine is the same language — the skill transfers. Second: Kyverno's wrapper is the thinnest of the three, and its background scanning + PolicyReport output give you fleet visibility that raw VAP does not. Third: Gatekeeper's two-layer model forces a platform-vs-app-team contract — templates owned centrally, constraints parameterized by consumers — which is either bureaucracy or governance, depending on how many teams you have.
Operationally: What Each Engine Costs You to Run
KYVERNO (chart 3.9.1) GATEKEEPER (chart 3.23.1)
─────────────────────────── ───────────────────────────
4 Deployments + webhooks: 2 Deployments + webhooks:
admissionController (admission) controller-manager
backgroundController (scan/reports) (admission + audit loop)
reportsController (report server) (audit = singleton!)
cleanupController (DeletingPolicy) + gator (CI, offline)
+ per-controller HA: set replicas webhook namespace-exempt
per your SLO (default 1 each!) config via Config resource
or --exempt-namespace
Failure modes:
webhook down + Fail → ADMISSION BLOCKED same class, fewer moving
5 admission-path CVEs in 2026 parts, quieter CVE ledger
(all patched in 1.19.1) (no 2026 advisories)
Kyverno ships as four separate deployments, each independently scalable via Helm values — admissionController.replicas, backgroundController.replicas, cleanupController.replicas, reportsController.replicas. Default install runs a single replica of each: fine for a lab, a bet you do not want in prod. Gatekeeper is leaner: one controller-manager plus an audit pod. Its docs are refreshingly blunt that the audit loop runs as a singleton — one audit at a time, by design. Fewer moving parts is a real operational argument when your alternative is four deployments plus a reports server.
Both are webhook-first, so both share the classical admission-controller risk: set the failure policy to Fail, take the webhook down, and the cluster stops admitting anything that matches your rules. Kyverno exposes per-policy spec.failurePolicy on the new types; Gatekeeper separates the webhook-level failure policy (validatingWebhookFailurePolicy) from the CEL evaluation default (--default-k8s-native-validation-failure-policy, default Fail), with an explicit escape hatch (Ignore) for clusters that need admission to survive policy-config errors.
Migration Taxes: The Part Every Skippable Comparison Omits
| If you run… | Your 2026 obligation | Real cost |
|---|---|---|
| Kyverno ≤ 1.18 (ClusterPolicy) | Rewrite every policy to ValidatingPolicy/MutatingPolicy/GeneratingPolicy before v1.20 deletes the API | validationFailureAction → validationActions, patterns → CEL expressions, per-rule match → matchConstraints, podSecurity rules have no direct equivalent (rewrite each control as CEL). Kyverno publishes a field-by-field mapping table, but every deny-rule conversion requires logic inversion. |
| Gatekeeper with Rego templates | None forced. Optional: adopt the K8sNativeValidation engine per-template to move simple policies onto VAP | Low — additive, per-template, reversible. But Rego templates needing referential data stay on the webhook path forever. |
| No policy engine | Decide whether you need one at all | Native VAP covers simple validation with zero controllers. Reach for an engine when you need mutation, generation, image verification, reports, or referential logic — not before. |
| Kyverno namespaced variants in multi-tenant clusters | Patch to 1.19.1 immediately if you allow tenant-authored policies | The 9.9 advisory chain requires only tenant create on namespaced Policies. If your RBAC grants that, you are one YAML apply away from cluster admin. |
The multi-tenant trapdoor, concretely
The CVSS 9.9 advisory deserves an operator-grade reading because the fix pattern generalizes. Kyverno's admission controller runs as a ServiceAccount with broad create powers (it must, to auto-generate webhooks and resources). The per-namespace clamp on namespaced Policy.apiCall was enforced by URL normalization — which percent-encoding defeated. Two attack paths were demonstrated: (A) reach a cluster-scoped collection and create a MutatingWebhookConfiguration that rewrites every admitted object; (B) POST a PolicyException into the kyverno namespace to disable an enforcing policy for your namespace. Both start from a privilege every multi-tenant cluster already grants: the right to write a namespaced Policy. If you run Kyverno < 1.19.1 in a multi-tenant cluster and tenants can create Policies — upgrade today, and audit your PolicyException namespace for surprises.
Decision Tree
START: Do you need mutation, generation, image
verification, scheduled cleanup, or reports?
│
├─ NO, and policies are single-object checks
│ └─→ Use native ValidatingAdmissionPolicy.
│ Zero controllers. Zero webhooks. Done.
│
├─ YES, and you need referential checks
│ (cross-object, cross-namespace validation)
│ └─→ Gatekeeper + Rego (synced cache).
│ Kyverno's GlobalContextEntry is an
│ approximation, not a substitute.
│
├─ YES, and you want one product for the
│ full lifecycle (mutate+generate+verify+clean)
│ └─→ Kyverno 1.19.x, budget the CEL migration
│ before v1.20 removes ClusterPolicy.
│
└─ YES, and you are a large org with a central
platform team + many consuming teams
└─→ Gatekeeper's ConstraintTemplate/
Constraint split maps directly onto
"guardrail authors" vs "guardrail
consumers" — if that's your org chart,
that's your engine.
Installation, Verified
Both install from their official Helm repos. Kyverno (non-production baseline; add replica overrides for HA):
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace \
--set admissionController.replicas=3 \
--set backgroundController.replicas=2 \
--set cleanupController.replicas=2 \
--set reportsController.replicas=2
# Optional: full Pod Security Standards policy set
helm install kyverno-policies kyverno/kyverno-policies -n kyverno
Gatekeeper:
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper/gatekeeper --name-template=gatekeeper \
--namespace gatekeeper-system --create-namespace
Two post-install checks the docs undersell. For Kyverno: kubectl -n kyverno get deploy should show four deployments; if reportsController is at one replica and you rely on PolicyReports for compliance evidence, scale it before your next audit cycle, not during. For Gatekeeper: verify the generated VAPs match your exemptions — the --sync-vap-enforcement-scope flag (deprecated, removal in v3.24) was the mechanism ensuring generated VAPs honor namespace exemptions; on 3.23.x confirm with kubectl get validatingadmissionpolicy that the scope object reflects your Config-resource exemptions before you trust it.
Who Should Skip This Entire Category
- Teams under ~50 nodes with a static rulebook. Native VAP + PSA (Pod Security Admission) labels cover admission validation with zero new controllers. Every webhook you run is a new HA dependency on the path to
kubectl apply. - Anyone who cannot commit to webhook operations. Failure-policy
Failplus a controller outage equals a cluster that stops admitting workloads. If you do not have PagerDuty coverage for the policy engine, do not put it on the admission path — run audit-only (Warn) first for a quarter. - Teams mid-Kyverno-migration without a deadline. v1.20 removes ClusterPolicy; the migration is not optional. If you cannot schedule it, you cannot schedule the upgrade either — plan both as one workstream or you will be pinned on 1.19.x watching CVEs accumulate.
The Bottom Line
The 2026 policy-engine question is no longer "Rego or YAML" — it is "how much engine do you actually need, and who survives as the management layer above in-tree CEL." Kyverno bet its entire API on CEL and is paying the migration tax on your behalf, plus it owns the broader lifecycle (mutate, generate, verify images, clean up) — but its 2026 security ledger shows what admission-path breadth costs, and the five-advisory day should be mandatory reading before you grant it a tenant-writable CRD. Gatekeeper bet on coexistence: Rego where you need depth and referential reach, CEL-through-VAP where you do not, and a two-layer authoring model that scales organizationally. Pick Kyverno for lifecycle breadth and one language; pick Gatekeeper for referential depth and delegation; pick neither if a native VAP covers you — and check the date on any comparison article that tells you otherwise.