Skip to content

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' MutatingAdmissionPolicy reached 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 - PolicyReport integration, 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's data/clone source. 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: true controls whether the policy also applies retroactively to resources that already exist when the policy itself is created or updated. By default, generate rules only fire on new trigger resources going forward (e.g. new Namespaces created after the policy exists) - a ClusterPolicy generating NetworkPolicies from a Namespace trigger won't backfill existing namespaces unless generateExisting: true is 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:

  1. 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.
  2. 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.
  3. At verification time (the keyless block in the example above), Kyverno checks that the signing certificate's subject and issuer match 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 in debug is 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[].ruleNames lets you exempt one rule inside a multi-rule policy rather than every rule in it. A Pod that legitimately needs privileged: true for a debug tool still shouldn't be exempt from, say, the same policy's "no hostNetwork" rule if it doesn't need that too.
  • Restrict who can create PolicyException objects. Since an exception can undo any validate policy's effect for matching resources, RBAC on the PolicyException CRD 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 contain PolicyException objects, which is worth setting to a small allowlist (e.g. only debug, 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 ./policies/
# 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

  1. Deploy policy in Audit mode. Let background scanner run for 24 hours.
  2. Review PolicyReport results. Identify violating resources.
  3. Fix violations or add targeted PolicyException objects.
  4. Switch to Enforce mode. Monitor admission webhook error rate.
  5. 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 Enforce before running an Audit pass. Same lesson as Gatekeeper's dryrun: every new policy should spend at least a day in Audit mode with background: true so the background scanner surfaces existing violations in PolicyReport before anything starts blocking admission.
  • Forgetting background: true on a validate policy. Without it, the background-controller never scans existing resources, so PolicyReport only 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: true on 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: true fights every edit the owner makes. Reserve synchronize: true for baseline/security resources you genuinely never want drifted.
  • Rolling out a generate policy without generateExisting: true and assuming it backfilled the whole cluster. By default generate rules only fire on new trigger resources - pre-existing Namespaces (or whatever the trigger kind is) won't get the generated resource unless you explicitly set generateExisting: true or run a one-off backfill.
  • Broad PolicyException objects. As covered above, an exception scoped only by namespace (no label selector) or exempting an entire policy (no ruleNames) reopens more risk surface than it needs to. Review kubectl get policyexception -A periodically the way you'd review firewall rules.
  • Treating patchStrategicMerge mutation as always safe to layer. Strategic merge patches interact with list merge keys (like name for 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 with kubectl apply --dry-run=server -o yaml rather than assuming the patch applied the way it reads.
  • No policy tests in CI. Same as the Gatekeeper side of this: kyverno test is cheap to run and catches both logic errors and accidental policy regressions before they reach Audit mode in a real cluster, let alone Enforce.
  • Unbounded cleanup policies. A ClusterCleanupPolicy with a broad selector and no dry-run verification first is a scheduled, unattended deletion job - treat writing one with the same care as writing a kubectl delete script that runs on a cron, because that's exactly what it is.
  • Starting new policy work on the deprecated ClusterPolicy model. As of Kyverno 1.19, ClusterPolicy, Policy, CleanupPolicy, and the legacy PolicyException are 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.