Back to blog
Security
IntermediateForPlatform EngineersSecurity EngineersSRE
9 min

OPA vs Kyverno in 2026: Kubernetes Ships Policy Now

Kubernetes now enforces policy in-process: ValidatingAdmissionPolicy went stable in 1.30, MutatingAdmissionPolicy in 1.36. So the real question is no longer OPA or Kyverno — it's what you still need a controller for.

opa vs kyvernokubernetes policy enginevalidatingadmissionpolicypolicy as codegatekeeper
Contents

Every comparison of OPA and Kyverno you’ll find was written for a world that no longer exists.

The old framing went: Rego is powerful but you have to learn it, Kyverno is YAML so it’s approachable, pick your poison. That framing died twice over. Kubernetes started enforcing policy itself, and Kyverno stopped being a YAML-only engine.

So before choosing between two controllers, the useful question is a different one: do you need a controller at all?

Kubernetes now enforces validation and mutation in-process, which changes what an external policy controller is actually for.

What Changed: Kubernetes Grew Its Own Policy Engine

Two features moved the floor.

ValidatingAdmissionPolicy went stable in Kubernetes 1.30. It’s a declarative, in-process alternative to validating admission webhooks, and it uses the Common Expression Language.

MutatingAdmissionPolicy went stable in Kubernetes 1.36, enabled by default. Same idea for changing objects rather than rejecting them. It took the long road — alpha in 1.30, beta in 1.34, GA in 1.36 — so if you’re on an older cluster, check before you plan around it.

A policy is three pieces: the policy itself, an optional parameter resource, and a binding that ties them together and scopes them. Here’s the whole thing:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 5"

No controller. No webhook. No TLS certificate to rotate. No extra network hop on every API call.

That last part matters more than it sounds, and I’ll come back to it.

The built-in policies are also more capable than the toy example suggests. You get variables for composing expressions, matchConditions for fine-grained filtering, messageExpression for useful rejection messages, auditAnnotations, and type checking — the API server parses your expression against the matched schema and reports mistakes in status.typeChecking before they ever bite you:

status:
typeChecking:
expressionWarnings:
- fieldRef: spec.validations[0].expression
warning: |-
apps/v1, Kind=Deployment: ERROR: <input>:1:7: undefined field 'replicas'

And validationActions let you run a policy as Deny, Warn, or Audit — which means you can roll a rule out in observation mode first. That’s the same discipline I argued for with runtime detection and alert fatigue: watch before you enforce.

The Second Thing That Changed: Kyverno Speaks CEL

The “YAML versus Rego” argument assumed Kyverno was a YAML-and-JMESPath engine. It isn’t any more.

Kyverno adopted CEL, and the project is explicit about why — Kubernetes invested heavily in CEL, so using it “reduces the cognitive load for platform teams as there is one less thing to learn.”

The practical consequence: a CEL expression in a built-in ValidatingAdmissionPolicy and a CEL expression in a Kyverno ValidatingPolicy look like close relatives. Language is no longer the axis that separates these tools.

This also came with a full API redesign. The current policy types:

The Second Thing That Changed: Kyverno Speaks CEL
Type What it does Status
ValidatingPolicy Validate resources or JSON payloads Stable (v1.18)
MutatingPolicy Mutate new or existing resources Stable (v1.18)
GeneratingPolicy Create or clone resources from triggers Stable (v1.18)
DeletingPolicy Delete matching resources on a schedule Stable (v1.18)
ImageValidatingPolicy Verify image signatures and attestations Stable (v1.18)
ClusterPolicy Legacy all-in-one type Deprecated (v1.18)
CleanupPolicy Legacy scheduled deletion Deprecated (v1.18)

If you’re running Kyverno today, that bottom section is not trivia — it’s a migration you have to schedule. More on the deadline below.

Kyverno also reached CNCF Graduated status on 16 March 2026, having been accepted in November 2020 and moved to Incubating in July 2022.

So What Do You Still Need a Controller For?

Here’s the honest list. Built-in policies handle admission-time validation and mutation. They do not do these things:

So What Do You Still Need a Controller For?
Capability Built-in Gatekeeper Kyverno
Validate at admission yes yes yes
Mutate at admission yes (1.36+) yes yes
Generate / clone resources no no yes
Verify image signatures no no yes
Audit resources already in the cluster no yes yes
Policy reports no yes yes
Scheduled deletion / cleanup no no yes
Policy outside Kubernetes no via OPA yes (JSON, Terraform)

Capability matrix: built-in policies cover admission, but generation, image verification and reporting still need a controller.

Three of those rows are the usual reason teams still install something.

Generation. When a namespace is created, you want a default NetworkPolicy, a pull secret copied in, a resource quota, a LimitRange. Admission control can only reject or edit what someone submitted; it can’t create the thing they forgot. Kyverno’s GeneratingPolicy can, and can keep the generated copy in sync with its source.

Image verification. Checking that an image is signed and carries the attestations you expect is supply-chain enforcement, and it needs to talk to a registry and a transparency log at admission time. That’s ImageValidatingPolicy territory. It’s also the missing enforcement half of the argument I made about why an SBOM alone won’t stop the next Log4j — an attestation nobody verifies is a document, not a control.

Background scanning. Admission only sees new and changed objects. Everything that was already running when you wrote the policy is invisible to it. Gatekeeper’s audit and Kyverno’s reports both answer “what’s already violating this?” — which is the first question anyone asks when adopting a policy.

Where OPA Actually Wins

If you only read Kubernetes comparisons, OPA looks like the more awkward option: you write Rego, you deploy Gatekeeper, you manage ConstraintTemplate and Constraint pairs.

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: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels[_]
not input.review.object.metadata.labels[required]
msg := sprintf("missing required label: %v", [required])
}

That’s more ceremony than a CEL one-liner. The payoff isn’t inside the cluster.

OPA is not a Kubernetes tool. It’s a general-purpose policy engine — CNCF graduated — and Rego is one language you can point at microservice API authorisation, Terraform plans in CI, data filtering, and admission control. If your organisation already runs OPA for service authorisation, adding cluster policy to the same engine and the same review process is a genuine consolidation, not an extra tool.

Gatekeeper adds the Kubernetes-native parts on top: a parameterised policy library, constraints as CRDs, mutation via Assign, AssignMetadata, ModifySet and AssignImage, audit, and external data support.

The trade is real in both directions. One language everywhere, at the cost of a language nobody knows on day one.

How I’d Actually Decide

Four questions, in order.

1. Do the built-in policies cover the rule? If it’s “reject X” or “set Y by default”, write a ValidatingAdmissionPolicy or MutatingAdmissionPolicy and stop. You’ve added zero operational surface. This should be your default for things like required labels, banned capabilities, registry allow-lists and replica limits.

2. Do you need the cluster to create things, not just judge them? Then you need Kyverno. Generation has no built-in equivalent and no Gatekeeper equivalent.

3. Do you need supply-chain verification at admission? Kyverno’s ImageValidatingPolicy, or a dedicated tool. Neither the built-ins nor Gatekeeper do this.

4. Does policy need to live outside Kubernetes? Then OPA, and accept Rego as the cost of one engine across cluster, CI and services.

Most teams land on: built-in policies for the simple majority, Kyverno for the handful of rules that need generation, image verification and reports. That combination is boring and it works.

A decision path: start with built-in policies, add a controller only for the capabilities they lack.

The Operational Trap Nobody Mentions First

A webhook is not a feature you enable. It’s infrastructure in the request path.

Set failurePolicy: Fail and an unavailable policy controller blocks every operation it matches. The classic failure looks like this: the controller is down, the rule matches Pod creation, and now you cannot schedule pods — during the exact incident where you’re trying to scale out of trouble.

Set Ignore instead and you’ve quietly turned enforcement off. Your policies still exist, still look green in git, and enforce nothing.

There is no third option that avoids the choice. What you can do:

  • Exclude the namespaces that must never be blockedkube-system and your policy controller’s own namespace, at minimum. A controller whose webhook can block its own recreation is a genuinely bad afternoon.
  • Scope matchConstraints narrowly. A policy matching resources: ["*"] puts your controller in the path of everything, including leases and events. The built-in docs show exactly this pattern with matchConditions that exclude leases, node users and RBAC requests.
  • Run new rules as Warn or Audit first, then promote to Deny.
  • Remember mutation runs before validation, and that mutations can trigger re-evaluation. reinvocationPolicy: IfNeeded exists because one mutation can invalidate another’s assumptions.

This is the strongest practical argument for the built-ins: policy evaluated inside the API server cannot be unavailable separately from the API server. An entire class of outage disappears.

Webhook failure modes: Fail blocks the cluster, Ignore silently disables enforcement, built-in policies avoid the choice.

The Migration You Might Not Know You Owe

If you adopted Kyverno before 2025, your policies are almost certainly ClusterPolicy resources. The project has published the runway:

The Migration You Might Not Know You Owe
Version Date Status
v1.17 Jan 2026 Marked for deprecation
v1.18 Apr 2026 Critical fixes only
v1.19 Jul 2026 Critical fixes only
v1.20 Oct 2026 Planned for removal

The CEL-based types arrived in v1.14 (April 2025) and v1.15 (July 2025) and are stable as of v1.18, released 29 April 2026. So this isn’t a surprise — but it is a real piece of work that belongs on a roadmap rather than in an emergency upgrade.

One detail worth checking before you plan anything: Kyverno’s support matrix lags Kubernetes. v1.18 lists supported Kubernetes versions as v1.33 to v1.35 — which does not include 1.36, the release where MutatingAdmissionPolicy went GA. Other versions “may work, but are not tested”. Kyverno also gives roughly three months of patch support per minor release, so the upgrade treadmill is faster than many teams assume.

The upside: migrating moves you to CEL, which is the same language as the built-in policies. Some of what you migrate, you may find you can delete entirely and express natively instead.

What Good Looks Like

  • Simple admission rules live in built-in ValidatingAdmissionPolicy objects, with no controller involved
  • Every new rule ships as Warn or Audit first, and gets promoted deliberately
  • Exactly one policy controller is installed, and it’s there for named capabilities you can justify — generation, image verification, reports
  • kube-system and the controller’s own namespace are excluded from blocking webhooks
  • Someone can answer “what is currently violating this policy?” without writing a script
  • Policy lives in git and ships through the same review path as everything else, which is the platform engineering argument applied to guardrails

The Bottom Line

The interesting comparison in 2026 isn’t OPA against Kyverno. It’s built-in policy against everything else.

Kubernetes now validates and mutates natively, in-process, in CEL, with type checking and audit modes — and that covers the majority of what most clusters enforce, without putting a webhook in the request path.

What’s left is a genuine but narrower question. Kyverno if you need the cluster to generate resources, verify image signatures, or report on what’s already broken. OPA if policy has to reach beyond Kubernetes and one language everywhere is worth learning Rego for.

Start with what’s already in the cluster. Add a controller when you can name the capability you’re adding it for — and when the answer isn’t “because that’s what we’ve always installed.”

The same principle runs through secure-by-default GKE: the control that works is the one with the least machinery between the rule and its enforcement.

Frequently asked questions

Is OPA or Kyverno better for Kubernetes in 2026?

For Kubernetes-only policy, Kyverno is usually the faster path: policies are Kubernetes resources written in YAML and CEL, and it covers validation, mutation, generation, image verification and reporting in one tool. OPA with Gatekeeper is the better fit when policy has to extend beyond the cluster, because Rego is a general-purpose policy language you can also use for API authorisation, Terraform plan checks and CI gates. But in 2026 the first question should be neither: Kubernetes enforces validation and mutation natively now, so start by checking whether the built-in policy objects already cover your rules.

What is ValidatingAdmissionPolicy and does it replace OPA and Kyverno?

ValidatingAdmissionPolicy is Kubernetes' built-in, declarative alternative to validating admission webhooks. It went stable in Kubernetes 1.30, uses the Common Expression Language, and runs in-process inside the API server, so there is no external webhook to deploy, no TLS certificate to rotate and no extra network hop. MutatingAdmissionPolicy is the equivalent for mutations and became stable in 1.36, enabled by default. Together they replace a large share of simple admission rules, but they do not replace a full policy engine: they cannot generate or clone resources, verify container image signatures, scan resources that already exist in the cluster, or produce policy reports.

Does Kyverno still use YAML instead of Rego?

Kyverno never used Rego, and it no longer relies solely on JMESPath either. Kyverno originally used JMESPath for JSON processing, and since Kubernetes invested heavily in the Common Expression Language, Kyverno adopted CEL as well. Modern Kyverno policies are written in YAML with CEL expressions, which the project frames as deliberately reducing what platform teams have to learn, since CEL is already used across Kubernetes. Practically, this means a CEL expression you write for a built-in ValidatingAdmissionPolicy looks very similar inside a Kyverno ValidatingPolicy.

What is the difference between OPA and Gatekeeper?

OPA is the general-purpose policy engine and Rego is its language; it is a CNCF graduated project and is not specific to Kubernetes. Gatekeeper is the Kubernetes integration built on top of OPA: it runs as a validating and mutating webhook and exposes policies as Kubernetes custom resources. In Gatekeeper you write a ConstraintTemplate containing the Rego logic, then create Constraint resources that instantiate it with parameters and scope. Gatekeeper adds things plain OPA does not have in a cluster context, including a parameterised policy library, audit functionality that reports existing violations, mutation resources such as Assign and AssignMetadata, and external data support.

Do I need to migrate my Kyverno ClusterPolicy resources?

Yes, if you want to stay on supported APIs. Kyverno introduced CEL-based policy types in v1.14 and v1.15, and as of v1.18 the new ValidatingPolicy, MutatingPolicy, GeneratingPolicy, DeletingPolicy and ImageValidatingPolicy types are stable while the legacy ClusterPolicy and CleanupPolicy types are deprecated. The project published a schedule: critical fixes only through v1.18 and v1.19, with removal planned in v1.20 in October 2026. Treat that as a real deadline and budget migration work rather than discovering it during an upgrade. Also check the support matrix before planning: Kyverno v1.18 lists supported Kubernetes versions as v1.33 to v1.35, and offers roughly three months of patch support per minor release.

What are the operational risks of an admission webhook?

A webhook sits in the path of every matching API request, so it becomes infrastructure that can take the cluster down. With failurePolicy set to Fail, an unavailable webhook blocks the operations it matches, which can prevent pods from being created during exactly the incident when you need to scale. With Ignore, you silently lose enforcement instead. You also own TLS certificates and their rotation, controller availability and upgrade compatibility. Built-in policies avoid this entire category of failure because evaluation happens inside the API server, which is one of the strongest arguments for using them where they suffice.

From the community

Discussion on the Fediverse

Replies from Mastodon and Bluesky — straight from the open web, no tracking.

Loading replies …

ENDE