Kyverno
Kubernetes-native policy engine for validation, mutation, and generation of resources.
Kyverno is a CNCF policy engine designed for Kubernetes. It uses admission webhooks to enforce, mutate, and generate Kubernetes resources based on declarative ClusterPolicy and Policy rules written in YAML. It is used in this cluster to enforce security standards (e.g. require securityContext, block privileged containers), auto-generate resources (e.g. default NetworkPolicies, pull secrets), and mutate resources at admission time.
Alternatives considered
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| OPA Gatekeeper | Yes | Yes | Uses Rego; more powerful but steeper learning curve |
| jsPolicy | Yes | Yes | JavaScript-based policies; less adoption |
| Kubewarden | Yes | Yes | Wasm-based; policies in multiple languages |
Installation
Architecture
HelmRelease kyverno in namespace kyverno, consuming the chart through chartRef from an OCIRepository at oci://ghcr.io/kyverno/charts/kyverno. CRDs installed via chart (crds.install: true). High-availability setup: admission controller (3 replicas), background controller (2 replicas), cleanup controller (2 replicas). Background scan enabled with 23h interval and 2 workers. PolicyException resources enabled in the kyverno namespace.
Security
RBAC is cluster-wide (admission controller requires watching all resource types). Webhook timeout set to 5 seconds. No custom pod securityContext in the HelmRelease values; chart defaults apply. The admission controller itself enforces security policies on all other workloads.
Updates
Managed by Renovate, which bumps the OCIRepository tag. There is no verify block on this source: the Kyverno chart carries no cosign signature (unlike Policy Reporter from the same organisation), so Flux has nothing to check — the engine that verifies everyone else's images is itself installed on trust in ghcr.io.
Usage
Cluster operators create ClusterPolicy resources to define admission rules. Policies can validate (block non-conforming resources), mutate (inject defaults), or generate (create derived resources). PolicyException resources in the kyverno namespace allow exempting specific workloads from policy rules. Background scanning continuously checks existing resources against policies.
Image verification
The ClusterPolicy objects under k8s/platform/configs/talos/kyverno/cosign/ use verifyImages with Sigstore identities (GitHub Actions OIDC + Rekor) or published public keys. Every identity was read from the signature of the digest actually running when the policy was written, not from upstream docs. background: false on all of them: they fire at admission only, never against pods already running, so a signature-lookup outage can block a rollout but never evicts anything.
| Policy | Images | Mechanism | Mode |
|---|---|---|---|
verify-flux-images | ghcr.io/fluxcd/* | keyless cosign, fluxcd/* | Enforce |
verify-vaultwarden-image | ghcr.io/dani-garcia/vaultwarden | Sigstore bundle, release.yml@refs/tags/* + SLSA v1 provenance pinned to dani-garcia/vaultwarden | Enforce |
verify-home-operations-images | ghcr.io/home-operations/* (the rootless *arr stack; kromgo skipped — unattested) | Sigstore bundle + SLSA v1 provenance pinned to home-operations/containers | Enforce |
verify-platform-images | External Secrets, Crossplane provider-opentofu, KubeVela (keyless, upstream release workflows); cert-manager (KMS key, sha512, no tlog) | classic cosign | Enforce |
verify-k8up-image | ghcr.io/k8up-io/k8up | keyless cosign, release.yml@refs/tags/v* | Audit — v2.16.0+ ships a cosign v3 DSSE bundle that Kyverno 1.19 cannot find on ghcr (kyverno#16664); Enforce would freeze k8up at v2.15.0 |
verify-policy-reporter-provenance | ghcr.io/kyverno/policy-reporter* | SLSA v0.2 attestation via slsa-github-generator, source repo pinned by condition | Enforce |
verify-cloudnative-pg-provenance | ghcr.io/cloudnative-pg/cloudnative-pg | same as above; the image is also every Postgres pod's bootstrap init container | Audit until passes are visible everywhere |
verify-johnnycube-images | ghcr.io/johnnycube/* | Sigstore bundle, any johnnycube/<repo> workflow on main or a v* tag | Audit — only cairn-core signs so far |
verify-kubernetes-addon-images | registry.k8s.io/kube-state-metrics (CoreDNS/metrics-server are in kube-system, control plane is static pods) | keyless cosign, Kubernetes release tooling (krel-*@k8s-releng-prod, Google issuer) | Enforce |
verify-gitea-johnny-images | gitea.web.kueber.eu/johnny/* | homelab cosign key (the Flux OCI key), private registry via regcred-docker-gitea | Audit — no app workflow signs yet |
Two namespaces are outside the webhook by design — kube-system and kyverno — so signed images there (Tetragon, CoreDNS, metrics-server, Kyverno itself) cannot be enforced without widening the webhook's namespaceSelector, which is a deliberate non-goal: a Kyverno outage must never be able to block CNI or the policy engine's own pods.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/platform/kyverno/talos/README.md.
Cluster Deployment
Kyverno — Talos cluster
Deviations from defaults
- HA controllers spread across nodes (
kyverno-values.yaml,topologySpreadConstraintswithDoNotScheduleon admission/background/cleanup) — the chart's anti-affinity is only preferred and both admission replicas landed on the same worker; the spread still allows 2+1 when a worker is down. - Reports controller honours
resourceFilters(features.backgroundScan.skipResourceFilters: false) — with the chart default, 927 of 1622 PolicyReports were ReplicaSets (808 zero-replica rollout history) and every policy change rescanned them all (4 cores for ~11 min on 2026-08-29). Deployments and Pods cover the same pod templates. - Reports controller emits no PolicyViolation Events (
reportsController.featuresOverride.omitEvents) — background rescans re-emitted every Audit failure as an Event (122k in the API server); Policy Reporter reads PolicyReports. The admission controller keeps its Events. - Every raw
Deploymentmanifest setsrevisionHistoryLimit: 2(repo-wide, 2026-08-29) — the default 10 kept ~800 dead ReplicaSets around, which every controller that lists ReplicaSets (Kyverno included) paid for. Helm-chart-managed Deployments keep their chart's value, and Flux's bootstrap-generatedgotk-components.yamlis left alone.
- HelmRelease:
kyverno@3.9.1 - HelmRepo:
kyverno(oci://ghcr.io/kyverno/charts/kyverno)
Rendered manifests (kustomize build)
apiVersion: v1
data:
values.yaml: |
# CRD installation moved to dedicated block
crds:
install: true
admissionController:
replicas: 3
container:
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
# Webhook timeout is now usually nested under webhooks
webhooks:
timeoutSeconds: 5
# The chart's default anti-affinity is only *preferred* (weight 1) and the
# scheduler ignored it — replicas ended up on the same worker. A spread
# constraint with DoNotSchedule forces one pod per node while there are
# enough nodes, and still allows 2+1 when a worker is down (a hard
# anti-affinity would leave a replica Pending instead).
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/component: admission-controller
backgroundController:
replicas: 2 # Recommended for HA
container:
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
# The chart's default anti-affinity is only *preferred* (weight 1) and the
# scheduler ignored it — replicas ended up on the same worker. A spread
# constraint with DoNotSchedule forces one pod per node while there are
# enough nodes, and still allows 2+1 when a worker is down (a hard
# anti-affinity would leave a replica Pending instead).
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/component: background-controller
cleanupController:
replicas: 2 # Recommended for HA
container:
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
# The chart's default anti-affinity is only *preferred* (weight 1) and the
# scheduler ignored it — replicas ended up on the same worker. A spread
# constraint with DoNotSchedule forces one pod per node while there are
# enough nodes, and still allows 2+1 when a worker is down (a hard
# anti-affinity would leave a replica Pending instead).
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/component: cleanup-controller
reportsController:
featuresOverride:
# Background rescans re-emit every Audit failure as a PolicyViolation
# Event (3.8k Events per rescan, 122k in the API server on 2026-08-29).
# Policy Reporter reads PolicyReports, not Events, and Enforce denials
# still come from the admission controller, whose Events stay enabled.
omitEvents:
eventTypes:
- PolicyApplied
- PolicySkipped
- PolicyViolation
features:
backgroundScan:
enabled: true
backgroundScanWorkers: 2
backgroundScanInterval: 23h
# Chart default (true) makes the reports controller ignore Kyverno's own
# resourceFilters, which exclude ReplicaSets. With it on, 927 of 1622
# PolicyReports were ReplicaSets (808 of them zero-replica rollout
# history) and every policy change rescanned all of them — a 4-core,
# 11-minute burst on 2026-08-29. Deployments and Pods cover the same pod
# templates, so nothing is lost.
skipResourceFilters: false
policyExceptions:
enabled: true
namespace: "kyverno"
kind: ConfigMap
metadata:
name: kyverno-values-hg5k9dt8gm
namespace: kyverno