Skip to content

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 — empreinte sha256 identique avant et aprĂšs, cĂŽtĂ© Secrets Manager ;
  • une fois les blocs retirĂ©s, le plan final dit No 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 :

  • iamadmin et tout chemin vers AdministratorAccess, 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_password et aws_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_state dans le dĂ©pĂŽt — ephemeral ne lit pas le state de persistent ;
  • ephemeral accĂšde aux secrets par data "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.password est en clair dans le state ephemeral, alimentĂ© par TF_VAR_db_password. La parade 2 ne le touche pas.
  • Les trois clĂ©s d'accĂšs IAM de persistent. module.iam crĂ©e aws_iam_access_key pour gitlab_ci, gitlab_ci_infra et cockpit_panel : l'attribut secret est 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 aucun GetSecretValue au plan de persistent. Le blocage identifiĂ© le 01/09 — une politique de ressource deny by default casserait 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. Les TF_VAR_* correspondantes peuvent quitter les variables CI, ce qui prolonge l'ADR 032. TF_VAR_db_password reste nĂ©cessaire : ephemeral en 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 secrets devient 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 apply sur persistent, 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 default sur 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 : policy terraform_ci restreinte au prĂ©fixe ephemeral)
  • Constats d'audit AW-5 (bucket de state), AW-10 (tout chemin vers AdministratorAccess lit 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)