Kyverno vs. Gatekeeper in 2026: Both Roads Lead to CEL — Pick Your Migration Tax

Sources

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

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-library with 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 validationFailureActionvalidationActions, patterns → CEL expressions, per-rule matchmatchConstraints, 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

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.