Skip to content

ADR 038 — Le mot de passe master RDS passe en écriture seule, le state ephemeral ne le porte plus (2026-09-10)

Statut

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

Cette ADR ferme le dernier flux de secret connu vers un state Terraform. Elle est la suite de l'ADR 033 (Terraform n'écrit plus les sept secrets) et de l'ADR 034 (ni les trois clés d'accès CI), qui nommaient toutes deux ce cas sans le trancher. L'ADR 037 a traité le stock des anciennes versions. Celle-ci ferme le flux qui l'alimentait encore.

La vérification sur un state réel reste à faire : elle exige un apply, donc un montage. Voir la décision 5.

Contexte

Le dernier flux ouvert

aws_db_instance recevait password = var.db_password. Terraform stocke la valeur de chaque attribut dans le state. Le mot de passe du master user PostgreSQL était donc écrit en clair dans fastapi-eks/ephemeral/terraform.tfstate, à chaque apply.

Le master user ne sert pas à l'application. Depuis #138, chaque environnement se connecte avec son propre user (app_dev, app_staging, app_prod). Le master sert au Job de bootstrap DB, qui crée ces bases et ces users sur une instance qui renaît vide à chaque montage.

Pourquoi les parades précédentes ne s'appliquaient pas

  • Restreindre l'accès (#198, !343) : l'utilisateur CI gitlab_ci_infra lit ce state, et il le doit, puisqu'il applique la stack. La restriction de préfixe porte précisément sur fastapi-eks/ephemeral/*.
  • Retirer la ressource du code (ADR 033) : impossible. RDS exige un mot de passe à la création, on ne déclare pas une instance sans dire comment son compte admin s'authentifie.

Ce qui distinguait ce cas des sept secrets

Le flux se rouvrait à chaque montage. Tourner la valeur (fait le 2026-09-09, #199) ne suffisait pas : le apply suivant réécrivait la nouvelle. C'est la règle apprise sur ce projet : la rotation vient après la parade.

Décision

1. password devient password_wo, un attribut en écriture seule

Terraform 1.11 a introduit les attributs en écriture seule (write-only). Terraform transmet la valeur au provider pendant l'apply, puis ne l'écrit ni dans le state, ni dans le plan. Le provider AWS l'expose sur aws_db_instance sous le nom password_wo.

Vérifié sur le schéma du provider, le 2026-09-10 : la version 6.64.0, que ~> 6.0 installe, déclare password_wo write_only=true. La CI tourne en Terraform 1.15, le poste en 1.14.3. required_version passe de >= 1.10.0 à >= 1.11.0, pour que l'exigence soit écrite et pas seulement satisfaite.

Le plan l'affiche ainsi :

+ password_wo         = (write-only attribute)
+ password_wo_version = 1

2. password_wo_version est exigé, et c'est la seule trace dans le state

Le provider refuse password_wo seul : all of password_wo,password_wo_version must be specified. Terraform ne peut pas comparer une valeur qu'il ne garde pas. Le compteur est le seul moyen de lui dire « pousse une nouvelle valeur ».

Il vaut 1 et n'a pas vocation à bouger. L'instance est recréée à chaque montage, et reçoit à sa création la valeur courante de TF_VAR_db_password. L'incrémenter ne sert que pour tourner le mot de passe d'une instance vivante.

3. La variable est éphémère : le garde-fou est un refus, pas un commentaire

db_password est déclarée ephemeral = true, à la racine de la stack ephemeral et dans le module rds. Mesuré dans un lab jetable le 2026-09-10 :

Montage Résultat
Variable éphémère branchée sur password terraform validate refuse : Invalid use of ephemeral value
password_wo, variable non éphémère, plan -out la valeur est dans le fichier de plan, 1 occurrence
password_wo, variable éphémère, plan -out 0 occurrence

Le premier effet est le garde-fou. Un retour à password = var.db_password, par erreur ou par copier-coller, ne rouvre pas le flux en silence : il casse validate.

Le second ferme une porte que la CI n'ouvre pas aujourd'hui. infra-start joue terraform apply -auto-approve, sans plan sauvegardé. Mais un plan sauvegardé en artefact, un jour, aurait porté la valeur.

4. Le double rôle de TF_VAR_db_password est gardé, et documenté

La même variable alimente le master RDS (par Terraform) et fastapi-eks/app:DB_PASSWORD (par seed-secrets.sh), que le Job de bootstrap lit pour se connecter en master. L'issue demandait de lever ou de documenter ce double rôle.

Il est gardé, parce qu'il n'est pas une confusion. Les deux valeurs doivent être identiques, sinon le Job échoue à l'authentification. Une source unique est ce qui les garantit égales. Le Job, son ExternalSecret et le rôle IRSA d'ESO ne changent pas.

5. La preuve se fera sur le state réel, au prochain montage

Le lab prouve que la configuration est acceptée et que le plan ne porte pas la valeur. Il ne prouve rien sur le state : seul un apply en écrit un. La vérification est ajoutée à la section 0 du runbook de validation. Elle répond à deux questions distinctes :

  • la valeur est absente du state : on cherche la valeur elle-même dans le fichier, pas seulement un attribut vide ;
  • RDS a bien reçu la valeur : l'instance renaît vide, donc les users app_<env> n'existent que si le Job s'est connecté en master. Des pods API Ready le prouvent.

Le « Done when » de #219 demandait si la bascule force un remplacement de l'instance. La question tombe : la stack est détruite, sa version courante est vide, et le prochain apply crée l'instance.

Conséquences

Le dernier flux de secret connu vers un state Terraform est fermé, sous réserve de la décision 5.

Le stock reste celui de l'ADR 037. Les versions du state ephemeral portent une valeur morte, tournée le 2026-09-09. La règle de cycle de vie les fait partir à 30 jours.

Un piège de précédence, trouvé en instruisant. Un terraform.tfvars local, gitignoré, portait une ancienne valeur de db_password. Terraform donne la priorité à terraform.tfvars sur TF_VAR_*. Un apply lancé depuis le poste aurait donc créé le RDS avec l'ancienne valeur, pendant que fastapi-eks/app portait la nouvelle, et le Job de bootstrap aurait échoué. La ligne a été retirée le 2026-09-10. La CI n'était pas exposée : elle n'a pas ce fichier.

manage_master_user_password reste possible plus tard, si un besoin de rotation automatique apparaît. Ce serait un changement d'architecture du bootstrap, pas un correctif.

Alternatives écartées

A. manage_master_user_password = true

C'était la piste de l'issue. RDS génère la valeur, la range dans un secret Secrets Manager qu'il possède, la fait tourner, et Terraform ne stocke que l'ARN.

Écartée pour une raison de structure, pas de coût. C'est AWS qui nomme ce secret (rds!db-...), et le nom change à chaque recréation de l'instance, donc à chaque montage. L'ExternalSecret du Job est en GitOps : il doit citer un nom fixe, écrit dans le dépôt. Il faudrait un maillon de plus pour faire passer le nom du jour de Terraform au cluster, en plus d'un nouveau secret dans la politique IRSA d'ESO et d'une nouvelle source pour le Job. Quatre changements pour résoudre ce que password_wo résout en un.

B. Accepter et documenter

La forme des ADR 035 et 036. Défendable : la stack est éphémère, la valeur est tournée, le seul lecteur est la CI.

Écartée parce que le flux se rouvre à chaque montage, ce qui n'était le cas d'aucun des constats acceptés jusqu'ici, et parce qu'une parade de quelques lignes existe.

Références

  • Issue #219, et #198 / #199 dont elle sort
  • ADR 033, 034 et 037
  • terraform/modules/rds/main.tf, terraform/modules/rds/variables.tf, terraform/ephemeral/variables.tf
  • docs/comprendre/cycle-de-vie-des-secrets.md, section 7
  • docs/validation-runbook.md, section 0