Skip to content

ADR-0006: Kyverno as the single admission engine

Status: Accepted — 001-walking-skeleton (2026-06); extended 003-governance-depth (2026-07) Principles served: II, III, V Source: specs/001-walking-skeleton/research.md D-6; specs/003-governance-depth/research.md G-1

Context

The platform needs admission-time enforcement for three distinct jobs: the attestation gate (refuse unattested images), the runtime-contract validations (registry-only images, no embedded secrets, required fields, caps), and governance-field defaulting at App creation. Splitting these across engines would fragment the policy plane and its CI test discipline.

Decision

Kyverno is the one admission engine for all of it:

  • verifyImages (cosign) refuses unattested or unverifiable images in every app namespace the operator provisions — sandbox and production alike, selected by the tendtech.io/app label — at the Kubernetes admission boundary, independent of Argo CD. The label is the selector: in a namespace without it the policy never matches, so the gate does not apply there.
  • Validate ClusterPolicies enforce required App CR fields, registry-only images, embedded-credential rejection, and that every container declares the requests and limits the operator-rendered ResourceQuota/LimitRange then caps numerically — Kyverno checks the declaration, the quota enforces the ceiling. Each ships a violating manifest in kyverno-test format.
  • app-field-defaults is a mutate policy: on App CREATE it defaults spec.owner from {{request.userInfo.username}}, spec.expiry from a policy-configured TTL, and spec.dataScopes to synthetic-only. Kyverno was chosen over an operator webhook because the creator identity exists only in the admission request's userInfo (a reconcile-time default cannot recover it), App admission already lives in Kyverno, and the operator scaffold runs no webhook server — adding cert-manager/webhook infrastructure for one default is a heavier footprint.

Alternatives considered

  • sigstore policy-controller — rejected: single-purpose, a second policy system beside the field validations.
  • Connaisseur — rejected: narrower ecosystem.
  • Verifying only in Argo CD — rejected: bypassable by direct apply, weaker than admission.
  • CEL ValidatingAdmissionPolicy — rejected: native, but splits the policy plane across two engines and loses the kyverno-test CI discipline.
  • Operator mutating webhook for defaults — rejected for the reasons above (userInfo, co-location, webhook-server footprint).

Consequences

One engine covers gate, validation, and defaulting, and every policy is CI-tested against a violating manifest. Enforcement posture is Enforce. The contract validations also run background scans, so pre-existing violators surface as PolicyReports while enforcement bites at the next admission event — no silent grandfathering, no mass eviction. Two policies are admission-only by design and do not background-scan: the attestation gate and the create-time expiry check. Defaults (TTL, scope set) are policy configuration delivered via GitOps, so changing them is a git change, not a code change.