Back to Distributed Systems & Kubernetes Series

Kyverno Track Part 1: Install & Validate Policies

June 6, 2026 Wasil Zafar 36 min read

Kyverno is a Kubernetes-native policy engine that uses YAML — no new language to learn. Policies are Kubernetes resources (ClusterPolicy / Policy) that validate, mutate, or generate resources. If a pod or deployment violates a policy, the admission webhook rejects the request with a clear message.

Table of Contents

  1. Why Kyverno
  2. Install Kyverno
  3. ClusterPolicy — Validate Rules
  4. Policy vs ClusterPolicy
  5. Match & Exclude
  6. Exercises
  7. Key Takeaways & Next Steps

Why Kyverno

Kyverno was designed specifically for Kubernetes. Unlike OPA/Gatekeeper which requires learning the Rego language, Kyverno policies are pure YAML — the same format you already use for every other Kubernetes resource.

Key Advantages: YAML-native (no Rego), Kubernetes-native (policies are CRDs), built-in support for validate/mutate/generate/verifyImages, fine-grained match/exclude with label selectors, and detailed policy reports.
  • No new language — Policies are standard Kubernetes resources written in YAML
  • Built for K8s — Understands Kubernetes resource structure natively
  • Multiple rule types — validate, mutate, generate, and verifyImages in one engine
  • Policy Reports — Generates PolicyReport/ClusterPolicyReport CRDs for audit visibility
  • kubectl apply — Deploy policies the same way you deploy any other K8s manifest

Install Kyverno

Kyverno is installed via Helm. The chart deploys the admission webhook controller, background controller (for generate rules), and reports controller.

# Add the Kyverno Helm repository
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

# Install Kyverno in its own namespace
helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set replicaCount=3

# Verify pods are running
kubectl get pods -n kyverno
# NAME                                          READY   STATUS    RESTARTS   AGE
# kyverno-admission-controller-xxxxx            1/1     Running   0          30s
# kyverno-background-controller-xxxxx           1/1     Running   0          30s
# kyverno-cleanup-controller-xxxxx              1/1     Running   0          30s
# kyverno-reports-controller-xxxxx              1/1     Running   0          30s

# Verify the webhook is registered
kubectl get validatingwebhookconfigurations | grep kyverno
# kyverno-resource-validating-webhook-cfg

kubectl get mutatingwebhookconfigurations | grep kyverno
# kyverno-resource-mutating-webhook-cfg
Production Tip: Always run replicaCount=3 for high availability. The admission controller is in the critical path of all API server requests — if it's down, pods can't be created (unless you configure failurePolicy: Ignore).

ClusterPolicy — Validate Rules

Validate rules check resources against conditions and reject non-compliant requests. The validationFailureAction field controls behavior: Enforce blocks the request; Audit logs a violation but allows the request.

Require Labels

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
  annotations:
    policies.kyverno.io/title: Require Labels
    policies.kyverno.io/category: Best Practices
    policies.kyverno.io/severity: medium
    policies.kyverno.io/description: >-
      Require that all Pods have 'app.kubernetes.io/name'
      and 'app.kubernetes.io/managed-by' labels.
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-labels
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: >-
          The labels 'app.kubernetes.io/name' and
          'app.kubernetes.io/managed-by' are required.
        pattern:
          metadata:
            labels:
              app.kubernetes.io/name: "?*"
              app.kubernetes.io/managed-by: "?*"

The "?*" pattern means "any non-empty string." If a Pod is created without these labels, Kyverno rejects the request with the specified message.

Disallow Latest Tag

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
  annotations:
    policies.kyverno.io/title: Disallow Latest Tag
    policies.kyverno.io/category: Best Practices
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: validate-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images must use a specific tag, not ':latest' or no tag."
        pattern:
          spec:
            containers:
              - image: "!*:latest & *:*"
    - name: validate-init-container-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Init containers must use a specific tag."
        pattern:
          spec:
            =(initContainers):
              - image: "!*:latest & *:*"

The pattern "!*:latest & *:*" means: NOT ending in :latest AND must contain a colon (i.e., must have an explicit tag). The =(initContainers) syntax is an "anchor" — it means "if initContainers exists, then validate it" (optional field check).

Require Resource 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: high
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: validate-resources
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: >-
          All containers must have CPU and memory requests and limits defined.
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"
                  limits:
                    memory: "?*"
                    cpu: "?*"

Test the policy:

# Apply the policy
kubectl apply -f require-resource-limits.yaml

# This pod will be REJECTED — no resource limits
kubectl run test-no-limits --image=nginx:1.25
# Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
# resource Pod/default/test-no-limits was blocked due to the following policies:
# require-resource-limits:
#   validate-resources: "All containers must have CPU and memory requests and limits defined."

# This pod will be ACCEPTED
kubectl run test-with-limits --image=nginx:1.25 \
  --requests='cpu=100m,memory=128Mi' \
  --limits='cpu=200m,memory=256Mi'
# pod/test-with-limits created

Policy vs ClusterPolicy

Kyverno provides two scopes for policies:

  • ClusterPolicy — Applies to resources across all namespaces. Use for organization-wide standards.
  • Policy — Namespace-scoped. Applies only to resources in the same namespace as the policy. Use for team-specific rules.
# Namespace-scoped policy — only affects resources in 'production' namespace
apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: production-require-probes
  namespace: production
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-readiness-probe
      match:
        any:
          - resources:
              kinds:
                - Deployment
      validate:
        message: "Production Deployments must have readinessProbe defined."
        pattern:
          spec:
            template:
              spec:
                containers:
                  - readinessProbe:
                      httpGet:
                        path: "?*"
                        port: "?*"

Match & Exclude

Every rule must define which resources it applies to via match. You can optionally exclude specific resources, namespaces, or label selectors.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-nodeport
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-nodeport
      match:
        any:
          - resources:
              kinds:
                - Service
              # Only match in these namespaces
              namespaces:
                - production
                - staging
      exclude:
        any:
          - resources:
              # Exclude services with this label
              selector:
                matchLabels:
                  allow-nodeport: "true"
          - subjects:
              - kind: ServiceAccount
                name: ci-deployer
                namespace: ci-system
      validate:
        message: "NodePort services are not allowed. Use LoadBalancer or ClusterIP."
        pattern:
          spec:
            type: "!NodePort"
Match Logic: match.any means "if ANY of these conditions match, apply the rule." match.all means "ALL conditions must match." Same logic for exclude. You can combine resource kinds, namespaces, labels, annotations, and subjects (users/service accounts).

Exercises

Exercise 1: Write a ClusterPolicy that requires all Deployments to have at least 2 replicas in the production namespace. Use a validate rule with the pattern spec.replicas and the operator ">=2". Test with kubectl create deployment test --image=nginx --replicas=1 -n production.
Exercise 2: Create a policy that disallows containers running as root (securityContext.runAsNonRoot must be true). Exclude the kube-system namespace. Verify by deploying a pod without runAsNonRoot: true.
Exercise 3: Write a ClusterPolicy that restricts image registries. Only allow images from ghcr.io/myorg/* and docker.io/library/*. Use the validate.deny form with conditions and the !~ (not match) operator on container images.

Key Takeaways & Next Steps

  • Kyverno policies are standard Kubernetes CRDs — kubectl apply to deploy
  • validationFailureAction: Enforce blocks; Audit logs only
  • Pattern matching uses anchors: "?*" (non-empty), "!value" (not equal), =(field) (if exists)
  • ClusterPolicy = cluster-wide; Policy = namespace-scoped
  • Match/Exclude support kinds, namespaces, labels, annotations, and subjects

Next in the Series

In Part 2: Mutate & Generate, we'll write mutate rules to auto-inject labels and sidecars, and generate rules to auto-create NetworkPolicies when namespaces are created.