# Kyverno Cheat Sheet

> **Tool:** Kyverno
> **Category:** Security & Compliance
> **Verified against:** Official docs — kyverno.io/docs/kyverno-cli, kyverno.io/docs/policy-types/cluster-policy,
> kyverno.io/docs/policy-types/cluster-policy/verify-images — 2026-09-05 (Kyverno isn't installable in
> this sandboxed environment; verified against current official docs rather than a local `--help` run)
> **Official docs:** https://kyverno.io/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](/tutorials/devsecops/03-container-and-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

```mermaid
flowchart TD
    Apply["kubectl apply<br/>(a Pod/Deployment/etc.)"] --> API["Kubernetes API server"]
    API --> Admission{"Kyverno admission<br/>webhook"}
    Admission -->|"validate: fails"| Reject["❌ Request REJECTED —<br/>resource never created"]
    Admission -->|"mutate: applies a patch"| Patched["Resource created,<br/>with Kyverno's fix applied"]
    Admission -->|"generate: triggers"| NewRes["A NEW related resource<br/>is auto-created"]
    Admission -->|"verifyImages: signature check fails"| Reject

    classDef crit fill:#fbe8e6,stroke:#b3261e,color:#10161c
    classDef ok fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    class Reject crit
    class Patched,NewRes ok
    class Admission info
```

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

```bash
# 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

| Concept | What it means |
|---|---|
| **`ClusterPolicy`** | Cluster-wide policy resource — applies to matching resources in every namespace |
| **`Policy`** | The namespaced equivalent — only applies within the namespace it's created in |
| **`validate` rule** | Accepts or rejects a resource based on a pattern/condition — the "gatekeeper" behavior |
| **`mutate` rule** | Patches a resource before it's persisted — Kyverno's standout capability versus Rego-based tools |
| **`generate` rule** | Creates a brand-new resource in reaction to another resource being created (or a source resource changing) |
| **`verifyImages` rule** | Verifies a container image's Cosign signature/attestations before allowing the Pod to admit |
| **`validationFailureAction`** | `Audit` (log only, don't block — for rolling out a new policy safely) vs `Enforce` (actually reject) |

## Writing and testing a validate policy

```yaml
# 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"
```

```bash
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

```yaml
# 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"
```

```bash
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

```yaml
# 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

```yaml
# 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

```yaml
# 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
```

```bash
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

```yaml
# .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.
