0075 — Kyverno CLI en CI : valider nos invariants de manifeste en statique
Proposed (2026-06-15).
Fonde sur l’audit
docs/audit/2026-06-15-audit-cncf-kyverno.md
(éventail + revue adversariale, 60 agents, 2 passages) qui a confronté chaque
opportunité CNCF aux garde-fous d’adoption et désigné Kyverno CLI statique en
CI comme le gain le moins cher et le plus défendable. Prolonge la réflexion
ouverte par
note-runtime-admission.md («
en CI (Kyverno CLI) … le candidat “en CI” le plus défendable »).
Cadre l’invariant digest
d’ADR 0006 sans le
superseder : 0006 reste Accepted. Cet ADR précise la portée exacte de
l’épinglage digest (jusqu’ici « idéalement … pour les composants critiques »,
0006 §6 L124) en la rendant
vérifiable en statique, et complète le garde-fou d’exposition
d’ADR 0020 (jusqu’ici vérifié seulement
contre un cluster vivant).
Conforme à la gouvernance
ADR 0057 §6 : tant que cet
ADR est Proposed, aucun code (policies, script lint:kyverno, job CI)
n’est produit. Le passage à Accepted est le signal qui autorise
l’implémentation. Application directe du biais adoptif borné
ADR 0061 : pratique CNCF adoptée
parce que son gain net dépasse le coût de diversité (binaire statique,
offline, zéro composant runtime) et qu’elle ne contredit aucun ADR Accepted.
Contexte
Section intitulée « Contexte »Le dépôt fait reposer la conformité de ses manifestes sur deux mécanismes hétérogènes :
- Trivy
config(IaC statique en CI) — détecte les misconfigurations connues du catalogue KSV. Efficace, mais (a) ne porte pas nos invariants propres, et (b) sa règle de tag-pinning est de gravité LOW/MEDIUM, or la CI ne casse qu’en HIGH/CRITICAL. - Du code impératif maison qui encode nos règles, mais partiellement et
tard :
scripts/audit-image-digests.shvérifie que chaque digest pointe un index multi-arch — mais sa capture (L50) ne matche que le motifimage: …:tag@sha256:…. Une image épinglée par tag seul n’est donc même pas auditée : elle échappe à la fois à Trivy (gravité trop basse) et à l’audit (hors motif). De plus l’audit exige le réseau (il interroge le registre pour lire leMediaType), donc ne tourne pas dans les jobs offline.- L’invariant d’exposition
d’ADR 0020 (« services applicatifs
en ClusterIP ; exposition uniquement par la bordure Gateway ») est codé
en couche 7b de
bootstrap/state.sh(L802-812) et ne s’exécute que contre un cluster vivant (toutes les sections sont gardées parcluster_target_ready, state.sh L106). Untype: LoadBalancerhors-Gateway introduit dansplatform/*.yamln’est donc détecté qu’après déploiement.
L’audit a vérifié sur fichiers que ces deux trous sont réels, pas théoriques :
platform/seaweedfs/seaweedfs.yaml(seaweedfs:4.31) etstorage/ceph/backup/snapshot-cronjob.yaml(bitnami/kubectl:1.34) sont pinnés par tag seul → invisibles pour l’audit digest. Ordre de grandeur : ~43 images digest-pinnées vs ~10 tag-only.platform/mailpit/mailpit.yamlexpose unService type=LoadBalancer(mailpit-smtp) hors-Gateway — une exception réelle et assumée, mais qui n’aurait été visible qu’au run.
Ces invariants sont les nôtres (ils découlent de nos ADR), pas des misconfig
génériques. C’est précisément le créneau de Kyverno en CLI : kyverno apply
évalue des policies maison contre les manifestes avant merge, 100 %
offline, sans déployer aucun composant. Complémentaire de Trivy (qui garde
les misconfig KSV) et de l’audit digest (qui garde « le digest pointe un index
multi-arch ») : Kyverno garantit, lui, que chaque image EST pinnée par
digest et qu’aucun Service interdit n’est exposé.
La question n’est donc pas « adopter Kyverno » au sens large (l’admission sur le
cluster est un autre sujet, runtime, traité ailleurs — cf. « À revoir si »),
mais introduire le binaire kyverno comme maillon de lint statique, au même
titre que kubeconform, yamllint ou shellcheck.
Décision
Section intitulée « Décision »Adopter Kyverno CLI comme étape de lint statique en CI
(pnpm lint:kyverno), portant deux ClusterPolicy maison, exécutées hors-ligne
contre les manifestes versionnés. Aucun déploiement de Kyverno sur le
cluster n’est décidé ici.
1. Deux policies maison, versionnées
Section intitulée « 1. Deux policies maison, versionnées »Les policies vivent dans un répertoire dédié non vendored, p. ex.
platform/policies/ (manifestes maison, donc soumis à prettier/yamllint, à la
différence des bundles vendored exclus).
require-image-digest (validate, failureAction selon §3) — toute image de
containers/initContainers/ephemeralContainers doit matcher *@sha256:*.
Ferme le trou tag-only de
audit-image-digests.sh : la policy
garantit la présence d’un digest ; l’audit garde la vérification que ce
digest pointe un index multi-arch (le banc est arm64,
ADR 0006 §6 L125). Les deux
sont complémentaires, aucun n’est redondant.
restrict-service-exposure (validate) — refuse tout kind: Service de
type LoadBalancer ou NodePort. Porte l’invariant
d’ADR 0020 avant merge, là où la
couche 7b de state.sh
(L802-812) ne le voyait qu’au run. La couche 7b
reste (elle attrape le drift en place, runtime) : l’invariant vit
désormais en deux endroits cohérents — statique avant merge, dynamique sur
cluster.
2. Mécanisme d’exception adapté au statique
Section intitulée « 2. Mécanisme d’exception adapté au statique »⚠️ Point dur identifié par l’audit. L’exception de la couche 7b repose sur
le label gateway.networking.k8s.io/gateway-name
(state.sh L808-809) que Cilium pose à runtime
sur le Service qu’il génère pour un Gateway. Ce label n’existe pas dans les
manifestes versionnés (qui déclarent un kind: Gateway, jamais ce Service) :
le mécanisme d’exception 7b est donc inopérant en statique. La policy doit
employer un mécanisme différent, au choix à l’implémentation :
- une clause
excludeKyverno ciblant nommément les Services exposés assumés, ou - un label maison versionné (p. ex.
expo.cluster/loadbalancer-assume: "<motif>") apposé sur les rares Services d’exposition légitimes.
Les exceptions versionnées connues à couvrir explicitement (vérifiées) :
mailpit-smtp
(platform/mailpit/mailpit.yaml,
exposition SMTP de bordure assumée) et l’exemple
storage/ceph/storageClass/examples/nodeport.yaml. Le Service LoadBalancer
du Gateway lui-même n’est jamais dans les manifestes (généré par Cilium),
donc hors-scope statique — aucun faux positif de ce côté.
3. Portée de l’enforcement : Audit d’abord, Enforce ensuite
Section intitulée « 3. Portée de l’enforcement : Audit d’abord, Enforce ensuite »⚠️ Point dur identifié par l’audit. ADR 0006 formule le digest comme «
idéalement … pour les composants critiques »
(§6 L124) — pas comme une
obligation. Un Enforce immédiat de require-image-digest ferait rougir la
CI sur les ~10 images tag-only assumées (dont les images maison
registry:80/… du banc, et des dépendances upstream pinnées par tag).
Cet ADR tranche la portée : l’invariant digest devient obligatoire pour
les manifestes versionnés, avec une liste d’exceptions explicite et
justifiée (même esprit que .trivyignore.yaml : allowlist par chemin avec
motif). Mise en œuvre en deux temps :
- policy en
Audit(rapport, CI verte) → on inventorie et on justifie chaque image tag-only restante (la corriger en digest, ou l’inscrire à l’allowlist) ; - bascule en
Enforce(CI rouge sur violation) une fois l’allowlist stabilisée.
restrict-service-exposure peut viser Enforce d’emblée (les 2 exceptions sont
connues et finies).
4. Intégration toolchain : un maillon de plus, pas une toolchain de plus
Section intitulée « 4. Intégration toolchain : un maillon de plus, pas une toolchain de plus »Nouveau script lint:kyverno dans package.json, ajouté
à la chaîne lint, calqué sur lint:k8s (même sélection git ls-files -z,
mêmes exclusions vendored :!: — ne pas dupliquer la liste mais la
factoriser avec lint:k8s pour éviter deux listes à maintenir). Le binaire
kyverno rejoint kubeconform/yamllint dans l’environnement CI. Job CI :
statique, offline, sans cluster — il tourne dans le même pnpm lint que le
reste (≠ jobs runtime). Conforme à la doctrine « un outil par action »
(ADR 0049) : Kyverno CLI = l’outil de
nos invariants de manifeste ; Trivy garde les misconfig KSV ; kubeconform
garde la conformité de schéma. Pas de recouvrement.
Conséquences
Section intitulée « Conséquences »Bénéfices.
- Deux trous de conformité réels fermés avant merge, prouvés sur fichiers (tag-only invisible ; exposition vérifiée seulement au run).
- Zéro composant runtime :
kyverno applyest un binaire de lint, offline, sans empreinte cluster ni charge opérationnelle — argument central sur un cluster non-HA mono-admin (ADR 0029 pour la philosophie « charge = prix à payer » ; ici le prix est nul côté runtime). - Nos invariants deviennent exécutables et tracés plutôt que dispersés entre un script réseau-dépendant et une couche bash runtime.
- Réversible : retirer une étape de lint est trivial (≠ désinstaller un composant cluster).
Coûts assumés.
- Un binaire de plus dans la CI (
kyverno) à épingler/maintenir dans la matrice de versions (ADR 0006). Coût de diversité réel mais borné (outil statique, pas de runtime). - Une liste d’exceptions à tenir (images tag-only assumées, Services
exposés) — comme
.trivyignore.yaml, avec justification par entrée. Dette de maintenance modeste et explicite. - Travail de cadrage préalable (phase
Audit) avant de pouvoir basculer enEnforcesans casser la CI sur l’existant.
Validation (à produire).
pnpm lint:kyvernovert sur l’état courant en modeAudit, avec un rapport listant les images tag-only et les Services exposés actuels.- Un manifeste de test introduisant une image tag-only et un
Service type=LoadBalancernon exempté →lint:kyvernorouge une fois enEnforce(preuve que le garde-fou mord). - Idempotence/reproductibilité : le lint donne le même verdict en local et en
CI, sans réseau (≠
audit-image-digests.sh).
À revoir si
Section intitulée « À revoir si »- L’admission sur le cluster est priorisée (Kyverno comme webhook refusant
les manifestes non conformes à l’
apply, et/ouverifyImagespour la signature) : c’est un composant runtime, donc un autre ADR, à traiter avec la Phase 7.2 (signature d’images cosign/Sigstore) pour décider Kyverno-admission vs policy-controller une seule fois — cf.note-runtime-admission.md. Le présent ADR ne décide que le volet CLI/CI. - Le nombre de policies maison croît au point de justifier un PolicyReport / policy-reporter : réévaluer alors le passage en admission (où ces rapports prennent leur sens), pas en CLI.
- ADR 0006 est révisé sur la portée digest : réaligner l’allowlist de
require-image-digest.
Alternatives écartées
Section intitulée « Alternatives écartées »Ne rien ajouter (garder uniquement Trivy + scripts maison). Statu quo. Écarté : les deux trous (tag-only, exposition runtime-only) sont vérifiés et réels ; les laisser ouverts contredit la posture qualité du dépôt alors que le coût de fermeture est faible et sans runtime.
Étendre audit-image-digests.sh au cas tag-only en bash. Possible, mais (a)
ne couvre que le digest, pas l’exposition ; (b) reste un script maison de
plus là où une policy déclarative est plus lisible et réutilisable (le même
binaire porte les deux invariants et les suivants). Surtout, l’audit a montré
que restrict-service-exposure justifie à lui seul d’introduire Kyverno CLI
— une fois le binaire là, require-image-digest est amorti gratuitement.
Deux scripts bash distincts coûteraient plus en dispersion qu’une étape Kyverno
unique (ADR 0049).
Déployer Kyverno en admission directement. Plus puissant (refus à
l’apply). Écarté ici : c’est un composant runtime stateful (webhook) sur
un cluster non-HA mono-admin, structurant, qui recoupe la Phase 7.2 — à
décider une seule fois, pas en doublon. Le volet CLI livre l’essentiel du gain
(garde-fou avant merge) sans ce coût. L’admission reste un palier ultérieur
assumé, pas un préalable.
Forcer le digest en Enforce immédiat. Écarté : romprait la CI sur les ~10
images tag-only assumées sans cadrage préalable, et survaloriserait la
formulation « idéalement » d’ADR 0006. La séquence Audit → allowlist →
Enforce (§3) adopte l’invariant proprement.