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 thetendtech.io/applabel — 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-defaultsis a mutate policy: on App CREATE it defaultsspec.ownerfrom{{request.userInfo.username}},spec.expiryfrom a policy-configured TTL, andspec.dataScopesto synthetic-only. Kyverno was chosen over an operator webhook because the creator identity exists only in the admission request'suserInfo(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.