# Kyverno vs OPA Gatekeeper in 2026: CEL, Rego, or Native Admission Policy

URL: https://www.devopsness.com/blog/kyverno-vs-opa-gatekeeper
Author: Kiril Urbonas
Published: 2026-10-31
Updated: 2026-10-31
Categories: DevOps
Tags: Kubernetes, Security, DevSecOps, Policy, Supply Chain

Summary: 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.

---
# Kyverno vs OPA Gatekeeper in 2026: CEL, Rego, or Native Admission Policy

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]
> 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:

```yaml
# 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:

```yaml
# 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:

```rego
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:

```yaml
# 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](/blog/kubernetes-security-best-practices), then add signature checks once you [sign images with cosign](/blog/sign-container-images-cosign). The caveat: whichever engine you choose, decide its fail mode on purpose and write it down.

## Sources

- [Kyverno docs: Policy types overview and deprecation schedule](https://kyverno.io/docs/policy-types/overview/)
- [Gatekeeper docs: Failing closed](https://open-policy-agent.github.io/gatekeeper/website/docs/failing-closed/)
- [Gatekeeper docs: Integration with Validating Admission Policy](https://open-policy-agent.github.io/gatekeeper/website/docs/validating-admission-policy/)
- [Kubernetes docs: Validating Admission Policy](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/)

