Kyverno¶
Who this page is for: in plain English, Kyverno is an admission-control policy engine - it inspects every object being created or changed in the cluster and can reject it, modify it, or generate companion resources, using rules written as ordinary Kubernetes YAML. This page is production depth, aimed at readers already comfortable with Pods, Services, and Deployments. New to Kubernetes? Start with the Beginner track.
Kyverno is a policy engine designed specifically for Kubernetes. Unlike OPA/Gatekeeper which uses the Rego language, Kyverno policies are written as Kubernetes YAML - using the same resource model you already know - and managed through the same GitOps workflows as your other manifests.
The key distinction from OPA: Kyverno is not a general-purpose policy language. That constraint is also its strength - policies are readable by anyone who knows Kubernetes, don't require learning Rego, and integrate naturally with tools that speak Kubernetes YAML (Helm, Argo CD, kustomize).
Kyverno reached CNCF Graduated status in March 2026, the same top-level maturity tier as OPA/Gatekeeper.
Architecture¶
flowchart TD
APIServer[Kubernetes API Server] --> |ValidatingWebhook| KyvernoAdmission[Kyverno\nadmission controller]
APIServer --> |MutatingWebhook| KyvernoAdmission
KyvernoAdmission --> |evaluate| Engine[Policy engine]
Engine --> |ClusterPolicy / Policy /\nValidatingPolicy / etc.| Rules[Validate / Mutate / Generate / VerifyImages / Delete]
subgraph Background controllers
Scanner[background-controller\ngenerate + mutate-existing +\nperiodic background scans]
Cleanup[cleanup-controller\nCleanupPolicy / DeletingPolicy]
Reporter[reports-controller\naggregates Admission/Background\nScan Reports into PolicyReport]
end
Engine --> Scanner
Engine --> Cleanup
Scanner --> Reporter
Reporter --> Reports[PolicyReport / ClusterPolicyReport]
Kyverno runs four sub-controllers. The admission controller handles real-time enforcement (validate, mutate, verifyImages, and PolicyException matching at admission time) and is the only one required for a minimal install. The background-controller processes generate and mutate-existing rules and runs the periodic background scan against existing resources. The cleanup-controller processes CleanupPolicy/ClusterCleanupPolicy (and their CEL-native successor, DeletingPolicy), managing the CronJobs that perform scheduled deletions. The reports-controller reconciles the intermediate Admission Reports and Background Scan Reports each admission/scan produces into the PolicyReport/ClusterPolicyReport objects you actually query.
Policy types¶
Kyverno has four rule types in a single policy:
| Rule type | What it does |
|---|---|
validate |
Deny admission if condition is not met |
mutate |
Modify the resource before admission |
generate |
Create, clone, or sync other resources when a trigger resource is created |
verifyImages |
Verify container image signatures (Cosign, Notary) |
ClusterPolicy is being phased out in favor of CEL-native policy types. Everything above - the kyverno.io/v1 ClusterPolicy/Policy CRDs and their validate/mutate/generate/verifyImages rule types - is Kyverno's original, JMESPath-based policy model, and it's on a deprecation path. Kyverno 1.17 marked ClusterPolicy, Policy, CleanupPolicy, and the legacy kyverno.io/v2 PolicyException as deprecated; as of the 1.19 release line they're in critical-fixes-only maintenance, with full removal planned for 1.20 - check the migration guide and release notes for the current schedule before you plan a migration. They still work today and the examples below remain accurate for existing deployments, but new policies should generally be written against the CEL-native types covered in CEL as a first-class policy type below. See Kyverno's migration guide for the field-by-field mapping.
Validation¶
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
annotations:
policies.kyverno.io/title: Require Resource Limits
policies.kyverno.io/severity: medium
policies.kyverno.io/description: >
All containers must declare CPU and memory limits to prevent noisy-neighbor problems.
spec:
validationFailureAction: Enforce # or Audit
background: true # run against existing resources in audit mode
rules:
- name: check-container-limits
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- kube-system
- monitoring
validate:
message: "CPU and memory limits are required for all containers."
pattern:
spec:
containers:
- name: "*"
resources:
limits:
cpu: "?*"
memory: "?*"
validationFailureAction: Enforce blocks admission. Audit logs violations in PolicyReport without blocking. Start with Audit for new policies.
?* matches any non-empty value. * matches any value including empty.
CEL-based validation (Kyverno 1.11+)¶
For complex logic, use CEL expressions instead of pattern matching, embedded inside a ClusterPolicy rule:
validate:
cel:
expressions:
- expression: >
object.spec.containers.all(c,
has(c.resources) &&
has(c.resources.limits) &&
has(c.resources.limits.memory)
)
message: "All containers must have memory limits"
CEL as a first-class policy type¶
Kyverno went further than embedding CEL inside JMESPath-style rules: it now ships a full family of CEL-native policy types under a new API group, policies.kyverno.io/v1 - ValidatingPolicy, MutatingPolicy, GeneratingPolicy, ImageValidatingPolicy, and DeletingPolicy, each the CEL-native counterpart to one of the legacy rule types (validate, mutate, generate, verifyImages, and CleanupPolicy, respectively). ValidatingPolicy and ImageValidatingPolicy landed first (Kyverno 1.14, April 2025); MutatingPolicy, GeneratingPolicy, and DeletingPolicy followed in 1.15 (July 2025); the whole family reached general availability in 1.18 (April 2026). As of 1.19 this is now the actively maintained policy model - the legacy ClusterPolicy-based rule types described elsewhere on this page are deprecated (see the note above).
ValidatingPolicy's structure closely mirrors the core Kubernetes ValidatingAdmissionPolicy API (the built-in, no-extra-controller admission mechanism that ships with Kubernetes itself) - Kyverno describes it as a superset of ValidatingAdmissionPolicy with added fields for background scanning, policy exceptions, and Kyverno's richer CEL function library:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: check-labels
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "'environment' in object.metadata.?labels.orValue([])"
message: "label 'environment' is required"
Why this matters in practice:
ValidatingAdmissionPolicy(core Kubernetes, GA since v1.30) runs inside the API server itself - no webhook round-trip, no extra controller to keep available, and it's a good fit for simple, high-frequency checks where Kyverno's full rule engine (mutation/generation/image verification) isn't needed. Kubernetes'MutatingAdmissionPolicyreached GA in v1.36 (April 2026), extending the same in-server CEL model to mutation.- Kyverno's CEL-native policy family gets you the same CEL expression language plus Kyverno's ecosystem -
PolicyReportintegration, policy exceptions,kyverno test, background scanning, and mutation/generation/image-verification in the same engine - while still being closer to native Kubernetes semantics than JMESPath pattern matching. - JMESPath-based
validate.pattern(the original mechanism, shown above) remains the easiest entry point conceptually for straightforward "this field must look like this" checks and is what most existing Kyverno policies already deployed in the wild still use - but it's the deprecated path for anything new, per the note above.
In practice: reach for plain ValidatingAdmissionPolicy/MutatingAdmissionPolicy if you only need validation or mutation and want to avoid running Kyverno's controllers at all; reach for Kyverno's CEL-native policy types for new work that needs mutation, generation, image verification, background scanning, or exceptions in the same engine.
Mutation¶
(The mutate rule type below is part of the deprecated ClusterPolicy model; its CEL-native counterpart is MutatingPolicy - see CEL as a first-class policy type.)
Mutations run before validation. Use them to inject defaults so that validation rules succeed for users who don't specify everything:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-security-context
spec:
rules:
- name: add-readonly-root
match:
any:
- resources:
kinds: [Pod]
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
securityContext:
+(readOnlyRootFilesystem): true # + means: set only if not present
+(allowPrivilegeEscalation): false
+(runAsNonRoot): true
+(field) is the "add if absent" operator - it sets the value only if the field doesn't already exist. This allows workloads to override defaults explicitly while enforcing them for workloads that don't specify a value.
PatchesJSON6902¶
For precise, surgical mutations:
mutate:
patchesJSON6902:
- path: /spec/template/spec/automountServiceAccountToken
op: add
value: false
Generation¶
(The generate rule type below is part of the deprecated ClusterPolicy model; its CEL-native counterpart is GeneratingPolicy - see CEL as a first-class policy type.)
Generate creates, clones, or syncs resources when a trigger resource is created:
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}}"
synchronize: true # keep in sync -- delete if policy is deleted
data:
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
When synchronize: true, Kyverno owns the generated resource - it will recreate it if deleted and update it if the policy changes. This is powerful for enforcing baseline resources (default network policies, resource quotas, LimitRanges) in every new namespace.
Clone from a source: copy a Secret or ConfigMap into every new namespace:
generate:
apiVersion: v1
kind: Secret
name: registry-credentials
namespace: "{{request.object.metadata.name}}"
clone:
namespace: kyverno
name: registry-credentials-template
synchronize: true
Sync vs one-time generation, and generateExisting¶
Three settings control the lifecycle of a generated resource, and mixing them up is a common source of surprise:
synchronize: true(shown above) means Kyverno owns the resource going forward. If someone edits or deletes it, Kyverno reverts the change or recreates it on the next reconcile, keeping the generated object permanently in sync with the policy'sdata/clonesource. Use this for baseline security posture you never want drifted - default-deny NetworkPolicies, mandatory ResourceQuotas.synchronize: false(or omitted) makes generation a one-time stamp: Kyverno creates the resource when the trigger fires, and after that it's a normal Kubernetes object the owner can freely edit or delete without Kyverno fighting them. Use this for starter/scaffold resources - a default ConfigMap a team is expected to customize.generateExisting: truecontrols whether the policy also applies retroactively to resources that already exist when the policy itself is created or updated. By default,generaterules only fire on new trigger resources going forward (e.g. new Namespaces created after the policy exists) - aClusterPolicygenerating NetworkPolicies from aNamespacetrigger won't backfill existing namespaces unlessgenerateExisting: trueis set. This is the generate-rule equivalent of Gatekeeper's audit/dryrun gap: without it, rolling out a new generate policy only protects namespaces created from that point forward, leaving every pre-existing namespace ungoverned until someone runs a one-off backfill.
Multi-resource generation: NetworkPolicy + ResourceQuota per namespace¶
A single trigger can drive multiple generate rules in one policy. This is the realistic version of "every new namespace gets a secure, resource-governed baseline" - one policy, one trigger (Namespace creation), two generated resources:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: namespace-baseline
spec:
generateExisting: true
rules:
- name: default-deny-network-policy
match:
any:
- resources:
kinds: [Namespace]
exclude:
any:
- resources:
namespaces: [kube-system, kube-public, kyverno]
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: default-deny-all
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
- name: default-resource-quota
match:
any:
- resources:
kinds: [Namespace]
exclude:
any:
- resources:
namespaces: [kube-system, kube-public, kyverno]
generate:
apiVersion: v1
kind: ResourceQuota
name: default-quota
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "100"
Both rules match the same trigger kind (Namespace) but generate independent resources - Kyverno evaluates each generate rule in the policy separately against the same admission event. With synchronize: true on both, deleting either the NetworkPolicy or the ResourceQuota out from under a namespace just causes Kyverno to recreate it, which is exactly the property you want for a security/governance baseline that shouldn't depend on every team remembering to keep it in place.
Image verification¶
(The verifyImages rule type below is part of the deprecated ClusterPolicy model; its CEL-native counterpart is ImageValidatingPolicy - see CEL as a first-class policy type.)
Kyverno can verify container image signatures using Cosign or Notary at admission time:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "registry.mycompany.com/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/my-app/.github/workflows/build.yaml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
mutateDigest: true # replace tag with digest after verification
required: true
mutateDigest: true replaces my-image:v1.2.0 with my-image@sha256:abc... after verifying the signature. This enforces immutability - the tag can't be swapped after deployment.
Keyless verification and why it's the modern default¶
Traditional signature verification needs a public key baked into the policy, which means a key management problem: where does the key live, who rotates it, what happens when someone leaves the team that controls it. Sigstore's keyless flow (Cosign + Fulcio + Rekor) sidesteps this entirely:
- At sign time, the CI pipeline authenticates to Fulcio (Sigstore's certificate authority) using its own OIDC identity - a GitHub Actions workflow token, a GitLab CI job token, or similar. Fulcio issues a short-lived signing certificate bound to that OIDC identity rather than to a long-lived private key.
- The image is signed with a key that exists only for the duration of that CI job, and the signature plus certificate are recorded in Rekor, Sigstore's public transparency log.
- At verification time (the
keylessblock in the example above), Kyverno checks that the signing certificate'ssubjectandissuermatch the expected workflow identity, and that the signature is present in Rekor - proving the image was signed by that specific CI workflow, at that specific commit, without either side ever managing a persistent key.
The subject field is the strongest part of this: it pins verification to a specific workflow file and branch (.github/workflows/build.yaml@refs/heads/main in the example above), so a signature produced by a different repo, a different workflow, or a feature branch won't satisfy the policy even if it's otherwise a valid Sigstore signature. This is what makes keyless verification enforce provenance, not just "someone signed this with some key."
SBOM and attestation verification¶
Beyond the signature itself, Kyverno can require that an image carry a verified in-toto attestation - an SBOM, a vulnerability scan result, or a provenance statement - and evaluate conditions against its contents:
verifyImages:
- imageReferences:
- "registry.mycompany.com/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/my-app/.github/workflows/build.yaml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
attestations:
- predicateType: https://spdx.dev/Document
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/my-app/.github/workflows/build.yaml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
conditions:
- all:
- key: "{{ element.name }}"
operator: NotEquals
value: "log4j-core"
- predicateType: https://slsa.dev/provenance/v0.2
conditions:
- all:
- key: "{{ builder.id }}"
operator: AnyIn
value:
- "https://github.com/myorg/my-app/.github/workflows/build.yaml@refs/heads/main"
Two things about the syntax are worth calling out, because both are easy to get wrong. First, Kyverno's JMESPath variables do not nest - {{ regex_match('...', '{{ element.value }}') }} is not valid; a {{ }} expression may not contain another one. Keep each condition to a single-level expression like {{ element.name }} and put the comparison in operator/value. Second, operator: Equals is an exact string comparison and does not glob, so a trailing * in the value is matched literally; use AnyIn against an explicit list (or Equals against the exact identity) instead of hoping for a wildcard.
The example checks two attestation types on the same image: an SPDX SBOM (asserting a specific package is absent from the component list - adjust the key to match your SBOM's actual field names) and a SLSA provenance attestation (verifying which builder produced the image). Both attestations must themselves be signed and pass the same attestors check as the image signature - Kyverno doesn't just check that an attestation exists, it verifies who attested to it, closing the gap where someone could attach a fake clean SBOM to a vulnerable image.
PolicyReport¶
Kyverno writes audit results to PolicyReport (namespaced) and ClusterPolicyReport (cluster-scoped) objects. Query them to find violations without enforcement:
kubectl get policyreport -A
kubectl describe policyreport -n production
# Find all violations across cluster
kubectl get policyreport -A -o json \
| jq '[.items[].results[] | select(.result == "fail")] | group_by(.policy) | map({policy: .[0].policy, count: length})'
PolicyReports integrate with Grafana (via the policy-reporter project) for dashboards and trending.
PolicyReport vs Gatekeeper's audit status¶
Both engines run a background scan of existing cluster resources against active policies, and both exist for the same reason: catch objects that were admitted before a policy existed, or admitted under Audit/dryrun. The difference is where the results live and how you consume them.
Gatekeeper writes audit results into status.violations on the Constraint object itself - to see all violations you list Constraints and read their status, which is fine for a handful of policies but gets unwieldy as a dashboard source since violations are scattered across many differently-typed Constraint objects.
Kyverno writes results into standalone PolicyReport/ClusterPolicyReport objects, one report per scanned resource, each containing a results array with an entry per rule evaluated against it. This is a normalized, queryable shape independent of how many policies exist - the jq query above works the same whether you have five policies or five hundred. It's also why the policy-reporter project (which builds Grafana dashboards, Slack alerts, and UI views on top of PolicyReports) works well for Kyverno specifically: the report shape was designed to be consumed this way, whereas scraping Gatekeeper's Constraint statuses for the same purpose requires more custom tooling.
Cleanup policies¶
(CleanupPolicy/ClusterCleanupPolicy at kyverno.io/v2, shown below, are also part of the deprecated legacy model; their CEL-native counterpart is DeletingPolicy - see CEL as a first-class policy type.)
CleanupPolicy (namespaced) and ClusterCleanupPolicy (cluster-scoped) do something Gatekeeper has no native equivalent for: scheduled, conditional deletion of matching resources. Rather than validating or mutating on admission, a cleanup policy runs on a cron schedule and deletes anything matching its selector and condition:
apiVersion: kyverno.io/v2
kind: ClusterCleanupPolicy
metadata:
name: cleanup-completed-jobs
spec:
match:
any:
- resources:
kinds:
- batch/v1/Job
conditions:
all:
- key: "{{ target.status.succeeded }}"
operator: Equals
value: 1
schedule: "0 * * * *" # every hour
apiVersion: kyverno.io/v2
kind: CleanupPolicy
metadata:
name: cleanup-stale-debug-pods
namespace: debug
spec:
match:
any:
- resources:
kinds: [Pod]
selector:
matchLabels:
purpose: emergency-debug
conditions:
all:
- key: "{{ time_since('', target.metadata.creationTimestamp, '') }}"
operator: GreaterThan
value: "24h"
schedule: "0 */6 * * *" # every 6 hours
Common uses: expiring completed Jobs and their Pods, sweeping up temporary debug workloads (pairs naturally with the PolicyException example earlier, which exempted purpose: emergency-debug Pods from the privileged-container policy - the cleanup policy makes sure that exemption doesn't become a permanent backdoor by deleting those Pods automatically after a day), and removing expired Certificates or other short-lived objects that a controller failed to garbage-collect. Cleanup policies run with a dry-run mode too (spec.dryRun: true reports what would be deleted without deleting it) - use it the same way you'd use Audit mode on a validate policy, to confirm the selector and condition match only what you expect before enabling real deletion.
Policy exceptions¶
(The kyverno.io/v2 PolicyException shown below, used with ClusterPolicy, is also on the deprecation path described above. Kyverno 1.16 added a parallel PolicyException mechanism scoped for the new CEL-native policy types - same purpose, wired to ValidatingPolicy/ImageValidatingPolicy/etc. instead of ClusterPolicy - so exceptions remain available on both sides of the migration.)
Exempt specific resources from specific policies without modifying the policy itself:
apiVersion: kyverno.io/v2
kind: PolicyException
metadata:
name: allow-privileged-debug-tool
namespace: debug
spec:
exceptions:
- policyName: disallow-privileged-containers
ruleNames:
- check-privileged
match:
any:
- resources:
kinds: [Pod]
namespaces: [debug]
selector:
matchLabels:
purpose: emergency-debug
Exceptions are scoped by namespace and label selector. This is safer than modifying the policy - the exception is explicit, reviewable, and can be removed independently.
Scoping exceptions safely¶
A PolicyException is itself a cluster-impacting object - an overly broad one silently reintroduces the exact risk the policy was written to prevent, just one layer removed. A few practices keep exceptions from becoming a second, ungoverned policy system:
- Scope to the narrowest match that solves the actual need. The example above combines a namespace (
debug) and a label selector (purpose: emergency-debug) - either alone is weaker. Namespace-only means every Pod indebugis exempt, even ones that have nothing to do with the emergency tooling the exception was written for. Label-only means the exception could be triggered anywhere in the cluster by anyone who can set that label. - Name the specific
ruleNames, not the whole policy.spec.exceptions[].ruleNameslets you exempt one rule inside a multi-rule policy rather than every rule in it. A Pod that legitimately needsprivileged: truefor a debug tool still shouldn't be exempt from, say, the same policy's "nohostNetwork" rule if it doesn't need that too. - Restrict who can create
PolicyExceptionobjects. Since an exception can undo any validate policy's effect for matching resources, RBAC on thePolicyExceptionCRD itself should be at least as tight as RBAC on the policies it can bypass - otherwise you've built a well-designed policy system with an unguarded escape hatch. Kyverno also supports a controller-level flag restricting which namespaces are even allowed to containPolicyExceptionobjects, which is worth setting to a small allowlist (e.g. onlydebug,platform-exceptions) rather than leaving it cluster-wide. - Prefer a time-boxed cleanup over an indefinite exception. Pairing an exception with a
CleanupPolicy(as in the debug-pod example above) or simply reviewing exceptions on a schedule keeps them from calcifying into permanent, forgotten holes in enforcement - the same operational discipline that applies to any firewall rule or IAM grant with "temporary" in the justification.
This is also the concrete answer to "why not just disable the policy for that namespace with exclude": a static exclude in the policy YAML is invisible unless someone reads the policy file, applies to everything matching it forever, and requires editing (and re-reviewing) the policy itself to add or remove. A PolicyException is its own object with its own audit trail (kubectl get policyexception -A), can be time-limited operationally, and doesn't touch the policy that everything else still depends on.
Testing with kyverno test¶
The kyverno test CLI validates policies against declarative test fixtures locally, before anything is applied to a cluster - it's the Kyverno equivalent of Gatekeeper's gator test, and should run in CI on every policy change:
# kyverno-test.yaml
name: test-require-limits
policies:
- require-resource-limits.yaml
resources:
- pod-with-limits.yaml
- pod-without-limits.yaml
results:
- policy: require-resource-limits
rule: check-container-limits
resource: pod-without-limits
result: fail
- policy: require-resource-limits
rule: check-container-limits
resource: pod-with-limits
result: pass
The test manifest isn't limited to validate rules - results entries can assert on mutate outcomes (comparing against a patchedResource fixture showing the expected post-mutation object) and on generate outcomes (asserting a specific generated resource was created with expected content), which makes it possible to cover an entire multi-rule policy like the namespace-baseline example above in one test file: one resource fixture (a Namespace object), and two expected results, one per generate rule.
Run kyverno test against a directory to execute every kyverno-test.yaml found underneath it recursively, which is the natural layout for a GitOps repo with policies grouped by domain (policies/security/, policies/governance/, each with its own tests alongside the policy YAML).
Production rollout pattern¶
- Deploy policy in
Auditmode. Let background scanner run for 24 hours. - Review
PolicyReportresults. Identify violating resources. - Fix violations or add targeted
PolicyExceptionobjects. - Switch to
Enforcemode. Monitor admission webhook error rate. - Commit policies to Git. Sync via Argo CD or Flux.
Kyverno vs OPA/Gatekeeper¶
| Kyverno | OPA/Gatekeeper | |
|---|---|---|
| Policy language | YAML (Kubernetes-native) | Rego |
| Learning curve | Low (know K8s YAML? you're done) | Medium-High (Rego is a new language) |
| Flexibility | Medium (CEL helps with complex logic) | High (Rego is a full logic language - set operations, comprehensions, cross-object correlation - while remaining non-Turing-complete and guaranteed to terminate, which is why it's safe on an admission hot path) |
| Mutation | First-class, patchStrategicMerge | Supported but separate |
| Generation | Built in | Not supported |
| Image verification | Built in (Cosign, Notary) | Requires external webhook |
| Audit/reporting | PolicyReport CRD | Status on Constraint object |
| Policy testing | kyverno test CLI |
conftest |
Use Kyverno when you want policy as YAML with low operational overhead and built-in generation and image verification. Use OPA/Gatekeeper when your policies are complex enough to need Rego's expressiveness or when you already have OPA in your stack for non-Kubernetes use cases. See OPA and Gatekeeper for the Rego-side depth (constraint templates, mutation CRDs, expansion, data replication) that mirrors this page's Kyverno-side coverage.
Common mistakes¶
- Enabling
Enforcebefore running anAuditpass. Same lesson as Gatekeeper'sdryrun: every new policy should spend at least a day inAuditmode withbackground: trueso the background scanner surfaces existing violations inPolicyReportbefore anything starts blocking admission. - Forgetting
background: trueon a validate policy. Without it, the background-controller never scans existing resources, soPolicyReportonly reflects objects admitted after the policy was created - everything already in the cluster is silently ungoverned and won't show up as a violation anywhere. - Using
synchronize: trueon a generate rule that users are expected to customize. If a generated resource is meant to be a starting point (a scaffold ConfigMap, a default ServiceAccount someone will add permissions to),synchronize: truefights every edit the owner makes. Reservesynchronize: truefor baseline/security resources you genuinely never want drifted. - Rolling out a generate policy without
generateExisting: trueand assuming it backfilled the whole cluster. By defaultgeneraterules only fire on new trigger resources - pre-existing Namespaces (or whatever the trigger kind is) won't get the generated resource unless you explicitly setgenerateExisting: trueor run a one-off backfill. - Broad
PolicyExceptionobjects. As covered above, an exception scoped only by namespace (no label selector) or exempting an entire policy (noruleNames) reopens more risk surface than it needs to. Reviewkubectl get policyexception -Aperiodically the way you'd review firewall rules. - Treating
patchStrategicMergemutation as always safe to layer. Strategic merge patches interact with list merge keys (likenamefor containers) in ways that can silently duplicate or fail to match elements if the target list doesn't have the merge key Kyverno expects. When mutating list fields, verify the patched output withkubectl apply --dry-run=server -o yamlrather than assuming the patch applied the way it reads. - No policy tests in CI. Same as the Gatekeeper side of this:
kyverno testis cheap to run and catches both logic errors and accidental policy regressions before they reachAuditmode in a real cluster, let aloneEnforce. - Unbounded cleanup policies. A
ClusterCleanupPolicywith a broad selector and no dry-run verification first is a scheduled, unattended deletion job - treat writing one with the same care as writing akubectl deletescript that runs on a cron, because that's exactly what it is. - Starting new policy work on the deprecated
ClusterPolicymodel. As of Kyverno 1.19,ClusterPolicy,Policy,CleanupPolicy, and the legacyPolicyExceptionare deprecated and slated for removal in 1.20. Existing policies keep working, but new policies should target the CEL-native types (ValidatingPolicy,MutatingPolicy,GeneratingPolicy,ImageValidatingPolicy,DeletingPolicy) so you're not migrating twice.
Source Links¶
- Kyverno documentation
- kyverno/kyverno on GitHub and its releases
- CNCF: Kyverno graduation announcement
- Kyverno migration to CEL-based policy types
- Kyverno policy types and rule reference
- Kyverno JMESPath variables and custom filters
- Kyverno image verification (
verifyImages) - Kyverno policy reports
- Kyverno policy library
- Sigstore Cosign attestations