ADR 033 â Terraform dĂ©clare les secrets, il ne les fabrique plus (2026-09-02)¶
Statut¶
Accepté, écrit le 2026-09-02. Issue #198, Sprint 7 (Remédiation & Fiabilité).
Le constat a été trouvé le 2026-09-01 par une lecture de code, pas par un scan.
Il ne figure dans aucun des 48 constats du rapport d'audit : celui-ci parle du
bucket de state (AW-5, HTTPS et MFA Ă la suppression) mais jamais de ce qu'il
contient.
Cette ADR tranche la parade 2 avant son exécution, pas aprÚs. La raison est
la conséquence 2 : une fois jouée, la stack persistent ne se reconstruit plus
d'un seul apply. C'est un arbitrage opérationnel, il se décide par écrit.
La parade 1 est déjà livrée (!343) : la policy terraform_ci est restreinte
au préfixe fastapi-eks/ephemeral/*. Elle réduit qui lit le state, elle ne change
rien Ă ce qu'il contient.
Contexte¶
Ce que Terraform fait aujourd'hui¶
terraform/modules/secrets/main.tf ne crée pas seulement un emplacement dans
Secrets Manager. Il génÚre la valeur, l'envoie, et en garde une copie.
random_password.db_env["prod"]
random_password.secret_key_env["prod"]
|
+--------------------------+
v v
Secrets Manager terraform.tfstate
fastapi-eks/prod s3://yk-devops-terraform-state/
| fastapi-eks/persistent/
v |
politique de ressource v
(action 11, #193) AUCUN contrĂŽle
Deux attributs stockent la valeur en clair dans le state :
random_password.result et aws_secretsmanager_secret_version.secret_string.
sensitive = true masque l'affichage, pas le stockage. C'est la mĂȘme
confusion que masquée contre protégée sur les variables CI (ADR 032) : deux
mots qui répondent à des questions différentes.
Le compte exact, mesuré sur le module :
| Ce que le module porte | Nombre |
|---|---|
Secrets Secrets Manager (aws_secretsmanager_secret) |
7 |
| Ressources porteuses d'une valeur dans le state | 15 |
dont random_password |
8 |
dont aws_secretsmanager_secret_version |
7 |
Les sept secrets : fastapi-eks/app, /dev, /staging, /prod, /grafana,
/alertmanager-slack, /argocd.
Les valeurs externes sont logĂ©es Ă la mĂȘme enseigne. Le webhook Slack, le
bcrypt admin ArgoCD et le client secret OIDC GitLab ne sont pas générés par
Terraform : ils arrivent par TF_VAR_*. Terraform les Ă©crit quand mĂȘme dans le
state, parce qu'il écrit la version du secret. Générer n'est pas la condition,
écrire suffit.
Ce que le lab a mesurĂ© (2026-09-01)¶
Sur un lab jetable, avant de toucher au vrai state :
- la valeur du secret survit au
state rmâ empreintesha256identique avant et aprĂšs, cĂŽtĂ© Secrets Manager ; - une fois les blocs retirĂ©s, le
planfinal ditNo changes.
La séquence est donc jouable sans perte et sans dérive de plan. Ce n'est pas une hypothÚse, c'est une mesure.
Pourquoi lifecycle { ignore_changes = [secret_string] } ne rĂšgle rien¶
C'est la parade qui vient Ă l'esprit en premier, et elle ne marche pas.
ignore_changes empĂȘche Terraform de modifier la ressource, pas de la
lire. Au refresh, il appelle GetSecretValue et réécrit la valeur dans le
state. Le secret y retournerait Ă chaque plan.
La seule façon de ne pas stocker une valeur est de ne pas l'avoir.
Ce que la parade 1 laisse ouvert¶
La parade 1 a retiré l'accÚs de la CI au state persistent. Restent :
iamadminet tout chemin versAdministratorAccess, qui lisent le bucket ;- les anciennes versions du state, le versioning étant actif (#199) ;
- le chiffrement SSE-S3 du bucket, sans seconde barriĂšre d'autorisation.
DĂ©cision¶
1. Terraform garde le contenant, il perd le contenu¶
- Terraform garde
aws_secretsmanager_secret: nom, ARN, tags, description, et demain la politique de ressource de #193. - Terraform perd
random_passwordetaws_secretsmanager_secret_version. - La valeur est posée à cÎté, hors Terraform, par
aws secretsmanager put-secret-value.
Le partage est net et se retient en une phrase : Terraform déclare l'existence du secret, il n'en connaßt jamais la valeur.
Rien ne casse en aval. Vérifié avant de décider :
- aucun
terraform_remote_statedans le dĂ©pĂŽt âephemeralne lit pas le state depersistent; ephemeralaccĂšde aux secrets pardata "aws_secretsmanager_secret"par nom (terraform/ephemeral/irsa.tf), pour cĂąbler les ARN dans les policies IRSA. Un data source par nom ne dĂ©pend pas de qui a Ă©crit la valeur.
2. La perte de la reconstruction automatique est acceptĂ©e, contre une procĂ©dure rejouĂ©e¶
Aujourd'hui, si persistent est détruite et recréée, Terraform refabrique les mots
de passe seul. AprĂšs, il faudra reposer les valeurs Ă la main : sept payloads,
quatorze clés au total.
C'est le vrai prix de cette ADR, et il est opérationnel, pas théorique.
Il est accepté à une condition : la procédure de (re)pose est écrite et
rejouée une fois de bout en bout avant que #198 se ferme. Une procédure jamais
exĂ©cutĂ©e est une intention, pas un filet â c'est le motif de la fiche
garde-fou-documentaire-vs-controle, déjà en cause dans cette issue.
3. L'ordre d'exĂ©cution est contraint, et il n'y a pas d'apply au milieu¶
1. terraform state rm (les 15 ressources)
2. retrait des blocs dans modules/secrets/main.tf
3. terraform plan -> doit dire "No changes"
Aucun apply entre 1 et 2. L'ordre inverse, ou un apply glissé au milieu,
détruit les aws_secretsmanager_secret_version chez AWS : Terraform les a encore
dans son state et ne les voit plus dans la configuration.
Comme recovery_window_in_days = 0 sur les sept secrets, une suppression est
immĂ©diate et sans fenĂȘtre de rĂ©cupĂ©ration.
Le critÚre de succÚs est le plan vide et la vérification sur le fichier de
state téléchargé, pas sur la sortie du plan. C'est la rÚgle du « vert qui ne
prouve rien » : la preuve est la vérification d'absence, jamais le succÚs de
l'appel.
4. Le pĂ©rimĂštre s'arrĂȘte au flux, et le reste est nommĂ©¶
Cette ADR traite le flux : ce que Terraform écrit désormais dans le state. Trois choses restent en clair et ne sont pas couvertes :
- Le stock. Les anciennes versions du state portent les mĂȘmes valeurs. C'est #199, et l'arbitrage KMS s'y rouvre.
- Le mot de passe RDS de la stack
ephemeral.aws_db_instance.passwordest en clair dans le stateephemeral, alimenté parTF_VAR_db_password. La parade 2 ne le touche pas. - Les trois clés d'accÚs IAM de
persistent.module.iamcréeaws_iam_access_keypourgitlab_ci,gitlab_ci_infraetcockpit_panel: l'attributsecretest stocké en clair, par construction du provider.
Conséquence à écrire noir sur blanc : aprÚs la parade 2, le state persistent
ne contient plus de secret applicatif, mais il n'est pas propre pour autant. La
case du « Done when » de #198 doit ĂȘtre lue au pĂ©rimĂštre des sept secrets, sinon
elle se fermerait sur une affirmation fausse.
ConsĂ©quences¶
- #198 ne se ferme pas sur cette ADR. Restent le
state rm, le retrait des blocs, la procédure de repose écrite et rejouée, et le sort des anciennes versions tranché par écrit. - Cette décision débloque #193. Sans
aws_secretsmanager_secret_version, plus aucunGetSecretValueauplandepersistent. Le blocage identifiĂ© le 01/09 â une politique de ressourcedeny by defaultcasserait le refresh â disparaĂźt, et le rĂŽle Terraform dĂ©diĂ© qu'on envisageait devient inutile. Les deux problĂšmes avaient la mĂȘme cause et ont la mĂȘme solution. - Trois variables sensibles sortent de
persistent:slack_webhook_url,argocd_admin_bcrypt,argocd_gitlab_client_secret. Elles n'ont aucun autre consommateur dans la stack. LesTF_VAR_*correspondantes peuvent quitter les variables CI, ce qui prolonge l'ADR 032.TF_VAR_db_passwordreste nécessaire :ephemeralen a besoin pour RDS. - Un geste manuel de plus au montage from-scratch, donc un oubli possible. Le
symptÎme serait un ESO qui ne résout pas, pods en
CreateContainerConfigError, et non un message parlant. La procédure de repose doit le dire. - Le module
secretsdevient presque vide : sept ressources déclaratives et aucune valeur. C'est voulu, et c'est ce qui rend la revue de ce fichier facile. - La rotation devient possible sans Terraform. Reposer une valeur ne demande
plus un
applysurpersistent, donc plus les droits qui vont avec. Ce n'est pas l'objet de l'ADR, mais c'est un effet Ă ne pas perdre de vue pour #199.
Alternatives Ă©cartĂ©es¶
lifecycle { ignore_changes = [secret_string] }. Traitée en contexte : le
refresh réécrit la valeur à chaque plan. Elle ne réduit rien et donne
l'impression du contraire, ce qui est pire que ne rien faire.
Chiffrer le bucket de state avec une clĂ© KMS dĂ©diĂ©e, et s'arrĂȘter lĂ .
L'arbitrage KMS a été écarté deux fois (backend/main.tf, CloudTrail) et il se
rouvre effectivement ici. ĂcartĂ©e comme parade principale : une seconde
barriÚre d'autorisation sur le state réduit qui lit, elle laisse la valeur dans le
fichier. C'est la parade 1 en plus fort, pas la parade 2. Reste souhaitable et
appartient Ă #199, oĂč elle porte sur le stock.
Générer les mots de passe hors Terraform mais les lui passer en TF_VAR_*.
SĂ©duisant parce que la stack resterait reconstructible d'un apply. ĂcartĂ©e
immédiatement : Terraform écrirait la valeur reçue dans le state exactement comme
aujourd'hui, et on aurait ajouté quatre secrets aux variables CI pour ne rien
gagner. Générer n'est pas la condition, écrire suffit.
Sortir le module secrets de Terraform entiĂšrement, secrets compris. ĂcartĂ©e :
on perdrait les noms, les tags, la description et surtout le point d'accroche de
la politique de ressource de #193. Le contenant a de la valeur en tant que code,
c'est le contenu qui n'en a pas.
Attendre #199 et tout traiter d'un coup. ĂcartĂ©e pour l'ordre : #198 arrĂȘte l'Ă©criture, #199 traite ce qui est dĂ©jĂ Ă©crit. Purger le stock avant d'arrĂȘter le flux, c'est vider une baignoire sans fermer le robinet.
RĂ©fĂ©rences¶
- Issue #198 (le constat, les deux parades), #193 (politique de ressource
deny by defaultsur les sept secrets,AW-10), #199 (les anciennes versions du state), #189 (durcissement Terraform), #137 et #138 (secrets par environnement), #33 (ESO + IRSA) - ADR 029 (multi-env, un secret par environnement), ADR 030 (layout du dépÎt), ADR 032 (privilÚges des clés CI, masquée n'est pas protégée)
- MR
!343(parade 1 : policyterraform_cirestreinte au préfixeephemeral) - Constats d'audit
AW-5(bucket de state),AW-10(tout chemin versAdministratorAccesslit les secrets) - Fiches :
garde-fou-documentaire-vs-controle(protéger une porte pendant que l'autre reste ouverte),critere-qui-ne-discrimine-pas(le vert qui ne prouve rien)