LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Kyverno vs OPA Gatekeeper

kyvernoopagatekeeperkubernetespolicy-as-codesecurity

Every Kubernetes cluster is a shared surface. Left to its own defaults, it will happily schedule containers running as root, pull images from any registry on the internet, spin up namespaces with no resource quotas, and generally do whatever the submitting developer asked — because its job is to run workloads, not second-guess them. That is exactly right for a single-team cluster where everyone is trusted. It is a serious problem everywhere else: multi-tenant platforms, production environments with compliance requirements, shared developer clusters where a careless kubectl apply can destabilize a neighbor.

The answer is policy as code: machine-enforced rules that intercept API server requests before they land and either reject them, rewrite them, or both. Two projects have dominated this space in Kubernetes: Kyverno, which graduated within the CNCF in March 2026, and OPA Gatekeeper, which brings the Open Policy Agent engine and its Rego language to the admission layer. A third contender, Kubernetes’ own ValidatingAdmissionPolicy (VAP) with its CEL engine, went GA in 1.30 and gained a mutating counterpart (MutatingAdmissionPolicy) in 1.36. Understanding all three — and which one actually belongs in your cluster — is the subject of this post.


How admission webhooks work

Before comparing tools, the plumbing matters. The Kubernetes API server processes every create/update/delete request through an ordered chain of admission controllers before the object is written to etcd. Two phases are relevant here:

   kubectl apply ...
          │
          ▼
   ┌─────────────────────────────────────────────────────────┐
   │              kube-apiserver                             │
   │                                                         │
   │  1. Authentication & Authorization                      │
   │  2. Mutating Admission (webhooks + MutatingAdmission-   │
   │     Policy) — can modify the object                     │
   │  3. Object schema validation                            │
   │  4. Validating Admission (webhooks + Validating-        │
   │     AdmissionPolicy) — approve or deny, no mutation     │
   └──────────────────────┬──────────────────────────────────┘
                          │ write to etcd
                          ▼
                    object persisted

A MutatingAdmissionWebhook fires first and can rewrite the incoming object — adding a label, injecting a sidecar, defaulting a missing field. A ValidatingAdmissionWebhook fires later with the final (possibly mutated) object and can only approve or deny. Both Kyverno and Gatekeeper register themselves as these webhooks; the API server calls them over HTTPS for each matching request. This means both tools add network round-trip latency to every relevant API call, which is why webhook failure policy and timeouts matter operationally.

The built-in CEL admission policies (ValidatingAdmissionPolicy, MutatingAdmissionPolicy) bypass the webhook mechanism entirely — they run in-process inside the API server. That is their defining advantage and it sets the stage for the threat-to-both-tools question we will return to later.


Kyverno: policy is just YAML

Kyverno was built from day one as a Kubernetes-native policy engine. Its defining thesis: the people writing admission policies are platform engineers and cluster operators who already know Kubernetes YAML deeply — do not make them learn a new language. Every Kyverno policy is itself a Kubernetes custom resource, expressed in the same YAML vocabulary as a Deployment or NetworkPolicy.

The four capabilities

Kyverno policies operate in four modes, all expressed within the same ClusterPolicy or Policy resource:

Validate — reject requests that violate a rule. The most common use: require labels, prohibit latest tags, enforce resource limits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-container-resources
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Resource limits are required for all containers."
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    memory: "?*"
                    cpu: "?*"

Mutate — rewrite objects before they are persisted. Add a default securityContext, inject an annotation, append a toleration. Kyverno mutation is GA, lives in the same policy resource as validation, and is the capability teams most frequently cite as the reason they chose Kyverno over Gatekeeper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-security-context
spec:
  rules:
    - name: set-run-as-non-root
      match:
        any:
          - resources:
              kinds: [Pod]
      mutate:
        patchStrategicMerge:
          spec:
            securityContext:
              runAsNonRoot: true
              seccompProfile:
                type: RuntimeDefault

Generate — create new resources in response to other events. The canonical example: generate a default NetworkPolicy and ResourceQuota every time a new namespace is created, so it is impossible to stand up a namespace that is wide open by default.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-network-policy
spec:
  rules:
    - name: default-deny-all
      match:
        any:
          - resources:
              kinds: [Namespace]
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-all
        namespace: "{{request.object.metadata.name}}"
        data:
          spec:
            podSelector: {}
            policyTypes: [Ingress, Egress]

verifyImages — enforce supply-chain integrity by verifying OCI image signatures (cosign) and attestations before a pod is admitted. This is Kyverno’s most differentiated capability; neither Gatekeeper nor the built-in CEL policies have an equivalent. See the supply chain security and container image hardening posts for the broader context this fits into.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences: ["registry.example.com/*"]
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQY...
                      -----END PUBLIC KEY-----

Policy reports

Kyverno implements the PolicyReport and ClusterPolicyReport CRDs from the Kubernetes Policy Working Group. These reports surface audit results — not just blocking-mode violations — as standard Kubernetes objects, queryable with kubectl and scraped by Prometheus. Flipping a policy from Audit to Enforce is a single field change; the report history tells you how many violations exist before you make things blocking.


OPA Gatekeeper: Rego, ConstraintTemplates, and reach beyond Kubernetes

Open Policy Agent is a general-purpose policy engine that Gatekeeper wires into the Kubernetes admission webhook layer. Where Kyverno is Kubernetes-specific, OPA is domain-agnostic — the same Rego policy language enforces rules in Envoy, Terraform, Kafka, CI pipelines, and IAM systems. Gatekeeper is the Kubernetes-shaped adapter.

ConstraintTemplates and Constraints

Gatekeeper splits policy into two layers. A ConstraintTemplate defines the policy logic in Rego and registers a new CRD kind. A Constraint instantiates that template with specific parameters. The separation is powerful: write Rego once, stamp out constraint instances across teams with different parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Missing required labels: %v", [missing])
        }

Then instantiate it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: pods-must-have-team
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    labels: ["team", "env"]

Rego: the learning curve is real, and so is the payoff

Rego is a declarative query language descended from Datalog. It is not hard in the way C++ is hard — it is unfamiliar in the way any new paradigm is unfamiliar. The mental shift: you describe what is true about a violation rather than how to check for one. Once the idiom clicks, Rego is genuinely expressive: recursive rules, set operations, complex cross-resource logic, and the ability to pull in external data via OPA’s bundle distribution. That expressive power is the reason enterprises with complex compliance requirements — regulatory mandates that require multi-document logic, or organizations that already enforce policy in Envoy and want to unify the language — continue to choose Gatekeeper over Kyverno.

Gatekeeper’s mutation support exists as a beta feature via Assign and AssignMetadata resources. It works, but the developer experience is markedly less polished than Kyverno’s: mutation lives in separate resource types with separate syntax, and as of v3.22 it is still not at the same maturity level as its validation story.

Gatekeeper’s audit mode continuously scans existing cluster objects against all active constraints and surfaces violations through the constraint’s .status.violations field. This matters: policies enforced at admission only catch new objects; audit catches drift in resources that pre-dated the policy or slipped through during a webhook outage.


Head-to-head

Dimension Kyverno 1.18 OPA Gatekeeper v3.22
Policy language YAML (with CEL and JMESPath expressions) Rego (purpose-built policy language)
Learning curve Low — fluent in an afternoon for K8s practitioners Moderate — Rego requires dedicated learning
Kubernetes-nativeness Purpose-built for K8s; policies are native CRDs K8s adapter on top of a general-purpose engine
Cross-platform reach Kubernetes only OPA unifies Envoy, Terraform, CI, IAM, and K8s
Validation GA, Enforce / Audit modes GA, deny / warn / dryrun enforcement actions
Mutation GA; same policy resource as validation Beta; separate Assign/AssignMetadata resources
Resource generation GA; unique to Kyverno among the three options Not supported
Image verification GA; cosign signatures and attestations Not supported natively
Policy reporting Native PolicyReport / ClusterPolicyReport CRDs Violations in .status.violations on each Constraint
Audit / drift detection Yes, via background scan and PolicyReport Yes, via continuous audit mode
Performance Webhook-based; single controller pod; lightweight footprint Webhook-based; multi-pod architecture; heavier RAM footprint
CNCF status Graduated (March 2026) Graduated (via OPA project)
Policy library Kyverno Policies repo; 200+ community policies OPA Library / Gatekeeper Policy Library
External data Limited (ConfigMaps, API calls in CEL rules) Rich — OPA bundle distribution, external data cache
Mutation ergonomics Excellent; strategic merge patch or RFC 6902 JSON patch Workable but verbose and separate from validation

The elephant in the room: ValidatingAdmissionPolicy and CEL

Kubernetes 1.30 made ValidatingAdmissionPolicy (VAP) generally available. Kubernetes 1.36, released April 2026, brought MutatingAdmissionPolicy (MAP) to GA as well. Both use the Common Expression Language (CEL) — the same runtime that powers Kubernetes’ built-in API validation — and both run in-process inside the API server, not through an external webhook.

That last point is significant. VAP and MAP impose zero webhook latency, have no availability dependency on an external deployment, and require no additional cluster components. For straightforward validation rules — “no latest tags,” “resource limits are required,” “privileged pods are forbidden” — they are genuinely competitive.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: no-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') && c.image.contains(':'))
      message: "Container images must specify a tag other than 'latest'."

Does this make Kyverno and Gatekeeper obsolete? Not yet, and the honest answer is more nuanced than either camp’s marketing suggests:

  • VAP/MAP cannot generate resources. The generate capability — creating a NetworkPolicy when a namespace appears — has no native analog.
  • VAP/MAP cannot verify image signatures. The cosign/supply-chain use case still belongs entirely to Kyverno.
  • VAP does not produce PolicyReports. Audit results require integrating a separate controller.
  • MAP mutation is newer and less battle-tested than Kyverno’s GA mutation.
  • Gatekeeper’s Rego expressiveness — external data, recursive rules, cross-resource logic — is well beyond what CEL currently offers.

The realistic 2026 picture: VAP/MAP handles the simple, ubiquitous rules (no latest tags, require limits, no host networking) with no additional tooling. Kyverno or Gatekeeper handles the rest. Kyverno 1.17 explicitly embraced this by aligning its CEL-based policy types with VAP syntax, so a rule written for VAP translates with minimal effort to Kyverno, and Kyverno adds the generate/verifyImages capabilities on top. Gatekeeper v3.22 enables sync-vap-enforcement-scope by default to integrate cleanly with VAP in the same cluster. The direction is coexistence, not displacement.


Policy reporting: the underrated capability

Both tools eventually surface violations as queryable objects, but the model differs. Kyverno’s PolicyReport CRDs are a CNCF standard, implemented by multiple tools, and work with dashboards like Policy Reporter (a Kyverno-ecosystem project) out of the box. A ClusterPolicyReport aggregates violations across the whole cluster; a namespaced PolicyReport scopes to a namespace.

Gatekeeper’s violations live on the Constraint object itself — in .status.violations — which is convenient for kubectl describe but harder to aggregate or feed into a dashboard without custom scripting. The Gatekeeper team has added experimental support for the PolicyReport CRD, but adoption has been slower than Kyverno’s native integration.

For clusters where compliance teams need audit trails and violation dashboards, the PolicyReport ecosystem is a meaningful Kyverno advantage.


Choose Kyverno if…

  • Your team is strong in Kubernetes YAML but has no existing Rego investment. Kyverno policies are readable by anyone who can read a Deployment manifest. Onboarding a new platform engineer to Kyverno takes an afternoon; onboarding them to Rego takes longer.
  • You need mutation that is first-class and GA, not a beta add-on. The ability to rewrite objects — inject securityContexts, default annotations, add tolerations — in the same policy resource that validates them is a genuine ergonomic win.
  • You need resource generation: auto-creating NetworkPolicies, RoleBindings, or ResourceQuotas when namespaces appear is a pattern no other tool covers natively.
  • You need image signature verification as part of your admission control. If you are building a supply-chain security posture with cosign, Kyverno is the only option among the three that does this natively. Pair it with Trivy for image scanning and the container security hardening practices for a complete picture.
  • You are greenfield or migrating away from PodSecurityPolicy. Kyverno’s community policy library has drop-in replacements and the PSP-to-Kyverno migration is well-documented.

Choose OPA Gatekeeper if…

  • Your organization already uses OPA elsewhere — in Envoy for service-mesh authorization, in Terraform for infrastructure policy, in CI pipelines for code policy — and you want to unify on one language and one policy library. The ability to write Rego once and enforce it across every tier of the stack is a serious architectural advantage for large engineering organizations.
  • You have complex, multi-document policy logic: rules that must join data across multiple resources, reason about historical state, or implement compliance frameworks (PCI-DSS, HIPAA) that require decision trees too intricate for pattern-matching YAML.
  • You need external data integration via OPA’s bundle distribution: policies that consult a central policy store, or that make decisions based on data external to the Kubernetes object itself.
  • Your security team already speaks Rego and finds YAML policies too implicit. Rego’s explicit violation[{"msg": msg}] structure makes policy logic auditable in a way that can satisfy a compliance reviewer.

The verdict

In 2026, Kyverno has won the adoption contest: CNCF graduation in March, a thriving policy library, first-mover advantage on image verification and resource generation, and the ergonomic lead that comes from not requiring a new language. For most teams starting fresh — or migrating off PodSecurityPolicy — Kyverno is the right default. Its mutation and generate capabilities handle the workflows that pure validation cannot, and its PolicyReport integration makes audit straightforward.

Gatekeeper is not losing; it is winning with a different audience. Organizations that run OPA across their full stack, or that face compliance requirements complex enough to need Rego’s expressive power, have no reason to switch. The Rego investment pays back across Envoy, Terraform, and CI in ways Kyverno simply cannot match. Gatekeeper continues to earn its place at the table in those environments.

The built-in ValidatingAdmissionPolicy and MutatingAdmissionPolicy are real competition for simple rules and will reduce the justification for either tool over time — but today they lack generation, image verification, and mature reporting. The practical answer for most clusters is to use VAP for the two or three dead-simple rules you want enforced with zero dependencies, and let Kyverno or Gatekeeper own everything else.

The kubernetes-basics admission control fundamentals are the prerequisite; this post is where you choose your enforcement strategy on top of them. Get the policy engine right and cluster security stops being a social contract enforced by code review and starts being a property enforced by the API server on every request.


Sources

Comments