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?

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/v1kind: ValidatingAdmissionPolicymetadata: 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:
| 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:
| 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) |

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/v1kind: ConstraintTemplatemetadata: name: k8srequiredlabelsspec: 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.

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 blocked —
kube-systemand your policy controller’s own namespace, at minimum. A controller whose webhook can block its own recreation is a genuinely bad afternoon. - Scope
matchConstraintsnarrowly. A policy matchingresources: ["*"]puts your controller in the path of everything, including leases and events. The built-in docs show exactly this pattern withmatchConditionsthat exclude leases, node users and RBAC requests. - Run new rules as
WarnorAuditfirst, then promote toDeny. - Remember mutation runs before validation, and that mutations can trigger re-evaluation.
reinvocationPolicy: IfNeededexists 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.

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:
| 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
ValidatingAdmissionPolicyobjects, with no controller involved - Every new rule ships as
WarnorAuditfirst, 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-systemand 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.




From the community
Discussion on the Fediverse
Replies from Mastodon and Bluesky — straight from the open web, no tracking.
Loading replies …
No replies yet. Start the conversation:
Replies could not be loaded right now.