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 CIgitlab_ci_infralit ce state, et il le doit, puisqu'il applique la stack. La restriction de préfixe porte précisément surfastapi-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 APIReadyle 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.tfdocs/comprendre/cycle-de-vie-des-secrets.md, section 7docs/validation-runbook.md, section 0