Skip to content

ADR 034 — Terraform déclare les identités CI, il n'en fabrique plus les clés (2026-09-04)

Statut

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

Le constat a été trouvé le 2026-09-02, en instruisant la rotation qui suit la parade 2 de #198. Il ne figure dans aucun des 48 constats du rapport d'audit, et l'ADR 032 est passée à côté en traitant pourtant le sujet voisin : elle a protégé la variable CI, personne n'a regardé que la même valeur était en clair dans le state.

Cette ADR tranche la parade avant son exécution, pas après. Même raison que l'ADR 033 : une fois jouée, la stack persistent ne refabrique plus les clés d'accès de la CI. C'est un arbitrage opérationnel, il se décide par écrit.

Contexte

Ce que Terraform fait aujourd'hui

terraform/modules/iam/main.tf crée trois aws_iam_access_key. Le provider AWS stocke l'attribut secret en clair dans le state, et il ne peut pas faire autrement : l'API IAM ne rend le secret qu'une seule fois, à la création. Pour pouvoir le restituer dans un output, le provider doit le garder.

   aws_iam_access_key.gitlab_ci_infra
              |
              +--------------------------+
              v                          v
      variable CI GitLab          terraform.tfstate
   (masquée + protégée, #184)   s3://yk-devops-terraform-state/
              |                     fastapi-eks/persistent/
              v                          |
        ADR 032 : traitée                v
                                   AUCUN contrôle

Relevé sur le state persistent :

Ressource Utilisateur Clé Créée le
gitlab_ci fastapi-eks-gitlab-ci AKIAS4X2I7YSKIGKQKXV 2026-05-12
gitlab_ci_infra fastapi-eks-gitlab-ci-infra AKIAS4X2I7YSJCRT2OMM 2026-05-17
cockpit_panel fastapi-eks-cockpit-panel AKIAS4X2I7YSFJC4WOPQ 2026-07-11

Les trois sont actives, et le sont depuis mai.

Pourquoi c'est plus grave que #198

198 portait sur des secrets applicatifs. Ici ce sont des **identifiants

AWS**.

gitlab_ci_infra porte la policy terraform_ci, avec iam:CreateUser, iam:CreateAccessKey et iam:AttachUserPolicy. L'ADR 032 l'a déjà écrit à propos de la variable CI :

Avec ces trois derniers appels, on se fabrique un administrateur. C'est une prise de contrôle du compte AWS, pas un accès à un registre.

C'est le motif garde-fou-documentaire-vs-controle pour la troisième fois sur ce dossier : une porte fermée pendant que l'autre reste ouverte.

Ce que la parade 2 de #198 n'a pas réglé

Elle a retiré random_password et aws_secretsmanager_secret_version, parce que Terraform pouvait cesser de connaître ces valeurs.

Elle ne change rien ici. aws_iam_access_key est toujours géré par Terraform, donc le provider réécrit secret dans le state à chaque apply. Pour ces trois valeurs, le robinet est resté ouvert.

Ce que le lab a mesuré (2026-09-04)

Sur un émulateur AWS local, avant de toucher au vrai state. Le périmètre de cet outil est fixé par #200 : les questions de forme du state s'y traitent, les questions de droits jamais. Celle-ci est une question de forme.

La valeur est à deux endroits du state, pas un :

Emplacement Contient la valeur
la ressource aws_iam_access_key oui
la section outputs du state oui, la même, en clair

sensitive = true est posé sur les six outputs. Il masque l'affichage, pas le stockage — même confusion que masquée contre protégée sur les variables CI (ADR 032).

Séquence mesurée, étape par étape :

Étape Résultat mesuré
state rm de la ressource la ressource part, l'output garde la valeur en clair
retrait des blocs, puis plan Changes to Outputs, et pas No changes
apply 0 added, 0 changed, 0 destroyed, outputs purgés
recherche sur le fichier de state la valeur n'apparaît plus
la clé côté AWS toujours Active

Deux conclusions, et la première contredit la recette de l'ADR 033.

Décision

1. Terraform garde l'identité, il perd les identifiants

  • Terraform garde aws_iam_user, aws_iam_policy et les attachements.
  • Terraform perd les trois aws_iam_access_key et les six outputs correspondants, dans modules/iam/outputs.tf et dans persistent/outputs.tf.
  • Les clés se créent hors Terraform (aws iam create-access-key) et se posent directement dans les variables CI, comme les valeurs de secrets se posent par scripts/seed-secrets.sh depuis l'ADR 033.

Le partage se retient en une phrase, symétrique de celle de l'ADR 033 : Terraform déclare l'identité, il n'en connaît jamais les identifiants.

2. La séquence exige un apply, contrairement à l'ADR 033

C'est le point neuf, et il est contre-intuitif :

1. terraform state rm   (les 3 aws_iam_access_key)
2. retrait des blocs ressource ET des 6 outputs
3. terraform plan    ->  annonce "Changes to Outputs"
4. terraform apply   ->  0 ressource touchée, outputs purgés
5. vérification sur le fichier de state téléchargé

L'ADR 033 posait plan vide comme critère de succès et interdisait tout apply. Appliquer cette recette ici laisserait la valeur dans les outputs. Un opérateur qui suivrait l'ADR 033 à la lettre s'arrêterait à l'étape 3 devant un plan non vide, en croyant à une erreur, et signerait une parade incomplète.

Cet apply est sûr, et la mesure le dit : il ne touche aucune ressource, il ne réécrit que la section outputs. La clé reste Active chez AWS.

Aucun apply entre 1 et 2, en revanche. L'ordre inverse, ou un apply glissé au milieu, supprimerait les clés d'accès chez AWS : Terraform les a encore dans son state et ne les voit plus dans la configuration. La CI perdrait ses identifiants au même instant, gitlab_ci_infra compris, c'est-à-dire l'identité qui sert à monter et à détruire l'infrastructure.

Le critère de succès est la vérification d'absence sur le fichier de state téléchargé, section outputs comprise — jamais la sortie du plan, jamais le succès de l'apply. C'est la règle du « vert qui ne prouve rien ».

3. La rotation vient après la parade, jamais avant

Tant que Terraform fabrique la valeur, la tourner la fait revenir dans le state au plan suivant. On tuerait des copies anciennes pour en créer une nouvelle, vivante.

C'est la leçon de #198, et la raison pour laquelle la rotation du 2026-09-02 a été faite après la parade.

L'ancienne clé ne se supprime qu'une fois la nouvelle prouvée par un pipeline vert, et pas sur le succès de l'appel create-access-key.

4. Le coût opérationnel est accepté, contre une procédure rejouée

La stack ne refabrique plus les clés seule. Après un destroy/apply de persistent, il faudra en créer trois à la main et les poser dans six variables CI.

Une contrainte propre aux clés d'accès, absente de l'ADR 033 : une valeur de Secrets Manager peut se relire autant de fois qu'on veut ; le secret d'une access key ne se lit qu'à la création. Il n'y a pas de « repose », seulement une re-création. Perdre la valeur entre sa création et sa pose dans GitLab oblige à supprimer la clé et à recommencer.

Accepté à une condition, la même que l'ADR 033 : la procédure est écrite et rejouée une fois de bout en bout avant que #202 se ferme. Une procédure jamais exécutée est une intention, pas un filet.

5. Le périmètre s'arrête au flux, et le reste est nommé

Cette ADR traite ce que Terraform écrit désormais. Restent non couverts :

  • Le stock. Les 756 versions du state (139 persistent, 617 ephemeral) portent les valeurs déjà écrites. C'est #199, et la parade ne les efface pas : après exécution, les anciennes versions restent lisibles.
  • Le mot de passe master RDS du state ephemeral (aws_db_instance.password), même problème, même parade à instruire.
  • terraform.tfstate.backup. Mesuré au lab : le backup local porte encore la valeur après l'apply. Sur le backend S3, ce rôle est tenu par le versioning de l'objet, ce qui renvoie à #199.

À écrire noir sur blanc : après cette parade, le state persistent ne contient plus d'identifiant AWS, mais les versions précédentes en contiennent toujours. La case du « Done when » de #202 se lit au périmètre de la version courante.

Conséquences

  • #202 ne se ferme pas sur cette ADR. Restent le state rm, le retrait des blocs et des outputs, l'apply de purge, la vérification sur le fichier, la rotation des trois clés et la procédure rejouée.
  • Six outputs disparaissent de modules/iam et de persistent. Vérifié avant de décider : aucun consommateur automatique. Aucun terraform_remote_state dans le dépôt, et aucun script ne lit terraform output *secret_access_key. Les valeurs étaient posées à la main dans les variables CI.
  • Un geste manuel de plus au montage from-scratch, donc un oubli possible. Le symptôme serait un pipeline qui échoue en NoCredentials sur le premier job AWS, ce qui est au moins un message clair. La procédure doit le dire.
  • La rotation devient possible sans Terraform. Créer une clé ne demande plus un apply sur persistent, donc plus les droits qui vont avec.
  • Le lien avec #193 : une politique de ressource deny by default sur les sept secrets ne protège rien tant qu'un identifiant capable de tout lire traîne dans un fichier. Cette ADR retire cet identifiant du fichier.
  • L'arbitrage KMS sur le bucket de state se rouvre, et cette fois pour protéger des identifiants AWS et non des secrets applicatifs. Il appartient à #199.

Alternatives écartées

lifecycle { ignore_changes = [secret] }. C'est la parade qui vient à l'esprit en premier, et elle ne règle rien : la valeur est déjà dans le state depuis la création, et ignore_changes ne retire pas ce qui y est écrit. Elle donnerait l'impression d'agir, 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à. Écartée comme parade principale, pour la même raison qu'en ADR 033 : une seconde barrière d'autorisation réduit qui lit, elle laisse la valeur dans le fichier. Reste souhaitable, et appartient à #199.

Générer les clés hors Terraform mais les lui passer en TF_VAR_*. Écartée immédiatement : Terraform écrirait la valeur reçue dans le state exactement comme aujourd'hui. Générer n'est pas la condition, écrire suffit.

Sortir tout le module iam de Terraform. Écartée : on perdrait les utilisateurs, les policies et les attachements en tant que code, c'est-à-dire tout ce qui se relit et se révise en MR. Le contenant a de la valeur, c'est le contenu qui n'en a pas.

Remplacer les clés statiques par une fédération OIDC entre GitLab et AWS. C'est la seule alternative qui supprime le problème à la racine plutôt que de le déplacer : plus de clé longue durée du tout, donc plus rien à stocker, à tourner ni à fuiter. Elle n'est pas écartée sur le fond, elle est hors périmètre de cette ADR : elle demande un provider OIDC, une refonte des rôles et une reprise des deux fichiers CI, quand la parade décidée ici tient en un state rm et un retrait de blocs. Retenue comme direction cible, à ouvrir en issue propre ; cette ADR réduit l'exposition en attendant, elle ne la referme pas.

Références

  • Issue #202 (le constat, les trois clés), #198 (la même mécanique sur les secrets applicatifs), #199 (les 756 versions du state), #193 (politique de ressource deny by default), #200 (périmètre de l'émulateur AWS local qui a servi à la mesure), #184 (variables CI masquées et protégées)
  • ADR 032 (privilèges des clés CI, masquée n'est pas protégée), ADR 033 (secrets hors du state — dont cette ADR corrige la recette sur le point de l'apply)
  • Fiches : garde-fou-documentaire-vs-controle, critere-qui-ne-discrimine-pas (la preuve est la vérification d'absence, jamais le succès de l'appel)