Skip to main content

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

ToolOpen SourceFull FeaturesNotes
OPA GatekeeperYesYesUses Rego; more powerful but steeper learning curve
jsPolicyYesYesJavaScript-based policies; less adoption
KubewardenYesYesWasm-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.

PolicyImagesMechanismMode
verify-flux-imagesghcr.io/fluxcd/*keyless cosign, fluxcd/*Enforce
verify-vaultwarden-imageghcr.io/dani-garcia/vaultwardenSigstore bundle, release.yml@refs/tags/* + SLSA v1 provenance pinned to dani-garcia/vaultwardenEnforce
verify-home-operations-imagesghcr.io/home-operations/* (the rootless *arr stack; kromgo skipped — unattested)Sigstore bundle + SLSA v1 provenance pinned to home-operations/containersEnforce
verify-platform-imagesExternal Secrets, Crossplane provider-opentofu, KubeVela (keyless, upstream release workflows); cert-manager (KMS key, sha512, no tlog)classic cosignEnforce
verify-k8up-imageghcr.io/k8up-io/k8upkeyless 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-provenanceghcr.io/kyverno/policy-reporter*SLSA v0.2 attestation via slsa-github-generator, source repo pinned by conditionEnforce
verify-cloudnative-pg-provenanceghcr.io/cloudnative-pg/cloudnative-pgsame as above; the image is also every Postgres pod's bootstrap init containerAudit until passes are visible everywhere
verify-johnnycube-imagesghcr.io/johnnycube/*Sigstore bundle, any johnnycube/<repo> workflow on main or a v* tagAudit — only cairn-core signs so far
verify-kubernetes-addon-imagesregistry.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-imagesgitea.web.kueber.eu/johnny/*homelab cosign key (the Flux OCI key), private registry via regcred-docker-giteaAudit — 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, topologySpreadConstraints with DoNotSchedule on 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 Deployment manifest sets revisionHistoryLimit: 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-generated gotk-components.yaml is left alone.
Kubernetes Metadata
  • 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