Verdict summary
| Criterion | ValidatingAdmissionPolicy (CEL) | Kyverno | OPA Gatekeeper |
|---|---|---|---|
| Policy language | CEL | YAML patterns, CEL, JMESPath | Rego, optional CEL |
| Learning curve | Low | Low | High |
| Mutation | No | Full | Partial (Assign, AssignMetadata, ModifySet) |
| Resource generation | No | Yes | No |
| Image verification | No | Yes (Cosign, Notary, in-toto) | No (needs external provider) |
| Referential constraints | No | Partial (context.apiCall) |
Yes (data.inventory) |
| External data | No | Partial (K8s API calls) | Yes (arbitrary HTTP providers) |
| Audit of existing resources | No | Yes (PolicyReports) | Yes (constraint .status) |
| Policy exceptions | Binding selectors only | PolicyException CRD | Constraint match exclusions only |
| CLI testing | None | kyverno test, Chainsaw |
gator test, gator verify |
| CNCF maturity | Built-in (K8s 1.30+) | Graduated (March 2026) | Graduated (OPA, February 2021) |
| Operational overhead | Zero | Low (1-4 controllers) | Medium (webhook + audit pods) |
Short verdict. For teams that write Kubernetes YAML daily and need mutation, resource generation, or image verification: Kyverno. For organizations that already use OPA across multiple systems or need referential constraints and external data providers: Gatekeeper. For simple field validation with zero operational overhead on K8s 1.30+: ValidatingAdmissionPolicy.
Table of contents
- Verdict summary
- How admission controllers work
- ValidatingAdmissionPolicy: the built-in option
- Kyverno: YAML-native policy engine
- OPA Gatekeeper: Rego-powered policy engine
- Same policy, three ways
- Deep dive per criterion
- Operational concerns for both engines
- When to choose each
- Recommendation
How admission controllers work
The Kubernetes API server processes every mutating request (create, update, delete) through a two-phase admission pipeline that fires after authentication and RBAC authorization. The mutating phase runs first: controllers modify the incoming object in serial. Then the validating phase runs: controllers approve or reject. If any validating controller rejects, the request fails.
Both Kyverno and OPA Gatekeeper register themselves as dynamic admission webhooks (MutatingWebhookConfiguration and ValidatingWebhookConfiguration). They run as in-cluster services that receive admission review requests over HTTPS. ValidatingAdmissionPolicy skips the webhook entirely and runs CEL expressions in-process inside kube-apiserver.
Read operations (get, list, watch) never trigger admission controllers. Only mutating operations do.
ValidatingAdmissionPolicy: the built-in option
ValidatingAdmissionPolicy (VAP) reached GA in Kubernetes 1.30 (April 2024). It requires no installation, no running services, and adds zero latency beyond CEL expression evaluation inside the API server process.
A VAP setup uses three resources:
ValidatingAdmissionPolicydefines the CEL validation logic.ValidatingAdmissionPolicyBindingscopes the policy to namespaces or resources and sets the action:Deny,Warn, orAudit.- Param resource (optional) provides runtime parameters via a ConfigMap or custom CRD.
CEL gives you access to the submitted object, the previous version via oldObject, and the request metadata. It supports .all(), .exists(), .filter(), .has(), optional chaining (?.), and regex operations. The .status.typeChecking field even catches typos in field names at policy creation time.
What VAP cannot do: mutation, resource generation, image signature verification, referential constraints (checking other cluster resources), external data queries, and structured exception management. If you need any of those, you need Kyverno or Gatekeeper.
Kyverno: YAML-native policy engine
Kyverno (Greek for "govern") writes policies as Kubernetes custom resources in YAML. No separate language required. It became a CNCF Graduated project on March 16, 2026.
Kyverno deploys up to four separable controllers: the admission controller (required), a background controller for async generation and mutation, a reports controller for PolicyReport CRDs, and a cleanup controller for time-based deletion. In a minimal setup, only the admission controller runs.
Policy capabilities span five areas:
- Validate: block or audit resources that violate rules. Supports YAML pattern matching (legacy API) and CEL expressions (new
policies.kyverno.ioAPI, Kyverno 1.14+). - Mutate: modify resources with strategic merge patches, JSON Patch (RFC 6902), and foreach loops. Set security context defaults, inject labels, add sidecars.
- Generate: create new resources as side effects. Auto-create a default-deny NetworkPolicy per namespace, clone secrets across namespaces, provision RoleBindings.
- Image verification: verify Cosign signatures, Notary signatures, and in-toto/SLSA attestations before admission. Kyverno mutates image references to include verified digests, preventing tag drift.
- Exceptions: a namespaced PolicyException CRD lets specific resources bypass named policies. Disabled by default; must be explicitly enabled and scoped to allowed namespaces.
The Kyverno CLI (kyverno apply, kyverno test) validates policies against local YAML without a cluster. Chainsaw adds declarative end-to-end testing against a live cluster.
OPA Gatekeeper: Rego-powered policy engine
OPA Gatekeeper builds on Open Policy Agent, a CNCF Graduated project since February 2021. Its primary policy language is Rego, a purpose-built declarative language for policy logic.
Gatekeeper's policy model uses two CRD layers:
- ConstraintTemplate: defines a reusable policy class with Rego logic and a typed parameter schema (
openAPIV3Schema). Each template creates its own CRD kind. - Constraint: an instance of a template, parameterized and scoped to specific resource kinds and namespaces.
This two-step model is more verbose than Kyverno's single-resource approach but gives you API-server-level parameter validation. The gatekeeper-library provides pre-built templates for common scenarios.
Key capabilities beyond validation:
- Mutation (stable since v3.10):
AssignMetadatafor labels/annotations,Assignfor arbitrary fields,ModifySetfor list operations,AssignImagefor image components. More limited than Kyverno's mutation:AssignMetadatacan only add labels, not modify existing ones. - External data providers (v3.11+): call out to in-cluster HTTP services for image scanning, compliance checks, or identity lookups. Neither VAP nor Kyverno offers this.
- Referential constraints: Rego can query
data.inventoryto check existing cluster resources. "Deny this Ingress if another Ingress already claims this hostname" is a straightforward Rego rule. - VAP integration: Gatekeeper can generate native
ValidatingAdmissionPolicyresources from templates that use theK8sNativeValidationCEL engine, giving you in-process enforcement alongside the webhook.
The gator CLI (gator test, gator verify) handles offline testing. Enforcement actions per constraint: deny (default), warn, or dryrun.
Same policy, three ways
Requiring an app label on every pod is the "Hello World" of Kubernetes policy. Here is how each approach handles it.
ValidatingAdmissionPolicy (CEL):
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-app-label
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels) && has(object.metadata.labels.app)"
message: "Pod must have an 'app' label"
Seven lines of spec. No running service needed.
Kyverno (ClusterPolicy):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-app-label
spec:
validationFailureAction: Enforce
rules:
- name: check-app-label
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Pod must have an 'app' label"
pattern:
metadata:
labels:
app: "?*"
The "?*" pattern means "any non-empty string." Readable without knowing a policy language.
OPA Gatekeeper (ConstraintTemplate + Constraint):
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])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: pods-must-have-app
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels: ["app"]
The two-resource model is more verbose, but the typed parameters schema means the API server validates constraint parameters at creation time. And the template is reusable: a second constraint can require different labels on Deployments without writing new Rego.
Deep dive per criterion
Mutation
ValidatingAdmissionPolicy cannot mutate. Full stop.
Kyverno offers the richest mutation model: strategic merge patches add fields without overwriting existing ones, JSON Patch (RFC 6902) targets precise paths, and foreach loops iterate over containers. The conditional +(field) syntax adds a field only if it is absent, making mutations idempotent. Setting securityContext.readOnlyRootFilesystem: true on every container, injecting an Istio sidecar label on namespaces, pinning image tags to digests after verification: these are all single-rule operations.
Gatekeeper's mutation (stable since v3.10) works through four dedicated CRDs: AssignMetadata, Assign, ModifySet, AssignImage. It covers most mutation use cases but with constraints. AssignMetadata can only add labels and annotations, not modify existing ones. Multiple mutations to the same object apply alphabetically by mutator name, which requires careful naming.
Winner for mutation: Kyverno, by a wide margin.
Resource generation
Only Kyverno supports this. Generate rules create new resources as side effects of admission or background scanning. The canonical example: auto-create a default-deny NetworkPolicy in every new namespace. With synchronize: true, the generated resource stays in sync with the policy template. With generateExisting: true, Kyverno creates the resource in already-existing namespaces when the policy is first deployed.
Neither VAP nor Gatekeeper can generate resources. If you need this without Kyverno, you are writing a custom controller.
Image verification and supply chain security
Kyverno verifies Sigstore Cosign signatures, Notary signatures, and in-toto attestations (including SLSA provenance) directly in admission. When a verifyImages rule passes, Kyverno mutates the image reference to include the verified digest (nginx:1.25 becomes nginx@sha256:abc...), preventing tag-based drift after verification.
Gatekeeper has no built-in image verification. You can achieve something similar with an external data provider that calls a signing verification service, but you are building and maintaining that service yourself.
VAP has no image verification capability.
Referential constraints
Referential constraints check the submitted resource against other existing resources in the cluster. "Deny this Ingress if another Ingress already claims this hostname." "Deny this PVC if the referenced StorageClass does not exist."
Gatekeeper handles this natively through data.inventory in Rego, which exposes a cache of cluster resources. This is Gatekeeper's strongest differentiator.
Kyverno can achieve referential checks via context.apiCall calls to the Kubernetes API, but this is a runtime API call per admission request, not a cached lookup. It works, but it is less efficient and less ergonomic than Gatekeeper's data.inventory.
VAP has no referential constraint support at all.
External data
Gatekeeper's external data providers (v3.11+) allow Rego policies to call arbitrary in-cluster HTTP services. Image vulnerability scanning, compliance system lookups, tag-to-digest resolution: the HTTP endpoint receives a list of keys and returns structured results. TLS 1.3+ is required, mTLS is supported, and responses can be cached (v3.13+).
Kyverno's context.apiCall is limited to the Kubernetes API server. It cannot call arbitrary external HTTP endpoints.
VAP cannot make any external calls.
Policy exceptions
Kyverno provides a dedicated PolicyException CRD that exempts specific resources from named policies and rules. Exceptions are namespaced, disabled by default, and must be scoped to allowed namespaces via controller flags. This is a structured exception workflow.
Gatekeeper uses match exclusions on constraints (namespace selectors, label selectors). There is no dedicated exception resource. Broader exemptions require modifying the constraint itself.
VAP uses binding selectors. Same limitation as Gatekeeper: no dedicated exception mechanism.
Policy language and learning curve
VAP uses CEL. If you have written CEL expressions for Kubernetes (or Google Cloud IAM), you already know the syntax.
Kyverno's legacy API uses YAML pattern matching and JMESPath. Its new API (policies.kyverno.io, Kyverno 1.14+) uses CEL. Either way, the policies look like Kubernetes resources. A platform engineer who writes YAML daily can author Kyverno policies without learning a new language.
Gatekeeper requires Rego. Rego is powerful: set operations, arbitrary computation, referential lookups. But it is a distinct language with its own evaluation model. Teams need dedicated learning time. The tradeoff: if your organization already uses OPA for microservice authorization, API gateway policies, or CI/CD checks, Rego is a shared language across the stack.
Audit of existing resources
Both Kyverno and Gatekeeper audit existing resources that predate policy deployment. Kyverno's background controller generates PolicyReport CRDs. Gatekeeper's audit controller writes violations to each constraint's .status.violations field.
VAP has no audit capability. It only evaluates resources at admission time.
Testing in CI
Kyverno: kyverno test runs policy unit tests with expected pass/fail outcomes. Chainsaw provides end-to-end testing against a live cluster.
Gatekeeper: gator test validates resources against templates and constraints offline. gator verify runs suite-based tests.
VAP: no official CLI testing tool.
Both Kyverno and Gatekeeper CLIs can be integrated into GitHub Actions, GitLab CI, or Jenkins. Policies should be tested against manifests in CI before they reach any cluster.
Operational concerns for both engines
Both engines register admission webhooks, which means a failing policy engine can block cluster operations. These operational practices are non-negotiable for production:
Failure policy. failurePolicy: Fail means an unreachable webhook rejects all matching API requests. Both engines default to this for validation webhooks. It ensures compliance, but it can lock out the entire cluster if the policy engine is down. Run with high availability or accept the risk.
Namespace exclusions. Exclude critical system namespaces from webhook rules: kube-system, kube-public, kube-node-lease, and the policy engine's own namespace (kyverno or gatekeeper-system). Without this, a failing engine cannot restart itself because the API server blocks its own pod creation. Kyverno 1.12+ excludes kube-system by default on EKS.
High availability. Kyverno: production installs should use --replicas=3 for the admission controller. Gatekeeper: deploy multiple webhook replicas with a PodDisruptionBudget.
Timeouts. Keep webhook timeoutSeconds low (1-5 seconds). Long timeouts cascade into cluster-wide API latency.
Gradual rollout. Both engines support non-blocking modes. Kyverno: set validationFailureAction: Audit. Gatekeeper: set enforcementAction: dryrun. Deploy new policies in audit/dryrun first, observe violations, then switch to enforcement.
When to choose each
Choose ValidatingAdmissionPolicy (CEL) when:
- Your minimum cluster version is Kubernetes 1.30+.
- You only need validation (no mutation, no generation, no image verification).
- Policies are simple field checks: label enforcement, naming conventions, replica limits.
- Zero operational overhead matters more than feature breadth.
Choose Kyverno when:
- Your team works in Kubernetes YAML daily and wants policies in the same language.
- You need mutation (security context defaults, label injection, sidecar injection).
- You need resource generation (default-deny NetworkPolicies, secret cloning, RoleBinding provisioning).
- You need image signature verification (Cosign, Notary, SLSA provenance).
- You want structured policy exceptions with namespace-scoped controls.
- You are migrating from PodSecurityPolicy. Kyverno has a documented PSP migration path. For the built-in PSP replacement, see my guide on Pod Security Standards.
Choose OPA Gatekeeper when:
- Your organization already uses OPA for policy across multiple systems (microservices, API gateways, CI/CD) and Rego is a shared language.
- You need referential constraints that check resources against cached cluster state.
- You need external data providers to call arbitrary HTTP services at admission time.
- You want deeply typed parameter schemas (the
openAPIV3Schemaon ConstraintTemplates).
Hybrid approach. Some organizations run Kyverno for Kubernetes-native concerns (mutation, generation, image verification) and OPA for cross-stack policy. This works but doubles the operational surface.
Recommendation
For the majority case, a platform team adopting policy enforcement for the first time: start with Kyverno. The learning curve is the lowest. YAML-native policies remove the barrier of a new language. Mutation, generation, and image verification cover the features teams actually need in practice. It graduated as a CNCF project in March 2026, which settles the maturity question.
If your organization already has OPA and Rego expertise across multiple systems, Gatekeeper is the natural choice. The investment in Rego pays off through a unified policy language from API gateway to Kubernetes admission to CI/CD pipeline. Referential constraints and external data providers are capabilities Kyverno cannot match.
For simple validation-only use cases on Kubernetes 1.30+, start with ValidatingAdmissionPolicy. You can always add Kyverno or Gatekeeper later when you need mutation or generation. Both engines can generate VAP resources from their own policy definitions, so migrating policies forward is a managed process rather than a rewrite.