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:
- Validate — Check that resources meet certain conditions and block creation or updates if they don't. For example, "all Pods must have resource limits defined."
- Mutate — Modify resources on the fly before they are persisted. For example, "automatically inject a sidecar container or add a default label to every Namespace."
- Generate — Create new Kubernetes resources when certain triggers occur. For example, "when a new Namespace is created, automatically generate a NetworkPolicy and a default RoleBinding for it."
- Verify Images — Inspect container image signatures, attestations, and provenance using tools like Cosign, Notary, or Sigstore before admitting images into the cluster.
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:
- Security Compliance — Enforce standards such as requiring images only from trusted registries, blocking privileged containers, or mandating that Pods run as non-root. Kyverno can validate against Pod Security Standards (PSS) and other compliance frameworks out of the box.
- Operational Consistency — Automatically inject standard labels, annotations, or sidecar containers across all workloads. This ensures every resource in your cluster conforms to your organization's conventions without requiring every developer to remember every detail.
- Audit Trail — Kyverno's background scanning generates Policy Reports that show which resources pass or fail policies, even for resources that existed before the policy was deployed. These reports are invaluable for auditors and for tracking remediation progress.
- Cost Management — Enforce resource limits and requests on every workload to prevent a single application from consuming the entire cluster's capacity, which aligns with FinOps goals.
- Supply Chain Security — Verify image signatures and attestations to ensure only cryptographically signed, provenance-tracked container images run in your environment, protecting against supply chain attacks.
- GitOps Readiness — Since policies are YAML documents, they live naturally in a Git repository. Tools like ArgoCD and Flux can apply Kyverno policies just like application manifests, making policy deployment auditable, versioned, and automated.
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.
match.any— The rule applies if the resource matches at least one of the conditions defined in the any list. Each element can combine multiple criteria with an implicit AND.match.all— The rule applies only if the resource matches every condition defined in the all list.exclude— Resources that match the exclude block are skipped entirely, even if they match the match block. Exclude uses the same any/all structure.
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:
validationFailureAction: Enforce— This makes the policy blocking. The alternative isAudit, which logs violations in Policy Reports without blocking the request. Use Audit mode while testing new policies.background: true— Enables periodic scanning of existing resources, not just newly arriving requests.- The
matchblock targets Pods in any namespace. - The
validate.deny.conditionsblock uses Kyverno's variable substitution with{{ }}syntax to extract values from the incoming resource. Theanycondition evaluates to true if at least one container is missing CPU or memory limits. - The
messageis returned to the user when the request is denied, making it clear what went wrong.
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:
generate.apiVersionandgenerate.kindspecify what resource to create.generate.nameis the name of the generated resource.generate.namespaceuses a variable to place the NetworkPolicy in the newly created Namespace.synchronize: truemeans Kyverno will keep the generated resource in sync with the policy. If someone manually deletes the NetworkPolicy, Kyverno recreates it. If the policy definition changes, Kyverno updates the generated resource.generate.datacontains the full spec of the resource to create, exactly as you would write it in a standard YAML manifest.
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.
- Start in Audit Mode — Always deploy new policies with
validationFailureAction: Auditfirst. Let them run for a few days, examine the Policy Reports to understand the blast radius, and only then switch toEnforce. This prevents policies from accidentally breaking legitimate workloads. - Use Descriptive Metadata — Add annotations like
policies.kyverno.io/title,policies.kyverno.io/category,policies.kyverno.io/severity, andpolicies.kyverno.io/descriptionto every policy. These annotations appear in Policy Reports and make it easier for teams