Verified10 commandsAI-assisted

Kyverno

.md

Verified against Official docs — kyverno.io/docs/kyverno-cli, kyverno.io/docs/policy-types/cluster-policy, · official docs

What it is and where it fits 🎯#

Kyverno is a Kubernetes-native policy engine — it validates, mutates, and generates cluster resources at admission time, the same architectural slot OPA/Gatekeeper occupies (both covered in Container & Kubernetes Security's admission control material). The difference that matters day to day: Kyverno policies are written in plain Kubernetes YAML, not Rego — a ClusterPolicy custom resource with a match/validate block reads like any other Kubernetes manifest a platform team already writes, with no separate policy language to learn. Kyverno also does something OPA/Gatekeeper structurally can't do as naturally: it can mutate a non-compliant resource to fix it automatically (inject a missing resource limit, add a required label) instead of only rejecting it outright — and it can generate a whole new resource in response to another one being created (auto-creating a default NetworkPolicy whenever a new Namespace appears).

Kyverno sits alongside OPA and Conftest (both covered in this series) as a third way to do policy-as-code, but at a different layer: OPA/Conftest most commonly validate config files in a CI pipeline (a Terraform plan, a raw Kubernetes YAML before it's ever applied); Kyverno validates/mutates live admission requests as they hit the Kubernetes API server, and — via verifyImages below — can also enforce container image signature policy at that exact same admission boundary.

Where Kyverno sits in the admission-control pipeline#

Diagram

All four rule types (validate, mutate, generate, verifyImages) run through the exact same admission-webhook mechanism — the difference is entirely in what the policy declares it should do once matched.

Installation#

# Install the Kyverno controller into a cluster (Helm — the officially recommended method)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace

# ...or the raw manifest, useful for quick local testing (kind, minikube)
kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.13.0/install.yaml
kubectl -n kyverno wait --for=condition=available --timeout=180s deployment/kyverno-admission-controller

# The Kyverno CLI — for offline policy testing, no cluster required
brew install kyverno                                              # Homebrew
kubectl krew install kyverno                                       # as a kubectl plugin: kubectl kyverno ...
go install github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno@latest

kyverno version

Core concepts#

ConceptWhat it means
ClusterPolicyCluster-wide policy resource — applies to matching resources in every namespace
PolicyThe namespaced equivalent — only applies within the namespace it's created in
validate ruleAccepts or rejects a resource based on a pattern/condition — the "gatekeeper" behavior
mutate rulePatches a resource before it's persisted — Kyverno's standout capability versus Rego-based tools
generate ruleCreates a brand-new resource in reaction to another resource being created (or a source resource changing)
verifyImages ruleVerifies a container image's Cosign signature/attestations before allowing the Pod to admit
validationFailureActionAudit (log only, don't block — for rolling out a new policy safely) vs Enforce (actually reject)

Writing and testing a validate policy#

# disallow-latest-tag.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Images must not use the ':latest' tag — pin an explicit version."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
kubectl apply -f disallow-latest-tag.yaml

# Offline dry-run against a resource file, no cluster admission needed — fast local feedback loop
kyverno apply disallow-latest-tag.yaml --resource notifier-deployment.yaml

kubectl apply -f notifier-deployment.yaml    # if it uses :latest, this is now REJECTED at admission

Sample rejection message a developer actually sees:

Error from server: error when creating "notifier-deployment.yaml": admission webhook "validate.kyverno.svc-fail" denied the request: resource Pod/default/notifier violates policy disallow-latest-tag: require-image-tag: 'validation error: Images must not use the '\'':latest'\'' tag — pin an explicit version. rule require-image-tag failed at path /spec/containers/0/image/'

Tip

Start every new policy with validationFailureAction: Audit, not Enforce. Audit mode logs every would-be violation without blocking anything — it's how you find out a policy would have broken half the cluster's existing deployments before flipping it to Enforce and actually blocking traffic.

Writing a mutate policy — Kyverno's standout feature#

# add-default-resource-limits.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-resource-limits
spec:
  rules:
    - name: default-limits
      match:
        any:
          - resources:
              kinds: ["Pod"]
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - (name): "*"
                resources:
                  limits:
                    =(memory): "256Mi"
                    =(cpu): "500m"
kyverno apply add-default-resource-limits.yaml --resource notifier-deployment.yaml
# shows the RESULTING mutated resource — inspect this before applying to a real cluster

The =(memory) syntax means "set this value only if the field isn't already present" — a Deployment that already declares its own limits is left untouched; one that doesn't gets a sane default injected automatically, rather than being rejected and sent back to the developer to fix by hand.

Writing a generate policy#

# generate-default-networkpolicy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-networkpolicy
spec:
  rules:
    - name: default-deny-new-namespace
      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"]

Every new Namespace created after this policy is active automatically gets a default-deny NetworkPolicy the moment it exists — directly reusing this series' network-segmentation principle from Part 3, but enforced structurally rather than depending on every team remembering to add one themselves.

verifyImages — enforcing Cosign signatures at admission#

# require-signed-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "registry.example.com/checkout-service/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/my-org/checkout-service/.github/workflows/*"
                    issuer: "https://token.actions.githubusercontent.com"

This is the same keyless (Fulcio/Rekor-backed) verification model as cosign verify (its own cheat sheet) — Kyverno just enforces it as an admission gate instead of a manual CI step, so an image that was never signed by the expected GitHub Actions workflow identity is rejected before a Pod running it can ever start, regardless of whether the CI pipeline itself was bypassed some other way.

Testing policies with the Kyverno CLI test framework#

# kyverno-test.yaml
name: disallow-latest-tag-test
policies:
  - disallow-latest-tag.yaml
resources:
  - notifier-deployment.yaml
results:
  - policy: disallow-latest-tag
    rule: require-image-tag
    resource: notifier
    kind: Pod
    result: fail
kyverno test .              # runs every kyverno-test.yaml under the current directory
kyverno test . -v            # verbose, per-rule results

Running kyverno test in CI, against every policy in a platform repo, before those policies are ever promoted to Enforce on a real cluster, is the direct policy-engine equivalent of the "write a denied test case, not just an allowed one" discipline this series' OPA cheat sheet also emphasizes.

Real-world scenario: rolling out policy without an outage#

  • Write the policy with validationFailureAction: Audit and apply it.
  • Run kubectl get policyreport -A (or clusterpolicyreport) after a day of real traffic to see every resource that would have been rejected.
  • Triage the audit findings — fix genuinely non-compliant workloads, or add a documented exclude block for a legitimate exception, before flipping anything to Enforce.
  • Flip validationFailureAction: Enforce only once the audit report is clean (or the remaining violations are explicitly excluded), and monitor admission-controller logs immediately after.

Warning

Flipping straight to Enforce on a brand-new policy against a live production cluster is how an entire team's deploy pipeline gets blocked at 2am. Audit mode exists specifically to de-risk this — skipping it to "save time" is the single most common way a policy rollout turns into an incident.

CI/CD integration recipe — pre-flight policy testing before merge#

# .github/workflows/kyverno-policy-test.yml
name: Kyverno policy tests
on: [pull_request]
jobs:
  kyverno-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Kyverno CLI
        run: |
          curl -LO https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_x86_64.tar.gz
          tar -xzf kyverno-cli_linux_x86_64.tar.gz && sudo mv kyverno /usr/local/bin/
      - name: Run policy tests
        run: kyverno test ./policies/

Common pitfalls#

  • Skipping Audit mode for a new policy. See the WARNING above.
  • Confusing mutate's =(field) conditional-set syntax with an unconditional overwrite. =(memory) only sets a value if absent; omitting the =() wrapper unconditionally overwrites whatever was there, including a value the developer deliberately set.
  • Forgetting synchronize: true on a generate rule when the generated resource should stay in sync with future changes to its source — without it, the generated resource is created once and then left alone even if the policy's template later changes.
  • Assuming kyverno apply (the CLI dry-run) and real cluster admission behave identically for every rule type. generate rules in particular have cluster-side effects the offline CLI can't fully simulate — always confirm behavior against a real (non-production) cluster before enforcing broadly.

When to reach for something else#

For validating config files before they're ever applied — a Terraform plan, a raw Kubernetes manifest in CI, a Dockerfile — reach for Conftest (its own cheat sheet), which is a more ergonomic front end over the same policy-as-code idea for that specific job. For hand-authoring or debugging the underlying Rego a Gatekeeper-based cluster already relies on, reach for OPA directly. If a team is already fully standardized on OPA/Gatekeeper for admission control and doesn't specifically need Kyverno's mutate/generate capabilities, there's limited reason to run both policy engines side by side — pick one per cluster to avoid two independent, potentially-conflicting sources of admission decisions.