Kyverno vs OPA Gatekeeper in 2026: CEL, Rego, or Native Admission Policy
Kyverno 1.19 deprecated ClusterPolicy and went all in on CEL, Gatekeeper still speaks Rego, and Kubernetes now ships validating and mutating policies natively. How to pick, with the same policy written three ways.
Key takeaways
- Pick Kyverno if you want one Kubernetes-native tool for validation, mutation, resource generation and image signature checks, written in CEL.
- Pick Gatekeeper if your organization already writes Rego or shares policy with OPA elsewhere.
- For simple validation rules on Kubernetes 1.30 or later, native ValidatingAdmissionPolicy needs no webhook at all.
On this page
Choose Kyverno when you want validation, mutation, resource generation and image signature verification in one Kubernetes-native tool. Choose Gatekeeper when your team already writes Rego or shares OPA policy beyond Kubernetes. And check first whether you need either: as of Kubernetes 1.36, both ValidatingAdmissionPolicy and MutatingAdmissionPolicy are stable, in-process, and webhook-free.
Verified against
Kyverno 1.19.2, Gatekeeper 3.23.1 (bundling OPA 1.17.1), Kubernetes ValidatingAdmissionPolicy (stable since 1.30) and MutatingAdmissionPolicy (stable since 1.36), against kyverno.io, the Gatekeeper docs and the kubernetes.io admission policy reference. Checked October 2026.
What changed in 2026
The comparison most teams remember is "Kyverno is YAML, Gatekeeper is Rego". That is out of date. Kyverno's policy types page now lists ClusterPolicy and CleanupPolicy as deprecated in 1.19 and removed in 1.20. Their replacements, ValidatingPolicy, MutatingPolicy, GeneratingPolicy, DeletingPolicy and ImageValidatingPolicy, are all stable since 1.18 and use CEL, the same expression language as the Kubernetes built-ins. A ValidatingPolicy is described as a superset of a ValidatingAdmissionPolicy.
Gatekeeper moved toward CEL from the other side. Its K8sNativeValidation engine lets a ConstraintTemplate carry CEL next to (or instead of) Rego, stable since Gatekeeper 3.18, and it can generate native ValidatingAdmissionPolicy objects so the API server enforces simple rules itself.
So the real question in 2026 is less "YAML or Rego" and more "how much do I need beyond what the API server already does?"
The comparison table
| Kyverno 1.19 | Gatekeeper 3.23 | Native VAP / MAP | |
|---|---|---|---|
| Policy language | CEL (legacy JMESPath types removed in 1.20) | Rego, plus CEL via K8sNativeValidation | CEL |
| Mutation | Yes, MutatingPolicy | Yes, Assign, AssignMetadata, ModifySet, AssignImage | Yes, MAP (stable since 1.36) |
| Generate resources | Yes, GeneratingPolicy | No | No |
| Image signature checks | Built in, ImageValidatingPolicy (cosign, Notary) | Through external data providers (beta) | No |
| Audit of existing resources | Background scans and policy reports | Audit controller | No, admission only |
| Runs where | Webhook, plus CLI and JSON payloads | Webhook, audit, gator CLI | Inside the API server |
The same policy three ways: disallow :latest
Kyverno, as a CEL ValidatingPolicy. The autogen block makes Kyverno write the matching rules for Deployments, StatefulSets, Jobs and CronJobs, so users get the error at kubectl apply time rather than when a ReplicaSet fails to create pods:
# kyverno/disallow-latest.yaml
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: disallow-latest-tag
spec:
validationActions: [Deny]
autogen:
podControllers:
controllers: [deployments, statefulsets, jobs, cronjobs]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
validations:
- expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "Pin an image tag or digest; :latest is not allowed."
Gatekeeper, using the k8sdisallowedtags template from the official gatekeeper-library. You install the template once, then each rule is a small Constraint:
# gatekeeper/no-latest.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
name: container-image-must-not-have-latest-tag
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
tags: ["latest"]
The Rego inside that template is the part your team would own if you wrote your own:
violation[{"msg": msg}] {
container := input_containers[_]
not is_exempt(container)
tags := [tag_with_prefix | tag := input.parameters.tags[_]; tag_with_prefix := concat(":", ["", tag])]
strings.any_suffix_match(container.image, tags)
msg := sprintf("container <%v> uses a disallowed tag <%v>; disallowed tags are %v", [container.name, container.image, input.parameters.tags])
}
Native, with no engine installed. A policy plus a binding, enforced by the API server:
# native/disallow-latest.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: disallow-latest-tag
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
validations:
- expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "Pin an image tag or digest; :latest is not allowed."
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: disallow-latest-tag
spec:
policyName: disallow-latest-tag
validationActions: [Deny]
Notice the CEL expression is identical in the Kyverno and native versions. That is the strongest argument for CEL in 2026: a rule you write today can move between Kyverno and the API server without a rewrite. None of the three CEL rules above catches an untagged image such as nginx, which defaults to latest; the Gatekeeper library template does, with a second violation rule.
The failure mode that causes the outages
Every webhook engine has to answer one question: what happens to API requests when the webhook is down?
Gatekeeper's docs answer it directly: it defaults to failurePolicy: Ignore, so when the webhook is unreachable, constraints are not enforced, and audit is expected to catch what slipped through. That is fail-open by design. You can switch to Fail, and the docs walk through what to consider first.
Kyverno's platform notes document the opposite failure. On EKS with the VPC CNI, if every node is gone (a single node group scaled to zero), a Fail-mode policy matching Pods can block the CNI's own DaemonSet pods, so nodes never become ready, so Kyverno never comes back to answer the webhook. Kyverno has excluded kube-system from its webhooks by default since 1.12 for exactly this reason. Don't remove that exclusion, and give the admission controller several replicas.
Native policies sidestep this class of outage because there is no webhook to be unreachable. A ValidatingAdmissionPolicy defaults to failurePolicy: Fail, but that governs CEL errors and misconfiguration, not network calls.
Where this breaks
CEL is deliberately limited. Rules that need to look up other cluster objects, call external data or loop over large lists are where Rego and Kyverno's extra CEL libraries earn their keep, and where native VAP runs out. Kyverno's migration also has a deadline: if your repo is full of kyverno.io/v1 ClusterPolicy files, those stop working in 1.20, so budget the conversion now. And Gatekeeper's image verification depends on a separate external data provider you operate yourself.
Questions people ask
Is Kyverno better than OPA Gatekeeper?
For Kubernetes-only teams, usually yes, because one tool covers validation, mutation, generation, cleanup and image verification, all in CEL. Gatekeeper is the better fit when you already run OPA for APIs, Terraform or CI and want one Rego codebase across all of them. Both audit existing resources as well as blocking new ones, so the decision is mostly about language and scope.
Is Kyverno ClusterPolicy deprecated?
Yes. As of October 2026, Kyverno's docs list ClusterPolicy (kyverno.io/v1) and CleanupPolicy as deprecated in 1.19 and removed in 1.20. The CEL-based replacements, such as ValidatingPolicy, MutatingPolicy and ImageValidatingPolicy, are stable since 1.18 and, per the docs, at full feature parity in 1.19. Kyverno publishes a migration guide for converting existing policies.
Do I still need Kyverno or Gatekeeper with ValidatingAdmissionPolicy?
Not for simple field checks: required labels, banned tags, replica caps. ValidatingAdmissionPolicy is stable since Kubernetes 1.30 and runs inside the API server. You still want an engine for mutation beyond MAP, generating resources, image signature verification, background audit reports of existing objects, and testing policy in CI before it reaches the cluster.
The call we'd make
New cluster, Kubernetes-only team: Kyverno 1.19 with CEL policy types, and let it generate native VAPs for the simple rules. Existing Rego estate: stay on Gatekeeper and write new templates with K8sNativeValidation CEL. Either way, start with the controls in our Kubernetes security guide, then add signature checks once you sign images with cosign. The caveat: whichever engine you choose, decide its fail mode on purpose and write it down.
Sources
Cite this article
Kiril Urbonas (2026). Kyverno vs OPA Gatekeeper in 2026: CEL, Rego, or Native Admission Policy. DevOpsNess. https://www.devopsness.com/blog/kyverno-vs-opa-gatekeeper
[Kyverno vs OPA Gatekeeper in 2026: CEL, Rego, or Native Admission Policy](https://www.devopsness.com/blog/kyverno-vs-opa-gatekeeper) (Kiril Urbonas, DevOpsNess, 2026-10-31)
More from DevOps
Explore more articles in this category
15 SRE Interview Questions With Answers, From SLOs to On-Call
SRE interviews test numbers and judgment. Fifteen questions on SLIs, error budgets, incident command, toil, capacity, on-call and postmortems, plus a systems-design scenario, each with the answer that scores and the one that loses the room.
Sign Container Images With Cosign: Keyless GitHub Actions Workflow, SBOM and In-Cluster Checks
One workflow that builds, signs keylessly with GitHub OIDC, attaches an SBOM and SLSA provenance, plus the Kyverno policy that refuses anything unsigned. Copy it, change three names, ship it.
Fix Prometheus High Cardinality: 6 Commands to Find and Drop Bad Series
When Prometheus runs out of memory, one label is almost always to blame. Six commands to find it, the relabel rules that drop it, and the scrape limits that stop it coming back.
You might have missed
Evergreen posts worth revisiting.