Skip to content

Kyverno

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).

Architecture

flowchart TD
    APIServer[Kubernetes API Server] --> |ValidatingWebhook| KyvernoAdmission[Kyverno\nadmission controller]
    APIServer --> |MutatingWebhook| KyvernoAdmission
    KyvernoAdmission --> |evaluate| Engine[Policy engine]
    Engine --> |ClusterPolicy / Policy| Rules[Validate / Mutate / Generate / VerifyImages]
    subgraph Background controllers
        Scanner[background-controller\naudit scan]
        Generator[generate-controller\nmanage generated resources]
        Updater[update-request-controller]
    end
    Engine --> Scanner
    Engine --> Generator
    Reports[PolicyReport / ClusterPolicyReport] --> Scanner

Kyverno runs four sub-controllers. The admission controller handles real-time enforcement. The background-controller scans existing resources against policies and writes PolicyReport objects. The generate-controller manages resources created by generate rules.

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)

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:

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"

Mutation

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

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

Image verification

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.

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.

Policy exceptions

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.

Testing with kyverno test

The kyverno test CLI validates policies against test fixtures locally:

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

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 Turing-complete)
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.