devops

Kyverno Policy-as-Code: Enforce Kubernetes Guardrails Without Rego

A hands-on Kyverno policy-as-code guide: install it, write validate/mutate/generate policies, run audit vs enforce, and test policies in CI before they block prod.

July 18, 2026·8 min read·
#kubernetes#security#policy-as-code#kyverno#platform-engineering#devops

What Kyverno Actually Does

Kyverno is a Kubernetes admission controller that enforces your rules the moment a resource is created or updated — before it ever reaches the cluster. Unlike OPA/Gatekeeper, you don't learn Rego. Policies are plain Kubernetes YAML, so anyone who can read a Deployment manifest can read a policy. That single design choice is why platform teams adopt it faster than any other policy engine.

Kyverno does three things: validate (block or warn on non-compliant resources), mutate (inject or patch fields automatically), and generate (create companion resources like default NetworkPolicies). This guide walks through all three with runnable manifests, plus the two operational details that decide whether policy-as-code helps or causes an outage: Audit vs Enforce mode, and testing policies in CI before they touch production.

If you already run RBAC to control who can act, Kyverno controls what they can create. The two are complementary — see the Kubernetes RBAC deep dive for the access side of the equation.

Install Kyverno

Install via the official Helm chart. For production, run it in high-availability mode (3 replicas of the admission controller) so a single node drain doesn't stall every deployment in the cluster:

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno \
  --namespace kyverno --create-namespace \
  --set admissionController.replicas=3 \
  --set backgroundController.replicas=2

Verify the controllers are running:

kubectl get pods -n kyverno
kubectl get crd | grep kyverno.io

You now have ClusterPolicy (cluster-wide) and Policy (namespaced) resource types available. Everything below is a ClusterPolicy.

Validate: Block Bad Resources at Admission

The most common use case: stop non-compliant workloads from being created. This policy rejects any Pod that requests the :latest image tag — the single most common cause of "it worked yesterday" incidents, because latest is mutable and gives you no way to roll back to a known image.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Using ':latest' or an untagged image is not allowed."
        pattern:
          spec:
            containers:
              - image: "!*:latest"

The pattern block is a template the resource must match. !*:latest means "anything except a string ending in :latest". Apply it and try to create a pod with nginx:latest — the API server rejects it with your custom message.

A second validation everyone needs: require CPU and memory limits. Unbounded pods are the top cause of node-level OOM kills and noisy-neighbor throttling. This is the enforcement counterpart to understanding OOMKilled exit code 137:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
spec:
  validationFailureAction: Audit
  rules:
    - name: validate-resources
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "CPU and memory requests and limits are required."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"
                  limits:
                    memory: "?*"

?* means "any non-empty value must be present." Note this one is set to Audit, not Enforce — more on why in the next section.

Audit vs Enforce: The Setting That Prevents Self-Inflicted Outages

validationFailureAction has two values, and choosing wrong is how teams take down their own deployment pipeline:

  • Audit — non-compliant resources are allowed, but a PolicyReport records the violation. Nothing breaks. Use this when you roll out a new policy.
  • Enforce — non-compliant resources are rejected at the API. Use this only after audit reports show the cluster is already clean.

Never ship a new Enforce policy straight to production. A require-resource-limits policy in Enforce mode will reject every Deployment, DaemonSet, and Helm upgrade that lacks limits — including your monitoring stack and ingress controller. Ship it as Audit, then read the reports:

# See every violation across the cluster
kubectl get policyreport -A

# Drill into one namespace
kubectl get policyreport -n production -o wide

# Count failures by policy
kubectl get clusterpolicyreport -o json | \
  jq -r '.results[] | select(.result=="fail") | .policy' | sort | uniq -c

Once the fail count for a policy hits zero, flip it to Enforce. This audit-first workflow is the difference between policy-as-code as a safety net and policy-as-code as an outage generator.

Mutate: Fix Resources Automatically Instead of Rejecting Them

Rejection creates friction. Often the better move is to fix the resource silently. Mutation policies patch incoming manifests before they're persisted. This one adds a default runAsNonRoot security context and drops all Linux capabilities — turning insecure-by-default pods into hardened ones without asking developers to remember the boilerplate:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-securitycontext
spec:
  rules:
    - name: set-runasnonroot
      match:
        any:
          - resources:
              kinds: ["Pod"]
      mutate:
        patchStrategicMerge:
          spec:
            securityContext:
              runAsNonRoot: true
              seccompProfile:
                type: RuntimeDefault
            containers:
              - (name): "*"
                securityContext:
                  allowPrivilegeEscalation: false
                  capabilities:
                    drop: ["ALL"]

The (name): "*" syntax is an anchor — it means "for every container in the list, apply the following patch." Mutation is how platform engineers ship paved-road defaults: developers write minimal manifests, and the platform fills in the safe settings. That philosophy is the heart of platform engineering as a product.

Generate: Create Companion Resources On Demand

The generate rule creates new resources when a trigger appears. A classic pattern: every new namespace should get a default-deny NetworkPolicy so pods can't talk to each other unless explicitly allowed. Kyverno can generate it automatically the instant a namespace is created:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: default-deny-networkpolicy
spec:
  rules:
    - name: add-default-deny
      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", "Egress"]

synchronize: true means if someone deletes or edits the generated NetworkPolicy, Kyverno restores it. This closes a gap RBAC alone can't: it's not about who can create resources, it's about guaranteeing a secure baseline exists everywhere.

Verify Image Signatures (Supply Chain)

Kyverno can enforce that only signed images run in the cluster — the admission-time half of a supply-chain story whose other half (signing in the pipeline) is covered in Argo CD 3.5 supply chain security:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-cosign-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"

This uses cosign keyless verification: only images built and signed by your GitHub Actions workflows pass admission. An attacker who pushes a malicious image to your registry can't get it scheduled, because it won't carry a valid signature tied to your CI identity.

Test Policies in CI Before They Reach the Cluster

The single practice that separates mature policy-as-code from cargo-culting: test your policies against sample manifests in CI. The Kyverno CLI runs the same admission logic locally, so you catch a broken policy in a pull request instead of in a broken deploy.

Create a test file that pairs policies with resources and expected outcomes:

# kyverno-test.yaml
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
  name: guardrails-test
policies:
  - policies/disallow-latest-tag.yaml
resources:
  - test/pod-good.yaml
  - test/pod-bad.yaml
results:
  - policy: disallow-latest-tag
    rule: require-image-tag
    resource: good-pod
    result: pass
  - policy: disallow-latest-tag
    rule: require-image-tag
    resource: bad-pod
    result: fail

Run it locally and in the pipeline:

kyverno test .

Wire that into a GitHub Actions job so no policy change merges without passing its own tests — the same shift-left discipline you'd apply to any CI/CD pipeline with GitHub Actions. A policy that silently stops matching resources is worse than no policy, because it gives you false confidence. Tests catch that regression.

A Sane Rollout Order

Don't apply twenty policies on day one. Roll out in this order to build trust and avoid breakage:

  1. Deploy Kyverno in HA mode. A single-replica admission controller is a cluster-wide single point of failure — if it's down and failurePolicy is Fail, deployments hang.
  2. Ship every new policy as Audit first. Let it run for a week. Read the PolicyReports.
  3. Fix existing violations surfaced by the reports, or scope the policy with exclude for legacy namespaces you can't fix yet.
  4. Flip to Enforce only once the fail count is zero.
  5. Add CLI tests for each policy and gate merges on them.

Kyverno's payoff is that guardrails become code: reviewed in pull requests, versioned in Git, and enforced automatically instead of living in a wiki page nobody reads. Combined with Kubernetes security best practices, it turns "we have a policy" from a statement of hope into a property the cluster actually guarantees.

#kubernetes#security#policy-as-code#kyverno#platform-engineering#devops
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →