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, 0aws_secretsmanager_secret_version, 0aws_iam_access_key; - aucun output de valeur sensible, section
outputsrelue 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 dedev/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 :
GetSecretValuesur les sept secrets : refusĂ© sept fois sur sept. C'est la politique de ressourcedeny by defaultde #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_dayscompte Ă partir du moment oĂč une version est devenue non courante, c'est-Ă -dire du jour oĂč l'applysuivant 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'
applydu 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 :
- 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.
ephemeraln'a aucun besoin de retour arriÚre. La stack est détruite et recréée à chaque session, son historique n'a jamais servi.- Le filet de
persistentn'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 defaultsur les sept secrets (AW-10) : le contrĂŽle qui refuseiamadminpar 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 perduscripts/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-5du rapport d'audit â il parle du bucket de state, jamais de ce qu'il contient