← Back to DevBytes

Kyverno Policies: Complete Configuration Guide

What Are Kyverno Policies?

Kyverno is a policy engine designed specifically for Kubernetes. Originally created as a Kubernetes-native policy management tool, it has since evolved into a full-featured CNCF project that enforces security, compliance, and operational best practices across your clusters. At its core, Kyverno operates as a dynamic admission controller coupled with a powerful background scanning capability.

Kyverno policies are Kubernetes resources written in YAML—just like Pods, Deployments, or ConfigMaps. There is no new language to learn, no unfamiliar syntax to wrestle with. A policy is simply a custom resource that describes what Kyverno should validate, mutate, generate, or clean up. This declarative model means that policies can be version-controlled in Git, reviewed in pull requests, and deployed through your existing CI/CD pipelines alongside application manifests.

A Kyverno policy can do four fundamentally different things, each corresponding to a rule type:

Because policies are standard Kubernetes resources, you interact with them using kubectl just like everything else: kubectl get policy, kubectl describe policy, kubectl delete policy. This dramatically lowers the barrier to entry compared to other policy frameworks that require learning a specialized query language or configuring complex webhook plumbing by hand.

Why Kyverno Policies Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In a modern Kubernetes environment, the number of actors creating resources grows quickly—developers deploying applications, platform engineers managing infrastructure, CI/CD pipelines applying manifests, and operators running maintenance tasks. Without a policy engine, every one of these actors operates with the permissions granted by RBAC, but RBAC alone cannot answer questions like "does this Pod use a prohibited image registry?" or "does this Deployment have the required cost-center label?"

Kyverno fills this gap by providing a unified framework for expressing guardrails. Here's why it matters for real-world operations:

Perhaps most importantly, Kyverno reduces the cognitive load on development teams. Instead of expecting every developer to be an expert on security and compliance, platform teams can encode their requirements as policies. Developers then receive clear, actionable error messages when their resources violate policy, allowing them to fix issues early in the development cycle rather than discovering them during a post-deployment audit.

Core Concepts and Policy Structure

Before writing policies, it's essential to understand the building blocks. Every Kyverno policy is a custom resource of kind ClusterPolicy (cluster-scoped) or Policy (namespace-scoped). A policy contains one or more rules, and each rule specifies a match block (which resources to act on), an action (validate, mutate, generate, or verifyImages), and rule-specific configuration.

Policy Kinds: ClusterPolicy vs. Policy

A ClusterPolicy is a cluster-wide resource that applies across all namespaces. Use this for policies that should govern every resource of a given type in the entire cluster—for example, "every Pod must have resource limits" or "every Namespace must have a network-policy label."

A namespaced Policy is scoped to a single namespace and only applies to resources in that namespace. This is useful when different teams or applications have distinct policy requirements within their own namespaces.

Both kinds share the exact same rule structure. The only difference is scope. Here is a minimal ClusterPolicy skeleton:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: my-policy
spec:
  rules:
    - name: my-rule
      match:
        any:
          - resources:
              kinds:
                - Pod
      # rule action goes here

And the equivalent namespaced Policy:

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: my-policy
  namespace: team-a
spec:
  rules:
    - name: my-rule
      match:
        any:
          - resources:
              kinds:
                - Pod
      # rule action goes here

Rule Anatomy: Match and Exclude

Every rule in a Kyverno policy has a match block and an optional exclude block. The match block uses a logical OR across its any entries and a logical AND across all entries within each element. This gives you precise control over which resources a rule applies to.

Here is an example that matches Pods or Deployments in namespaces that have the label environment: production but excludes resources in the kube-system namespace:

match:
  any:
    - resources:
        kinds:
          - Pod
          - Deployment
        namespaces:
          selector:
            matchLabels:
              environment: production
  exclude:
    any:
      - resources:
          namespaces:
            - kube-system

The resources block inside match or exclude can filter by kinds, names, namespaces, namespaceSelector, and annotations. This gives you fine-grained targeting without resorting to a separate query language.

Rule Types Overview

Each rule must specify exactly one of the following actions: validate, mutate, generate, or verifyImages. The structure differs slightly for each, and we will explore every one in detail with complete, working examples.

How to Install Kyverno

Before writing policies, you need Kyverno running in your cluster. The recommended installation method is via Helm. The Kyverno project provides a Helm chart that deploys the admission controller, the background scanner, the cleanup controller, and the reports controller.

First, add the Kyverno Helm repository:

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

Then install Kyverno into its own namespace. The chart supports many configuration options; for a standard production installation, you typically want high-availability replicas and the full controller suite:

helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set admissionController.replicas=3 \
  --set backgroundController.replicas=2 \
  --set cleanupController.enabled=true \
  --set reportsController.enabled=true

Verify the installation by checking that the Kyverno pods are running:

kubectl get pods -n kyverno

You should see pods for the admission controller, background controller, cleanup controller, and reports controller. Once all pods show Running status with ready containers, you can begin creating policies.

For air-gapped environments or clusters where Helm is not available, Kyverno also supports installation via raw YAML manifests published on the releases page. However, Helm remains the recommended approach for production because it simplifies upgrades and configuration management.

Writing Your First Kyverno Policy

Let's build complete, practical policies for each rule type. Every example in this section is self-contained and ready to apply to a cluster with Kyverno installed.

Validation Policies: Enforcing Constraints

Validation policies are the most common type. They inspect resources during admission (create, update, delete operations) and either allow or deny the request based on a condition expressed as a CEL expression, a JSON pattern, or a JMESPath expression. Let's start with a policy that requires every Pod to define CPU and memory limits:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
  annotations:
    policies.kyverno.io/title: Require Resource Limits
    policies.kyverno.io/category: Best Practices
    policies.kyverno.io/severity: medium
    policies.kyverno.io/description: >
      Every container in every Pod must specify both CPU and memory limits.
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: validate-resource-limits
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Every container must specify CPU and memory limits."
        deny:
          conditions:
            any:
              - key: "{{ request.object.spec.containers[].resources.limits.cpu || '' }}"
                operator: Equals
                value: ""
              - key: "{{ request.object.spec.containers[].resources.limits.memory || '' }}"
                operator: Equals
                value: ""

Let's walk through this policy step by step:

Now let's write a more advanced validation policy that enforces a specific pattern on labels. This policy requires that every Namespace have a cost-center label with a value matching a regex pattern like CC-1234:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-cost-center-label
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-cost-center
      match:
        any:
          - resources:
              kinds:
                - Namespace
      validate:
        message: "Namespace must have a 'cost-center' label matching pattern 'CC-\\d+'"
        pattern:
          metadata:
            labels:
              cost-center: "CC-?*"

The validate.pattern block provides a declarative way to describe the expected structure. Kyverno overlays the pattern on the incoming resource and flags any divergence. The wildcard ?* means "zero or more characters." For stricter pattern matching, use a deny condition with a regex validator instead.

For an even stricter approach using CEL (Common Expression Language), available in Kyverno 1.11+:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-cost-center-cel
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-cost-center-cel
      match:
        any:
          - resources:
              kinds:
                - Namespace
      validate:
        cel:
          expressions:
            - expression: "has(object.metadata.labels) && object.metadata.labels.contains('cost-center')"
              message: "Namespace must have a 'cost-center' label."
            - expression: "object.metadata.labels['cost-center'].matches('^CC-\\\\d+$')"
              message: "The 'cost-center' label must match pattern CC-1234."

CEL expressions are compiled and evaluated efficiently, and they support complex logic including regex matching, list operations, and type checking. This is the recommended approach for new policies because of its clarity and performance.

Mutation Policies: Transforming Resources

Mutation policies modify resources before they are persisted. They can add labels, annotations, environment variables, volumes, sidecar containers, and even modify existing fields. Mutations happen transparently—the user submits a resource, Kyverno applies the mutation, and the mutated version is what gets stored in etcd.

Here is a policy that automatically adds a team label and a monitoring: enabled annotation to every new Namespace:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-namespace-labels
spec:
  rules:
    - name: add-team-label
      match:
        any:
          - resources:
              kinds:
                - Namespace
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              team: platform
              created-by: kyverno
            annotations:
              monitoring: "enabled"

The mutate.patchStrategicMerge block uses Kubernetes' strategic merge patch semantics. Kyverno merges the specified fields into the incoming resource. If the team label already exists, it is overwritten with platform. If it doesn't exist, it is added. This behavior is configurable: you can use patchStrategicMerge with an overlay approach for more control.

Now a more powerful mutation: automatically injecting a sidecar container into every Pod that has the label sidecar: true. This is a common pattern for service mesh injection, log collection, or tracing:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: inject-sidecar
spec:
  rules:
    - name: inject-logging-sidecar
      match:
        any:
          - resources:
              kinds:
                - Pod
              selector:
                matchLabels:
                  sidecar: "true"
      mutate:
        patchesJson6902: |
          - op: add
            path: /spec/containers/-
            value:
              name: log-collector
              image: busybox:1.36
              command: ["sh", "-c", "tail -f /var/log/app.log"]
              volumeMounts:
                - name: app-logs
                  mountPath: /var/log

This uses a JSON Patch (RFC 6902) to append a container to the spec.containers array. The - path indicates "append to the end of the array." JSON patches give you surgical precision when modifying deeply nested structures.

Mutation policies can also use the newer mutate.foreach construct to apply transformations to each element of a list. For instance, to add an environment variable to every container in a Pod:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-env-to-containers
spec:
  rules:
    - name: add-standard-env
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        foreach:
          - list: "request.object.spec.containers"
            patchesJson6902: |
              - op: add
                path: /env/-
                value:
                  name: KYVERNO_MUTATED
                  value: "true"

The foreach block iterates over every element in the specified list and applies the patch to each one. This is far more concise than writing separate rules for init containers, ephemeral containers, and regular containers.

Generation Policies: Creating Resources Automatically

Generation policies create new Kubernetes resources when a trigger event occurs. The classic example is generating a default NetworkPolicy in every new Namespace to ensure isolation by default:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-network-policy
spec:
  rules:
    - name: generate-np
      match:
        any:
          - resources:
              kinds:
                - Namespace
      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

Key details in this generation policy:

Generation policies are incredibly useful for bootstrapping. You can generate RBAC roles, ServiceAccounts, ConfigMaps, or even entire application scaffolding whenever a new Namespace appears. Here is a more ambitious example that generates a full set of resources for each new Namespace labeled app: microservice:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: bootstrap-microservice-namespace
spec:
  rules:
    - name: generate-rbac
      match:
        any:
          - resources:
              kinds:
                - Namespace
              selector:
                matchLabels:
                  app: microservice
      generate:
        apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        name: default-reader
        namespace: "{{ request.object.metadata.name }}"
        synchronize: true
        data:
          subjects:
            - kind: Group
              name: developers
              apiGroup: rbac.authorization.k8s.io
          roleRef:
            kind: ClusterRole
            name: view
            apiGroup: rbac.authorization.k8s.io
    - name: generate-quota
      match:
        any:
          - resources:
              kinds:
                - Namespace
              selector:
                matchLabels:
                  app: microservice
      generate:
        apiVersion: v1
        kind: ResourceQuota
        name: default-quota
        namespace: "{{ request.object.metadata.name }}"
        synchronize: true
        data:
          spec:
            hard:
              requests.cpu: "10"
              requests.memory: "20Gi"
              limits.cpu: "20"
              limits.memory: "40Gi"

Notice that a single ClusterPolicy can contain multiple rules, each generating a different resource. All rules are evaluated independently, so the RoleBinding and ResourceQuota are both created when a matching Namespace appears.

Cleanup Policies: Removing Stale Resources

Introduced in Kyverno 1.9, cleanup policies address the problem of resource drift and stale objects. They automatically delete resources that match certain criteria on a scheduled basis. This is invaluable for removing expired test environments, cleaning up completed Jobs, or deleting temporary resources that developers forget to remove.

Here is a policy that deletes all Pods in namespaces labeled ttl: 7d after they have been running for more than 7 days:

apiVersion: kyverno.io/v2
kind: ClusterCleanupPolicy
metadata:
  name: cleanup-old-pods
spec:
  schedule: "0 0 * * *"   # Run every day at midnight
  match:
    any:
      - resources:
          kinds:
            - Pod
          namespaces:
            selector:
              matchLabels:
                ttl: "7d"
  conditions:
    any:
      - key: "{{ target.metadata.creationTimestamp }}"
        operator: LessThan
        value: "{{ now | subtract('168h') | quote }}"

Cleanup policies use a schedule field (cron syntax) to define when they run. The conditions block uses Kyverno's variable substitution with built-in time functions like now and subtract to calculate age. When a resource matches both the match block and the conditions, it is deleted.

A simpler cleanup policy for deleting completed Jobs older than 30 days:

apiVersion: kyverno.io/v2
kind: ClusterCleanupPolicy
metadata:
  name: cleanup-completed-jobs
spec:
  schedule: "0 2 * * *"   # 2 AM daily
  match:
    any:
      - resources:
          kinds:
            - Job
  conditions:
    all:
      - key: "{{ target.status.succeeded }}"
        operator: Equals
        value: "1"
      - key: "{{ target.metadata.creationTimestamp }}"
        operator: LessThan
        value: "{{ now | subtract('720h') | quote }}"

Cleanup policies are an essential part of a mature cluster management strategy. They prevent the accumulation of garbage resources that waste etcd storage, clutter kubectl output, and complicate debugging.

Verify Images Policies: Supply Chain Security

Image verification policies leverage Cosign, Notary, or Sigstore to cryptographically verify container images before they are allowed to run. This ensures that only images signed by trusted identities enter your cluster.

Here is a policy that requires all images to be signed using Cosign with a specified public key:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-images
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.io/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

This policy uses keyless (OIDC-based) signing, which ties image signatures to GitHub Actions workflows. The imageReferences field supports glob patterns, so myregistry.io/* matches all images from that registry. The attestors block defines what constitutes a valid signature—in this case, a signature from a specific GitHub Actions workflow verified via the public Rekor transparency log.

For simpler use cases with a static public key:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-images-static-key
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

Image verification integrates seamlessly into your deployment pipeline. When a user or CI system creates a Pod with an unsigned or improperly signed image, Kyverno blocks the request and returns a clear message explaining which image failed verification.

Advanced Policy Techniques

Once you're comfortable with basic policies, several advanced features unlock powerful automation and precise control.

Using Variables and JMESPath

Kyverno supports variable substitution using the {{ }} syntax. Variables can reference the incoming resource (request.object), the user who made the request (request.user), the operation type (request.operation), and more. For complex transformations, JMESPath expressions allow you to query and reshape JSON data within policy definitions.

Here is a policy that extracts all container image references from a Pod and validates that each one comes from an allowed registry, using a JMESPath expression to flatten the list:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: allowed-registries
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "All container images must come from registries: registry.company.com or docker.io/company"
        deny:
          conditions:
            any:
              - key: "{{ request.object.spec.containers[].image | join(@, ' ') }}"
                operator: NotEquals
                value: ""
              - key: |
                  {{ request.object.spec.containers[].image | map(@, 'contains(@, ''registry.company.com'') || contains(@, ''docker.io/company'')') | contains(@, false) }}
                operator: Equals
                value: true

The JMESPath expression map(@, 'contains(@, ...)') checks each image against the allowed registries and returns a boolean list. The outer contains(@, false) checks whether any image failed the check. This is a powerful pattern for validating every element in a list against a condition.

Variables can also reference external data. Kyverno supports context blocks that fetch data from ConfigMaps, Secrets, or even external API endpoints before evaluating the rule:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: allowed-registries-from-configmap
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      context:
        - name: allowedRegistries
          configMap:
            name: allowed-registries
            namespace: kyverno
            defaultLookup: true
      validate:
        message: "Images must be from an allowed registry defined in ConfigMap."
        deny:
          conditions:
            any:
              - key: "{{ request.object.spec.containers[].image }}"
                operator: AnyNotIn
                value: "{{ allowedRegistries.data.allowed || '' }}"

The context block fetches a ConfigMap named allowed-registries in the kyverno namespace and makes its data available as the variable allowedRegistries. This decouples policy logic from data, allowing you to change the list of allowed registries by updating a ConfigMap rather than modifying the policy itself.

Background Scanning and Policy Reports

When background: true is set on a policy (the default), Kyverno periodically scans all existing resources in the cluster and evaluates them against the policy rules. The results are written to PolicyReport (namespaced) and ClusterPolicyReport (cluster-scoped) custom resources.

You can inspect reports with kubectl:

kubectl get policyreports -A
kubectl get clusterpolicyreports

A report entry for a failing resource looks like this:

apiVersion: wgpolicyk8s.io/v1
kind: PolicyReport
metadata:
  name: pol-require-resource-limits
  namespace: default
results:
  - policy: require-resource-limits
    rule: validate-resource-limits
    result: fail
    severity: medium
    message: "Every container must specify CPU and memory limits."
    resources:
      - apiVersion: v1
        kind: Pod
        name: nginx-deployment-5c689d88bb-abcde
        namespace: default

These reports are machine-readable and can be aggregated by tools like Grafana, Prometheus, or custom dashboards. They give security and compliance teams a clear picture of policy adherence across the entire fleet of clusters.

Combining Rules in a Single Policy

A single ClusterPolicy can contain multiple rules, each with different match criteria and actions. This is more efficient than creating many small policies because it reduces the number of resources Kyverno must process. Here is a policy that both validates and mutates Namespaces in one declaration:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: namespace-governance
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: require-cost-center
      match:
        any:
          - resources:
              kinds:
                - Namespace
      validate:
        message: "Namespace must have cost-center label."
        pattern:
          metadata:
            labels:
              cost-center: "?*"
    - name: add-network-label
      match:
        any:
          - resources:
              kinds:
                - Namespace
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              network-policy: required
    - name: generate-network-policy
      match:
        any:
          - resources:
              kinds:
                - Namespace
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny
        namespace: "{{ request.object.metadata.name }}"
        synchronize: true
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress

This single policy validates that every Namespace has a cost-center label, mutates it to add a network-policy label if missing, and generates a default-deny NetworkPolicy. All three actions fire in sequence during admission: first mutation, then validation, then generation.

Best Practices for Kyverno Policies

Writing effective policies is a skill that develops with experience. The following best practices will help you build a robust, maintainable policy framework from day one.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles