Skip to content

ADR 037 — Trois valeurs sont tournĂ©es, le stock du bucket de state est purgĂ© et bornĂ© (2026-09-09)

Statut

Accepté, écrit le 2026-09-09. Issue #199, Sprint 7 (Remédiation & Fiabilité).

Cette ADR tranche le stock : les copies du state Terraform déjà écrites dans S3, que le versioning conserve sans limite. Elle est la suite de l'ADR 033 (Terraform n'écrit plus les sept secrets) et de l'ADR 034 (Terraform n'écrit plus les trois clés d'accÚs CI), qui traitent toutes deux le flux.

Elle ferme la derniÚre case ouverte du « Done when » de #198 : « le sort des anciennes versions du state est tranché, par écrit ».

Elle est écrite sur des mesures du 2026-09-09, pas sur celles du 2026-09-01 qui figurent dans l'issue. Les deux diffÚrent, et la section suivante dit pourquoi.

Contexte

Ce que le versioning conserve

Le bucket yk-devops-terraform-state a le versioning activĂ© et aucune rĂšgle de cycle de vie — vĂ©rifiĂ© le 2026-09-09, GetBucketLifecycleConfiguration rĂ©pond NoSuchLifecycleConfiguration. Chaque apply remplace la version courante et conserve la prĂ©cĂ©dente indĂ©finiment.

Le chiffrement est AES256 (SSE-S3), pas KMS : aucune barriÚre de clé ne se superpose aux droits S3, toute identité autorisée sur l'objet déchiffre automatiquement.

Les chiffres de l'issue sont faux, et l'erreur est instructive

L'issue #199 annonce 133 versions sur fastapi-eks/persistent/terraform.tfstate et l'ADR 034 en annonce 756 sur le bucket. La mesure du 2026-09-09 en trouve 49 sur cette clé.

Une version S3 ne disparaßt pas toute seule : c'est le compte qui était faux, pas le stock qui a fondu. --prefix fastapi-eks/persistent/terraform.tfstate est un préfixe, pas un nom : il matche aussi terraform.tfstate.tflock, le fichier de verrou que Terraform écrit puis supprime à chaque opération.

La reconstitution est exacte : au 2026-09-01, la clé portait 37 versions de state et le verrou 96, soit les 133 annoncées. Et les « 96 delete markers » de l'issue sont exactement les 96 suppressions de verrou.

Un verrou n'est pas un state. Il ne porte aucune valeur, il fait juste du nombre. Compter les deux ensemble gonfle le constat et, pire, rend la vérification d'aprÚs-purge ininterprétable.

Ce que porte réellement le bucket, au 2026-09-09

list-object-versions sur le préfixe fastapi-eks/ : 936 versions et 333 delete markers, répartis ainsi.

Clé Versions dont > 20 Ko Volume Plus ancienne Plus récente
ephemeral/terraform.tfstate 513 401 24,2 Mo 2026-05-12 2026-09-06
persistent/terraform.tfstate 49 42 3,5 Mo 2026-05-12 2026-09-06
main/terraform.tfstate 31 25 1,2 Mo 2026-05-10 2026-05-11
vpc/, eks/, rds/, ecr/, iam/ 22 2 0,2 Mo 2026-05-06 2026-05-10
verrous .tflock (7 clés) 320 0 0,07 Mo 2026-05-06 2026-09-06

Deux choses que ni l'issue ni les ADR 033 et 034 ne mentionnent.

Les six prĂ©fixes hĂ©ritĂ©s. main/, vpc/, eks/, rds/, ecr/, iam/ sont les states d'avant la dĂ©coupe persistent / ephemeral, de mai 2026. Ils sont logiquement supprimĂ©s — un delete marker est posĂ© sur chacun — mais leurs 53 versions restent intĂ©gralement lisibles. La plus grosse version de main/ (2026-05-10, 67 Ko) porte un aws_db_instance et un aws_iam_access_key. Un objet supprimĂ© dans un bucket versionnĂ© n'est pas un objet effacĂ©.

Les verrous représentent un tiers du volume en nombre et zéro en valeur. Toute rÚgle de cycle de vie les traitera au passage, c'est un gain propre.

La parade 2 de #198 est appliquée, la case est décochée à tort

Vérification faite le 2026-09-09 sur le fichier, selon le critÚre de docs/aws-setup.md §14, et non sur un plan. La version courante de persistent (serial 49, 2026-09-06) porte :

  • 0 random_password, 0 aws_secretsmanager_secret_version, 0 aws_iam_access_key ;
  • aucun output de valeur sensible, section outputs relue en entier.

La stack ephemeral porte 0 ressource : le cluster est détruit.

Le flux est donc arrĂȘtĂ© sur persistent, et il est arrĂȘtĂ© en vrai. Le stock est tout ce qui reste, et rien ne bloque plus sa purge.

Sur les 12 valeurs du stock, 3 sont encore vivantes

C'est la mesure qui commande la dĂ©cision, et elle rĂ©pond Ă  la question posĂ©e par l'issue — « combien des versions sont rĂ©ellement distinctes en valeurs ? » — en la reformulant : la bonne question n'est pas combien de copies, mais combien de valeurs encore valides.

Méthode, sans afficher aucune valeur : empreinte SHA-256 de chaque valeur portée par la version du 2026-09-01 17:38 (la derniÚre avant la parade), comparée à l'empreinte de la variable GitLab qui alimente la repose.

Variable Secret visé Verdict
TF_VAR_db_password app:DB_PASSWORD différente, tournée
TF_VAR_slack_webhook_url alertmanager-slack encore vivante
TF_VAR_argocd_admin_bcrypt argocd:admin-password-bcrypt encore vivante
TF_VAR_argocd_gitlab_client_secret argocd et grafana encore vivante

Le critÚre discrimine : trois empreintes collent, une ne colle pas. Une comparaison qui aurait tout déclaré identique, ou tout déclaré différent, n'aurait rien prouvé (critere-qui-ne-discrimine-pas).

L'explication est dans scripts/seed-secrets.sh, qui fait deux choses différentes selon la valeur :

  • il en regĂ©nĂšre par openssl rand -hex (gen 16, gen 32) : app:SECRET_KEY, les six valeurs de dev / staging / prod, le mot de passe admin Grafana. Neuf valeurs tournĂ©es le 2026-09-02 ;
  • il en repose Ă  l'identique depuis les variables CI : les trois du tableau.

Ce n'est pas une découverte isolée : le cockpit le notait déjà le 2026-09-02 (« quatre rotations demandent une interface tierce », le secret OIDC comptant pour deux puisqu'il vit dans deux secrets). La mesure du jour confirme la note sans l'avoir consultée, ce qui est la meilleure raison de la croire.

La porte d'entrée est fermée, la porte de derriÚre est ouverte

Le mĂȘme jour, sur le mĂȘme compte, avec la mĂȘme identitĂ© user/iamadmin :

  • GetSecretValue sur les sept secrets : refusĂ© sept fois sur sept. C'est la politique de ressource deny by default de #193, et elle fonctionne ;
  • les mĂȘmes valeurs, en clair, dans 42 anciennes versions du state persistent : lisibles sans obstacle.

Le contrÎle de #193 ferme un chemin et laisse l'autre ouvert. C'est la forme exacte du constat de #198, mesurée sur le stock au lieu du flux.

L'asymétrie entre persistent et ephemeral

persistent : le flux est arrĂȘtĂ©. Une purge ramĂšne le stock Ă  zĂ©ro, et c'est dĂ©finitif.

ephemeral : le flux ne peut pas ĂȘtre arrĂȘtĂ©. La CI applique cette stack, elle lit donc son state, et ce state porte le mot de passe master RDS (aws_db_instance.password) par construction du provider. La parade 1 de #198 (!343) a scopĂ© la policy terraform_ci au prĂ©fixe fastapi-eks/ephemeral/*, c'est-Ă -dire exactement le prĂ©fixe qui reste exposĂ© — le commentaire de terraform/modules/iam/main.tf le dit dĂ©jĂ  en toutes lettres.

Sur ephemeral, aucune purge n'atteint zĂ©ro : le prochain apply réécrit la valeur. La seule chose dĂ©cidable est la taille de la fenĂȘtre.

Décision

1. Les trois valeurs vivantes sont tournées, avant la purge

Des trois issues nommĂ©es par #199 — tourner, purger, accepter — les deux premiĂšres sont retenues, dans cet ordre, et pour des raisons diffĂ©rentes.

La rotation traite ce que la purge ne peut pas atteindre : une copie déjà sortie du bucket. La purge la rendrait invisible chez AWS sans la rendre inoffensive.

Les trois gestes, chacun dans une interface tierce :

Valeur OĂč la tourner Ce que ça casse
slack-webhook-url Slack, nouveau webhook entrant Alertmanager jusqu'au prochain montage
gitlab-oidc-client-secret GitLab, application OIDC SSO ArgoCD et Grafana
admin-password-bcrypt htpasswd -nbBC 10, local Connexion admin locale ArgoCD

Puis la variable CI est mise à jour, et scripts/seed-secrets.sh --seed --force repose la valeur. Les deux drapeaux, pas --force seul : sans --seed le script ne sait pas quoi faire et sort son aide avec un code 2. Et --seed sans --force ne réécrit jamais une valeur déjà posée (§14). Aucun cluster n'est nécessaire cÎté AWS : Secrets Manager vit dans persistent et survit au teardown. ESO resynchronise au montage suivant.

--force réécrit les douze valeurs, pas seulement les trois. Les mots de passe PostgreSQL de dev, staging et prod changent donc aussi. C'est sans conséquence : le Job de bootstrap DB rejoue à chaque montage et repose les users avec les nouvelles valeurs.

Un piÚge de l'outil, corrigé en séance : joué avec SECRETS_READER_ROLE_ARN, le script sortait ABANDON à sa derniÚre ligne aprÚs avoir posé les sept valeurs. Son exec de re-vérification retentait l'assume-role depuis le rÎle déjà assumé. Un contrÎle qui échoue pour une raison étrangÚre à ce qu'il mesure, au pire endroit possible. C'est #217, corrigé par !402.

Le webhook Slack est le credential, pas un identifiant qui en dĂ©signe un : qui l'a poste dans le canal. Le secret OIDC l'est aussi. Le bcrypt est un hash, donc attaquable hors ligne et non rejouable tel quel : c'est le moins urgent des trois, et il est traitĂ© avec eux parce que le geste est le mĂȘme.

Les neuf valeurs déjà tournées le 2026-09-02 ne sont pas retouchées. Les retourner ne retirerait rien et coûterait un montage.

2. Deux gestes sur le stock, pas un seul

Geste A — purge one-shot des versions non courantes, sur les huit prĂ©fixes, verrous compris. Elle traite le stock constituĂ©. Elle est irrĂ©versible.

Geste B — rĂšgle de cycle de vie NoncurrentVersionExpiration Ă  30 jours, posĂ©e en code dans terraform/backend/main.tf, sur le bucket entier, avec l'expiration des delete markers devenus orphelins et l'abandon des uploads multipart incomplets.

Le geste A sans le geste B laisse ephemeral se reconstituer à l'identique en quelques semaines : 513 versions se sont accumulées en quatre mois.

⚠ Correction du 2026-09-09, aprĂšs la pose du code (!403)

Cette ADR a d'abord écrit que « le geste B sans le geste A laisse les versions de mai lisibles encore trente jours ». C'est faux, et l'erreur porte sur la sémantique de S3, pas sur la décision.

noncurrent_days compte Ă  partir du moment oĂč une version est devenue non courante, c'est-Ă -dire du jour oĂč l'apply suivant l'a remplacĂ©e. Il ne compte pas Ă  partir de la pose de la rĂšgle. Une version de mai est non courante depuis plus de cent jours : elle part au premier passage du cycle de vie, sous 24 Ă  48 heures.

Conséquence, mesurée le 2026-09-09 sur les 1269 objets du préfixe fastapi-eks/ (versions et delete markers confondus) :

Objets
Non courants 1249
Supprimés dÚs le premier passage 958, soit 20,8 Mo
Survivants, devenus non courants il y a moins de 30 jours 291

C'est donc l'apply du geste B qui est le geste irréversible, pas le geste A. Le geste A ne porte plus que sur les 291 objets récents, et il perd son caractÚre décisif : ces 291 objets ne contiennent plus aucune valeur vivante depuis l'exécution de l'étape 1 (rotation), le 2026-09-09.

Ce que l'erreur ne change pas : la dĂ©cision, l'ordre, et le fait qu'il faille les deux gestes. Ce qu'elle change : oĂč se situe le point de non-retour. Le croire plus loin qu'il n'est, c'est appliquer une rĂšgle en pensant cadrer l'avenir alors qu'on purge le passĂ© dans la foulĂ©e.

A traite le passé, B borne l'avenir, et aucun des deux ne remplace l'autre.

3. Les six prĂ©fixes hĂ©ritĂ©s sont purgĂ©s entiĂšrement, sans fenĂȘtre

main/, vpc/, eks/, rds/, ecr/, iam/ sont des states morts : plus aucun code du dépÎt ne les référence, un delete marker est déjà posé sur chacun, et la derniÚre activité date du 2026-05-11.

Leurs 53 versions sont supprimĂ©es sans conserver de fenĂȘtre de trente jours : il n'existe aucun scĂ©nario oĂč l'on reviendrait Ă  un state d'une architecture qui n'existe plus. C'est la seule partie du stock oĂč la dĂ©cision ne coĂ»te rien.

Ce point est devenu sans objet, pour la raison Ă©crite dans l'encadrĂ© du point 2 : ces 53 versions datent toutes de mai, elles sont non courantes depuis plus de cent jours, et la rĂšgle du geste B les emporte dĂšs son premier passage. Il n'y avait pas de fenĂȘtre Ă  leur refuser. Le point est conservĂ© plutĂŽt que supprimĂ© : il documente une distinction qu'on a cru devoir faire.

4. Trente jours, et la perte du retour arriÚre est acceptée

NoncurrentVersionExpiration supprime la possibilitĂ© de revenir Ă  un state antĂ©rieur Ă  la fenĂȘtre. C'est la raison d'ĂȘtre du versioning sur un bucket de state : le coĂ»t est rĂ©el, il se nomme.

Il est accepté à trente jours pour trois raisons :

  1. Un retour arriÚre utile est récent. Un state de mai décrit des ressources AWS qui n'existent plus ; le restaurer ne restaure rien.
  2. ephemeral n'a aucun besoin de retour arriÚre. La stack est détruite et recréée à chaque session, son historique n'a jamais servi.
  3. Le filet de persistent n'est pas le versioning, ce sont les procédures de repose de l'ADR 033 (§14) et de pose des clés de l'ADR 034 (§15). Elles reconstruisent, elles ne restaurent pas.

5. KMS est écarté, et cette fois la raison n'est plus le coût

L'arbitrage KMS a été écarté deux fois (backend/main.tf, CloudTrail) et l'ADR 033 le rouvrait explicitement ici. Il est écarté une troisiÚme fois.

Une clĂ© gĂ©rĂ©e (CMK, une clĂ© dont on Ă©crit soi-mĂȘme la politique) ne change qui dĂ©chiffre que si l'identitĂ© qui applique Terraform est distincte de l'identitĂ© administrateur. Or Terraform tourne sous iamadmin, porteur d'AdministratorAccess, qui serait donc autorisĂ© sur la clĂ© de toute façon.

Une seconde barriĂšre devant la mĂȘme identitĂ© n'est pas une barriĂšre. C'est la forme du piĂšge de critere-qui-ne-discrimine-pas : un contrĂŽle qui ne peut structurellement rien refuser.

KMS redevient un arbitrage rĂ©el le jour oĂč l'apply tourne sous un rĂŽle dĂ©diĂ©. Le prĂ©requis est la sĂ©paration des identitĂ©s, pas le budget.

6. Ce qu'on ne peut pas prouver est écrit, pas sous-entendu

On ne peut pas prouver qu'aucune copie n'est sortie du bucket. CloudTrail est actif, multi-rĂ©gion, avec les Ă©vĂ©nements de gestion — mais les data events S3 ne le sont pas, et le commentaire de terraform/modules/cloudtrail/main.tf (ligne 175) le dit : ils sont facturĂ©s Ă  l'Ă©vĂ©nement. Or une lecture d'objet S3 est un data event. Aucun GetObject sur ce prĂ©fixe n'a jamais Ă©tĂ© journalisĂ©.

C'est précisément pour cela que le point 1 tourne les trois valeurs au lieu de se contenter de la purge. Ce qu'on ne peut pas vérifier, on le rend sans objet.

Ce qui reste vrai malgré tout : le bucket n'a jamais été public (block_public_acls, block_public_policy, ignore_public_acls, restrict_public_buckets posés depuis l'origine), et les identités ayant eu la lecture du préfixe sont user/iamadmin et, jusqu'à !343, l'utilisateur CI gitlab_ci_infra. Aucun tiers.

À instruire sĂ©parĂ©ment : activer les data events S3 sur le seul bucket de state. C'est le geste qui rendrait « qui a lu ? » rĂ©pondable, et il n'appartient pas Ă  cette ADR.

7. La preuve de la purge est une vérification d'absence, et le critÚre est la date

Le critÚre de succÚs est un list-object-versions qui ne rend plus que les versions postérieures à la purge, identifiées par leur date.

Le seuil de 20 Ko ne discrimine pas. Il a servi Ă  compter vite, il ne peut pas servir Ă  prouver : un state peut ĂȘtre volumineux sans porter de secret, et en porter un en Ă©tant petit. Le confondre avec une preuve est le motif de critere-qui-ne-discrimine-pas.

Et le compte doit exclure les verrous, faute de quoi il reproduit l'erreur de l'issue : filtrer sur la clé exacte, jamais sur le préfixe.

Conséquences

Ce qui change. Trois credentials de production changent de valeur. Le bucket de state cesse d'accumuler sans limite. AprÚs exécution, persistent ne porte plus aucune valeur applicative, ni en courant ni en historique, et les six préfixes hérités disparaissent.

Ce qui ne change pas. Le mot de passe master RDS retourne dans le state ephemeral Ă  chaque apply, et la CI le lit. La rĂšgle de cycle de vie borne la fenĂȘtre Ă  trente jours, elle ne ferme pas le flux. Ce constat n'a pas de parade ici : le traiter demanderait de confier la gĂ©nĂ©ration du mot de passe Ă  RDS lui-mĂȘme (manage_master_user_password), ce qui est un chantier distinct. À ouvrir en issue, et Ă  ne pas laisser implicite.

L'ordre d'exécution :

1. rotation des 3 valeurs    Slack, GitLab OIDC, bcrypt ArgoCD
                             + variable CI + seed-secrets.sh --seed --force
2. geste B, regle de cycle de vie    terraform/backend, code puis apply
   -> L'APPLY EST LE POINT DE NON-RETOUR : 958 objets partent sous 24-48 h
      (voir l'encadre du point 2 de la decision)
3. geste A, purge one-shot           ne porte plus que sur les 291 objets recents
   -> preuve : list-object-versions, critere = la DATE, verrous exclus
4. validation au prochain montage    ESO resout les 7 secrets, SSO ArgoCD et
                                     Grafana passent, Alertmanager poste

Le point 4 n'est pas optionnel : la rotation d'un secret n'est prouvée que par le consommateur qui l'accepte, jamais par le succÚs du put.

Ce que ça coûte. Le retour arriÚre sur le state est perdu au-delà de trente jours, définitivement. Et le SSO ArgoCD et Grafana est cassé entre la rotation et le montage suivant, ce qui est sans conséquence cluster éteint.

Ce qu'il faut retenir. La purge supprime les copies sans invalider la valeur, la rotation invalide la valeur sans supprimer les copies. Il fallait les deux, et la mesure a été nécessaire pour savoir lesquelles des douze valeurs justifiaient la rotation. Se contenter de « les mots de passe ont été renouvelés » aurait laissé trois credentials vivants dans 42 fichiers lisibles.

Alternatives écartées

A. Purger sans rien tourner

ÉcartĂ©e par la mesure. C'Ă©tait l'option retenue avant de comparer les empreintes : le flux Ă©tant arrĂȘtĂ©, purger suffisait. Elle tombe sur trois valeurs encore valides et sur l'impossibilitĂ© de prouver qu'aucune lecture n'a eu lieu (point 6). C'est la mesure qui a changĂ© la dĂ©cision, pas un avis.

B. Tourner les douze valeurs

ÉcartĂ©e. Neuf ont dĂ©jĂ  tournĂ© le 2026-09-02, vĂ©rifiĂ©. Les retourner demanderait un montage et un rejeu du Job de bootstrap PostgreSQL pour ne rien retirer de plus. Un geste qui ne change pas l'Ă©tat d'exposition n'est pas une mesure de sĂ©curitĂ©, c'est du bruit.

C. Accepter et documenter, sans rien faire

ÉcartĂ©e, et il fallait la nommer pour ne pas la choisir par dĂ©faut, comme l'issue #199 le demande. Elle est dĂ©fendable sur un constat qu'on ne peut pas corriger — c'est la forme des ADR 035 et 036. Ici la correction est bon marchĂ©, sans impact sur le live cluster Ă©teint, et trois credentials de production sont en jeu.

D. Deux rÚgles de cycle de vie, une par préfixe

ÉcartĂ©e pour la complexitĂ©. Sept jours sur ephemeral et trente sur persistent se dĂ©fendent, puisque ephemeral n'a aucun besoin de retour arriĂšre. Mais la CI lit la version courante de toute façon : raccourcir la fenĂȘtre des versions non courantes ne lui retire rien. Deux rĂšgles Ă  maintenir pour un gain non mesurable. À rouvrir si la lecture du prĂ©fixe ephemeral est un jour ouverte Ă  un tiers.

E. Supprimer le versioning du bucket

ÉcartĂ©e sans hĂ©sitation. Elle traiterait le stock futur en supprimant le seul filet contre un apply destructeur ou un state corrompu. Le problĂšme est l'absence de borne, pas l'existence du versioning.

F. Chiffrer avec une clĂ© KMS et s'arrĂȘter lĂ 

TraitĂ©e au point 5. ÉcartĂ©e tant que Terraform tourne sous l'identitĂ© administrateur.

Références

  • Issue #199 — les copies du state qui portent les secrets : rotation ou purge (le stock)
  • Issue #198 — les sept secrets en clair dans le state et l'accĂšs de la CI (le flux) ; cette ADR ferme sa derniĂšre case
  • ADR 033 — Terraform dĂ©clare les secrets, il ne les fabrique plus : la parade dont l'application est vĂ©rifiĂ©e ici sur le fichier
  • ADR 034 — Terraform dĂ©clare les identitĂ©s CI, il n'en fabrique plus les clĂ©s : « la rotation vient aprĂšs la parade », et la mesure dont cette ADR corrige le compte
  • ADR 036 — un constat acceptĂ© par Ă©crit avec ses dĂ©clencheurs : la forme qui a Ă©tĂ© envisagĂ©e puis Ă©cartĂ©e au point C
  • Issue #202 (fermĂ©e) — les trois clĂ©s CI re-créées Ă  la main
  • Issue #193 (fermĂ©e) — politique de ressource deny by default sur les sept secrets (AW-10) : le contrĂŽle qui refuse iamadmin par la façade pendant que le stock lui reste ouvert
  • docs/aws-setup.md §14 (repose des valeurs, --force) et §15 (pose des clĂ©s) — les procĂ©dures qui remplacent le retour arriĂšre perdu
  • scripts/seed-secrets.sh — la distinction entre valeur regĂ©nĂ©rĂ©e et valeur reposĂ©e, qui explique pourquoi trois valeurs ont survĂ©cu
  • Constat AW-5 du rapport d'audit — il parle du bucket de state, jamais de ce qu'il contient