Aller au contenu

Les secrets hors de l'état

300 Concevoir ⏱ 1 h 30 terraformopentofuscalewayiachcl

À la fin, vous saurez

  • Distinguer ce qu'un secret fait dans l'état, le plan et les journaux, et quelle protection couvre quoi
  • Déclarer une variable éphémère et une ressource éphémère, et dire où une valeur éphémère est acceptée
  • Passer un secret à une ressource par un argument en écriture seule, et gérer sa rotation avec le numéro de version
  • Lire un secret depuis Secret Manager avec une ressource éphémère plutôt qu'une source de données
  • Configurer le chiffrement de l'état d'OpenTofu et planifier un changement de clé
  • Vérifier, sur un état réel, qu'un secret n'y figure pas

Prérequis

Testé avec opentofu 1.11 et plus (d'après la documentation) random 3.9.1 scaleway 2.84.0 terraform 1.16.1 , vérifié le 5 octobre 2026

Pourquoi

Le premier cours a posé le problème sans le résoudre entièrement : l'état de Terraform est un inventaire en clair de tout ce que les ressources ont comme attributs, et un mot de passe de base de données en fait partie. Marquer une variable sensitive masque la valeur dans les affichages, mais pas dans le fichier. Quiconque peut lire l'état (le bucket Object Storage, une sauvegarde, la sortie d'un terraform state pull, l'artefact d'un plan dans la CI) lit le secret. Dans une organisation, l'état circule bien plus que prévu : il est relu par la CI, copié pour un diagnostic, analysé par des outils.

Deux mécanismes récents ont changé la donne. Les valeurs éphémères (Terraform 1.10) n'existent que le temps d'une opération et ne sont écrites ni dans le plan ni dans l'état. Les arguments en écriture seule (Terraform 1.11) permettent à une ressource de recevoir un secret sans le garder. OpenTofu les a repris dans sa version 1.11, et propose en plus, depuis la 1.7, le chiffrement de l'état, qu'il faut connaître même si l'on reste sur Terraform. Cette leçon explique à quoi sert chaque outil, ce qu'il ne couvre pas, et comment les combiner pour que le dépôt signalements-iac ne contienne aucun secret nulle part : ni dans le code, ni dans l'état, ni dans le plan.

Avec une réserve d'emblée : tout n'est pas encore possible partout. Une ressource ne reçoit un secret en écriture seule que si son fournisseur a prévu l'argument. Pour le reste, la protection de l'état redevient la seule barrière, et c'est pourquoi on la traite aussi.

Les concepts

Quatre niveaux de protection

NiveauCe qu'il protègeCe qu'il ne protège pas
sensitivel'affichage dans les plans et les sortiesl'état, le plan enregistré, les sorties JSON
valeur éphémèrel'état et le plan (la valeur n'y est jamais écrite)les endroits où le fournisseur la transmet ensuite (son API, un journal)
argument en écriture seulel'état et le plan, pour cet argumenttout autre argument de la ressource
chiffrement de l'état (OpenTofu)le fichier d'état et les plans au reposquiconque dispose de la clé, et le contenu pendant l'exécution

Le premier niveau est de l'hygiène. Les trois suivants sont des protections réelles, à utiliser en couches : on évite d'abord que le secret entre dans l'état, et pour ce qui y entre malgré tout, on chiffre.

Les valeurs éphémères

Une valeur éphémère est une valeur qui n'existe que pendant une phase de l'exécution. Terraform ne la persiste ni dans le plan ni dans l'état. Elle peut être :

  • une variable d'entrée déclarée ephemeral = true (Terraform 1.10, OpenTofu 1.11) ;
  • une sortie de module enfant déclarée ephemeral = true ;
  • le résultat d'une ressource éphémère, un bloc ephemeral qu'on ouvre au début d'une opération et qu'on referme à la fin (Terraform 1.10, OpenTofu 1.11) ;
  • une valeur locale qui dérive d'une valeur éphémère (elle le devient à son tour : l'éphémérité se propage).

Les endroits où une valeur éphémère est acceptée sont limités, et c'est la garantie : une configuration de fournisseur, un bloc provisioner ou connection, une autre valeur éphémère, et un argument en écriture seule. Partout ailleurs, Terraform refuse, dès la validation, plutôt que de persister silencieusement. On verra le message d'erreur plus bas.

Une ressource éphémère est un bloc ephemeral "type" "nom" { ... }, distinct de resource et de data. Elle n'a pas de cycle de vie en plusieurs étapes : Terraform l'ouvre (« Opening »), l'utilise, la referme (« Closing »), à chaque phase d'évaluation qui en a besoin. Elle ne laisse aucune trace dans l'état. Le fournisseur random en offre deux (random_password, random_bytes) ; le fournisseur Scaleway en 2.84 en offre sept, dont scaleway_secret_version (lire un secret) et scaleway_key_manager_decrypt.

Les arguments en écriture seule

Un argument en écriture seule (write-only) est un argument de ressource que le fournisseur reçoit pendant l'opération et ne renvoie jamais : Terraform ne l'enregistre pas dans l'état (il y vaut null) ni dans le plan, qui l'affiche (write-only attribute). Il est apparu avec Terraform 1.11 ; OpenTofu l'a ajouté dans la 1.11. C'est un attribut marqué write_only dans le schéma du fournisseur.

Le prix est évident : sans trace de la valeur, Terraform ne peut pas détecter qu'elle a changé. Un argument en écriture seule n'entre pas dans la comparaison du plan. Les fournisseurs règlent le problème avec un second argument, un numéro de version : password_wo_version accompagne password_wo, data_wo_version accompagne data_wo. On incrémente le numéro pour dire « la valeur a changé, renvoyez-la ». C'est la mécanique de rotation, détaillée plus bas.

Dans le fournisseur Scaleway 2.84, les arguments en écriture seule couvrent les mots de passe de plusieurs produits (scaleway_rdb_instance.password_wo, scaleway_rdb_user.password_wo, scaleway_redis_cluster.password_wo, scaleway_mongodb_instance.password_wo, etc.) et le contenu d'un secret (scaleway_secret_version.data_wo). Il n'y a pas de règle générale : on consulte le schéma (terraform providers schema -json) ou la page de la ressource.

Source de données ou ressource éphémère ?

Pour lire un secret existant (le mot de passe de la base, stocké dans Secret Manager), Terraform offre deux voies :

  • la source de données data "scaleway_secret_version" lit le secret et l'écrit dans l'état, marqué sensible mais présent ;
  • la ressource éphémère ephemeral "scaleway_secret_version" lit le secret à chaque opération et ne l'écrit nulle part.

Dès qu'elle est disponible, la seconde est la bonne, et la première un défaut à signaler en revue de code.

Chiffrer l'état avec OpenTofu

Quand un secret ne peut pas être gardé hors de l'état (le fournisseur n'offre aucun argument en écriture seule, ou la ressource renvoie des identifiants à la création), la défense est de chiffrer le fichier lui-même. OpenTofu le propose nativement depuis la version 1.7, dans un bloc terraform { encryption { ... } } : on y déclare un fournisseur de clé (d'où vient la clé), une méthode (comment on chiffre) et les cibles, state et plan. Terraform n'a pas d'équivalent : il compte sur le chiffrement du stockage (serveur) et sur le contrôle d'accès.

En pratique

Les démonstrations utilisent Terraform 1.16.1 et les fournisseurs random et terraform_data, sans rien créer dans le cloud. Les parties Scaleway ont été validées contre le schéma du fournisseur (terraform init et terraform validate), jamais appliquées.

Le défaut : un secret dans l'état

Pour mémoire, un mot de passe généré par la ressource classique :

resource "random_password" "classique" {
  length  = 16
  special = false
}

Le plan et terraform show le masquent (result = (sensitive value)), mais le fichier d'état, lui, le contient :

$ jq -c '.resources[] | select(.type=="random_password") | .instances[0].attributes | {result, bcrypt_hash}' terraform.tfstate
{"result":"w1kUcaUD3QvcYNLp","bcrypt_hash":"$2a$10$ugKF7QC1.7YLcDtzTA4/z.pQAzeiOJ5dUlq6b.7waGFLGT7BRzH5y"}

La valeur et son empreinte bcrypt sont là, lisibles par quiconque lit l'état. C'est le cas que nous voulons éliminer.

Une ressource éphémère, et le refus de Terraform

On déclare la version éphémère du même générateur :

ephemeral "random_password" "db" {
  length  = 16
  special = false
}

Si l'on essaie de la mettre dans un argument ordinaire, Terraform refuse dès la validation :

$ terraform validate -no-color

Error: Invalid use of ephemeral value

  with terraform_data.essai,
  on main.tf line 20, in resource "terraform_data" "essai":
  20:   input = ephemeral.random_password.db.result

Ephemeral values are not valid for "input", because it is not a write-only
attribute and must be persisted to state.

C'est le garde-fou central : le message dit pourquoi (l'argument « must be persisted to state »). Un secret éphémère ne peut aller que là où rien n'est conservé.

Une variable éphémère : ce qui entre dans le plan et l'état

Soit une ressource qui utilise un jeton passé en variable éphémère, dans un fournisseur (ici, un local-exec, un des endroits autorisés) :

variable "jeton" {
  type      = string
  ephemeral = true
}

variable "version_jeton" {
  type    = number
  default = 1
}

resource "terraform_data" "service" {
  triggers_replace = var.version_jeton

  provisioner "local-exec" {
    command     = "echo \"longueur du jeton reçu : $${#JETON}\""
    environment = { JETON = var.jeton }
  }
}

On fournit la valeur par l'environnement et on enregistre le plan :

$ export TF_VAR_jeton="jeton-de-demonstration-0042"
$ terraform plan -no-color -out=t.plan
...
Plan: 1 to add, 0 to change, 0 to destroy.

Saved the plan to: t.plan
$ terraform apply -no-color t.plan
terraform_data.service: Creating...
terraform_data.service: Provisioning with 'local-exec'...
terraform_data.service (local-exec): (output suppressed due to ephemeral value in config)
terraform_data.service: Creation complete after 0s [id=3cc59b61-e146-5661-acbd-859a04068d9e]

Deux constats. Terraform supprime la sortie du provisioner (« output suppressed due to ephemeral value in config »), parce qu'elle pourrait contenir le secret : c'est une précaution voulue. Et la valeur n'est ni dans le plan, ni dans l'état. Une recherche dans les deux fichiers (le plan enregistré est une archive ZIP) :

$ python3 - <<'EOF'
import zipfile
z = zipfile.ZipFile("t.plan")
print("occurrences dans le plan :", sum(z.read(n).count(b"jeton-de-demonstration") for n in z.namelist()))
print("occurrences dans l'état  :", open("terraform.tfstate").read().count("jeton-de-demonstration"))
EOF
occurrences dans le plan : 0
occurrences dans l'état  : 0

La sortie JSON du plan ne liste que les variables non éphémères :

$ terraform show -json t.plan | jq -c '.variables'
{"version_jeton":{"value":1}}

La variable jeton n'y est pas du tout.

Le numéro de version : pourquoi il existe

Pour voir la mécanique des arguments en écriture seule sans cloud, on utilise le bloc store de terraform_data (Terraform 1.16), dont l'argument input est en écriture seule :

resource "terraform_data" "essai" {
  store {
    input   = ephemeral.random_password.db.result
    version = 1
  }
}

À la création, le plan l'affiche comme tel :

  # terraform_data.essai will be created
  + resource "terraform_data" "essai" {
      + id = (known after apply)

      + store {
          + input   = (write-only attribute)
          + output  = (known after apply)
          + version = 1
        }
    }

Ensuite, le mot de passe éphémère change à chaque exécution (il est régénéré), pourtant le plan est vide :

$ terraform plan -no-color
No changes. Your infrastructure matches the configuration.

C'est la raison d'être de password_wo_version : tant que le numéro ne bouge pas, Terraform n'a aucun moyen de savoir que la valeur est autre. Quand on passe version à 2, le plan propose la mise à jour :

  # terraform_data.essai will be updated in-place
  ~ resource "terraform_data" "essai" {
        id = "1a368aa9-6318-5d05-836c-b047ede67a00"

      ~ store {
          ~ output  = "mTBRdddDTTSbtidL" -> (known after apply)
          ~ version = 1 -> 2
            # (1 unchanged attribute hidden)
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Warning

Cet exemple montre aussi un piège. Le bloc store de terraform_data est une échappatoire volontaire : il a pour rôle de conserver une valeur éphémère, dans store.output ou store.sensitive_output, qui sont écrits dans l'état. On l'a vérifié : la valeur de l'input se retrouve en clair dans le fichier d'état, sous store.output, et le plan suivant l'affiche. Il sert à capturer une valeur pour un usage ultérieur, jamais à protéger un secret. Un secret ne passe jamais par store.

Scaleway : mot de passe de base généré, rangé dans Secret Manager

Le dépôt signalements-iac crée la base sig-db et range son mot de passe dans Secret Manager (voir la leçon 8 de Scaleway en pratique). Version sans secret dans l'état :

ephemeral "random_password" "db" {
  length  = 32
  special = false
}

resource "scaleway_rdb_instance" "db" {
  name                = "sig-db"
  node_type           = "DB-DEV-S"
  engine              = "PostgreSQL-16"
  user_name           = "signalements"
  password_wo         = ephemeral.random_password.db.result
  password_wo_version = var.version_mot_de_passe
}

resource "scaleway_secret" "db" {
  name = "sig-db-password"
}

resource "scaleway_secret_version" "db" {
  secret_id       = scaleway_secret.db.id
  data_wo         = ephemeral.random_password.db.result
  data_wo_version = var.version_mot_de_passe
}

La même valeur éphémère alimente les deux arguments pendant l'opération : la base reçoit le mot de passe, Secret Manager reçoit le secret, et ni l'un ni l'autre n'est dans l'état. Cette configuration est valide d'après le schéma du fournisseur 2.84 (terraform validate). Dans le schéma, password_wo et data_wo portent bien l'attribut write_only, et la description de password_wo précise : « password_wo will not be set in the Terraform state. To update the password_wo, you must also update the password_wo_version ».

Un détail propre à cette construction : random_password éphémère est régénéré à chaque opération. Tant que version_mot_de_passe ne change pas, la base et le secret ne sont pas touchés, ce qui est voulu. Mais le jour où l'on incrémente la version, les deux reçoivent la nouvelle valeur au même apply : ils restent cohérents. Les deux versions doivent donc rester liées à la même variable.

Lire un secret existant, sans le garder

Quand le secret est déjà dans Secret Manager (rangé par un autre dépôt, ou à la main), on le lit par la ressource éphémère :

ephemeral "scaleway_secret_version" "db" {
  secret_name = "sig-db-password"
}

resource "scaleway_rdb_instance" "db" {
  name                = "sig-db"
  node_type           = "DB-DEV-S"
  engine              = "PostgreSQL-16"
  user_name           = "signalements"
  password_wo         = ephemeral.scaleway_secret_version.db.data
  password_wo_version = var.version_mot_de_passe
}

Le schéma de la ressource éphémère scaleway_secret_version (fournisseur 2.84) accepte secret_id ou secret_name, un revision optionnel, et expose data, sensible. Le contraste avec la source de données est net : data "scaleway_secret_version" aurait écrit data dans l'état.

Le chiffrement de l'état avec OpenTofu

Pour ce qui reste dans l'état, voici la configuration d'OpenTofu (d'après la documentation officielle ; non exécutée ici, OpenTofu n'étant pas installé) :

terraform {
  encryption {
    key_provider "pbkdf2" "phrase" {
      passphrase = var.phrase_etat
    }

    method "aes_gcm" "etat" {
      keys = key_provider.pbkdf2.phrase
    }

    state {
      method   = method.aes_gcm.etat
      enforced = true
    }

    plan {
      method   = method.aes_gcm.etat
      enforced = true
    }
  }
}
  • key_provider dit d'où vient la clé. OpenTofu en propose : pbkdf2 (dérivation depuis une phrase secrète), aws_kms, gcp_kms, azure_vault, openbao (moteur transit) et external (un programme que vous fournissez). Scaleway n'a pas de fournisseur de clé dédié : en production, openbao (OpenBao auto-hébergé) ou external (un petit programme qui interroge le service de gestion de clés retenu) sont les voies, et pbkdf2 reste le cas simple si la phrase est bien gardée.
  • method "aes_gcm" chiffre en AES-GCM avec une clé de 16, 24 ou 32 octets.
  • state et plan désignent ce qu'on chiffre, enforced = true interdit d'écrire un état non chiffré (utile pour qu'un changement de configuration ne dégrade pas la protection sans bruit).

La configuration peut aussi venir de la variable d'environnement TF_ENCRYPTION, ce qui évite de commiter la configuration quand elle contient la phrase. Les variables et les valeurs locales sont autorisées dans ce bloc à condition de ne dépendre ni de l'état ni de fonctions de fournisseur : tout doit se résoudre dès tofu init.

Important

Sans la clé, l'état est perdu. Sauvegardez la clé (ou la phrase) hors du dépôt et hors du système qu'elle protège, et ne renommez pas un fournisseur de clé ou une méthode une fois l'état chiffré (la documentation le déconseille sans procédure de migration).

Le fournisseur terraform_remote_state d'une configuration qui lit cet état doit connaître la configuration de chiffrement de l'état lu : OpenTofu la déclare dans un bloc remote_state_data_sources.

La rotation

Trois rotations sont à distinguer.

Rotation d'un secret en écriture seule. On incrémente le numéro : version_mot_de_passe = 2. Le plan montre une mise à jour de password_wo_version (et de data_wo_version), l'apply envoie la nouvelle valeur (un nouveau random_password éphémère), et les deux destinations changent ensemble. Les applications qui lisent le secret dans Secret Manager le récupèrent à leur prochain accès ; il faut qu'elles supportent un changement de mot de passe sans redémarrage ou qu'un déploiement suive.

Rotation de la clé de chiffrement de l'état (OpenTofu). On déclare la nouvelle méthode et on garde l'ancienne en repli :

state {
  method = method.aes_gcm.nouvelle
  fallback {
    method = method.aes_gcm.ancienne
  }
}

La lecture essaie la méthode principale, puis le repli ; l'écriture utilise toujours la méthode principale. Après un apply, l'état est réécrit avec la nouvelle clé, et le repli peut être retiré.

Rotation des identifiants de la plateforme. Les clés d'API Scaleway utilisées par la CI et l'état sont un sujet de la leçon 12 du premier cours : identités par usage, courte durée.

Sous le capot

Pourquoi la valeur éphémère ne fuit pas. Terraform marque la valeur avec une marque « éphémère » qui l'accompagne dans toutes les expressions. Les endroits qui persistent (attributs ordinaires, sorties non éphémères, count, for_each) refusent une valeur marquée à la validation. Les endroits qui ne persistent pas (arguments en écriture seule, configuration de fournisseur) l'acceptent. Le fichier de plan et l'état sont écrits à partir des valeurs non éphémères.

Pourquoi l'ouverture et la fermeture. Un plan et un apply sont deux processus distincts, parfois séparés de plusieurs heures. Une ressource éphémère est donc rouverte à chaque phase qui en a besoin : au plan, puis à l'apply. Deux ouvertures successives de random_password donnent des valeurs différentes, ce qui explique le « No changes » vu plus haut. Pour un secret lu (scaleway_secret_version), les deux ouvertures donnent la même valeur tant que le secret n'a pas changé.

Le fournisseur reçoit quand même la valeur. Rien n'empêche le fournisseur de la journaliser ou de la transmettre. La garantie porte sur ce que Terraform écrit. Elle ne couvre pas le contenu de la requête HTTP vers l'API ni ce que fait l'API ensuite.

Pas d'effet rétroactif. Passer une ressource de password à password_wo n'efface rien du passé : l'historique des versions de l'état (les versions du bucket, les sauvegardes terraform.tfstate.backup) conserve les anciennes valeurs. Une fuite passée se traite par une rotation, pas par la correction du code.

Pièges courants

Invalid use of ephemeral value : on a branché une valeur éphémère sur un argument ordinaire. Le message dit que l'argument « must be persisted to state ». Il faut un argument _wo, ou une autre approche (laisser le fournisseur générer le secret, voir plus bas).

Oublier le numéro de version. On change le secret, on lance le plan : « No changes ». Le mot de passe réel n'a pas changé. C'est le comportement attendu, et la raison d'être de _wo_version.

Passer le secret par store. Voir l'avertissement plus haut : il l'écrit dans l'état.

Une sortie de module éphémère consommée au mauvais endroit. Une sortie ephemeral = true ne peut aller que dans une valeur éphémère, une configuration de fournisseur ou un argument _wo. Les erreurs sont les mêmes que ci-dessus, mais la cause est dans un autre module.

Un secret dans terraform.tfvars. Une variable éphémère n'écrit rien dans l'état, mais si la valeur est dans un fichier commité, elle est dans Git. Passez-la par l'environnement (TF_VAR_...) ou par la CI, depuis un gestionnaire de secrets.

Compatibilité entre les deux outils. Une configuration qui utilise ephemeral et _wo demande Terraform 1.11 ou OpenTofu 1.11. Le bloc encryption n'existe qu'avec OpenTofu : un module partagé qui le contiendrait est illisible par Terraform. Gardez-le dans la configuration racine, jamais dans un module réutilisable.

Sécurité

  • L'état est un secret. Même si vous avez éliminé le plus gros, il contient des identifiants, des adresses, des clés publiques. Bucket privé, versionné, chiffré côté serveur, accès limité à l'identité de la CI, journalisation des lectures. Voir la leçon 7 du premier cours.
  • Un secret fuité se traite par une rotation. Corriger le code ne retire pas l'ancienne valeur des versions de l'état ni des journaux de la CI.
  • Préférez la génération côté fournisseur quand elle existe. Si le service sait créer lui-même le mot de passe initial (ou un compte lié à une identité, sans mot de passe), Terraform n'a rien à manipuler. Un mot de passe qui n'existe pas dans votre configuration ne peut pas fuir.
  • Séparez qui peut lire le secret de qui peut planifier. Le compte du plan sur demande de fusion n'a pas besoin du droit de lire le mot de passe de production : une ressource éphémère scaleway_secret_version n'ouvre qu'avec le droit d'accès au secret, ce qui se règle par une politique IAM (voir le premier cours pour les identités par usage).
  • La sortie des provisioners est supprimée avec une valeur éphémère. C'est voulu ; ne la contournez pas en écrivant la valeur dans un fichier.
  • Chiffrer l'état n'est pas supprimer le secret. Qui obtient la clé et le fichier lit tout. Placez la clé dans un système distinct (KMS, OpenBao) et tracez ses usages.

En production

Un chemin de migration progressif. Inventoriez d'abord les secrets de l'état : terraform show -json | jq filtré sur les valeurs sensibles, ou une recherche dans le schéma des ressources utilisées. Pour chaque secret, passez à _wo quand le fournisseur le permet, en commençant par les mots de passe de bases (le gros du risque). Prévoyez une rotation à l'occasion du changement : c'est elle qui rend les anciennes valeurs inoffensives.

Un état qui n'est plus une corvée. Si plus aucun secret ne passe par l'état, ses droits de lecture peuvent être élargis à l'équipe (diagnostic) sans exposer de mot de passe. C'est un gain d'exploitation concret.

Terraform ou OpenTofu ? Les deux traitent désormais les valeurs éphémères et l'écriture seule. Le chiffrement natif de l'état est le seul avantage fonctionnel d'OpenTofu dans cette leçon ; il compte si les exigences d'un client imposent que l'état soit chiffré au niveau applicatif, pas seulement par le stockage. La leçon 10 traite des passages d'un outil à l'autre, et un état chiffré par OpenTofu ne se relit pas par Terraform.

Qui crée le secret ? Un schéma courant : un dépôt « plateforme » crée les secrets vides dans Secret Manager et gère les droits ; le dépôt applicatif lit par ephemeral. Le mot de passe est créé et roulé par un processus dédié (ou par le fournisseur), pas par la configuration qui consomme.

Les couches de la leçon 4 : les secrets sont un cas où l'on évite le partage par terraform_remote_state. Une couche ne publie pas un mot de passe en sortie pour qu'une autre le lise ; elle publie l'identifiant du secret et l'autre couche le lit par la ressource éphémère.

Exercices

Exercice 1 : classer

Pour chacun, dites si la valeur finit dans l'état et ce qu'il faut changer si c'est inacceptable : (a) variable "mdp" { sensitive = true } passée à scaleway_rdb_instance.password ; (b) ephemeral = true passée à scaleway_rdb_instance.password_wo ; (c) data "scaleway_secret_version" qui alimente password_wo ; (d) ephemeral "random_password" dans scaleway_secret_version.data_wo ; (e) un output ordinaire qui expose ephemeral.random_password.db.result.

Solution

(a) Oui : sensitive ne masque que l'affichage, password est un attribut ordinaire. Passer à password_wo avec une variable éphémère et un numéro de version. (b) Non : c'est le cas voulu. (c) La source de données écrit data dans l'état ; seul l'argument de destination est en écriture seule. Remplacer par ephemeral "scaleway_secret_version". (d) Non, c'est le cas voulu. (e) Terraform refuse à la validation : une sortie ordinaire est persistée, la valeur éphémère n'y est pas admise (il faudrait ephemeral = true sur la sortie, d'un module enfant).

Exercice 2 : la rotation

La base sig-db utilise password_wo_version = 1. L'équipe sécurité demande une rotation. Décrivez la modification, ce que le plan affichera, et ce qui doit se passer côté application. Que se passe-t-il si vous oubliez de changer aussi data_wo_version sur le secret ?

Solution

On passe la variable version_mot_de_passe de 1 à 2, ce qui change password_wo_version et data_wo_version. Le plan montre deux mises à jour en place, avec (write-only attribute) pour les valeurs. À l'apply, un nouveau random_password éphémère est ouvert et envoyé aux deux ressources. L'application doit recharger le secret (prévoyez une courte fenêtre ou un redémarrage progressif, le temps que les connexions existantes se renouvellent). Si l'on oublie data_wo_version, la base a le nouveau mot de passe et Secret Manager conserve l'ancien : l'application, qui lit le secret, ne peut plus se connecter. D'où la règle de lier les deux numéros à la même variable.

Exercice 3 : retrouver un secret dans l'état

Écrivez la commande qui cherche dans un fichier d'état toute valeur d'attribut dont le nom contient password, secret ou token, puis dites comment vous vérifieriez qu'un secret précis n'est ni dans l'état ni dans le plan enregistré.

Solution
jq -r '.resources[] | . as $r | .instances[].attributes
  | to_entries[] | select(.key | test("password|secret|token"; "i"))
  | select(.value != null) | "\($r.type).\($r.name) \(.key)"' terraform.tfstate

Pour un secret précis, on cherche sa valeur : grep -c sur le fichier d'état, et sur le plan enregistré, qui est une archive ZIP (le plan se lit avec python3 -m zipfile ou unzip -p). Une valeur absente des deux est la preuve attendue, comme dans la démonstration de la leçon. Faites aussi la recherche sur les sauvegardes et versions antérieures de l'état.

Exercice 4 : choisir le chiffrement

Un client exige que l'état soit chiffré avec une clé qu'il contrôle, et que Lyneko ne puisse pas lire seul. Quelle architecture proposez-vous avec OpenTofu, et quelles en sont les contraintes ?

Solution

Chiffrement de l'état par OpenTofu avec un fournisseur de clé openbao ou external dont la clé est détenue par le client (un moteur de transit dans son périmètre), méthode aes_gcm, enforced = true pour l'état et le plan. Le bucket reste privé et chiffré côté serveur : deux couches. Contraintes : la CI doit pouvoir joindre le service de clés du client (disponibilité, réseau), la perte de la clé rend l'état irrécupérable (sauvegarde et procédure de séquestre), terraform_remote_state doit connaître la configuration de chiffrement, un module partagé ne doit pas porter le bloc, et l'on ne peut plus relire l'état avec Terraform.

Récapitulatif

  • sensitive masque l'affichage, pas le fichier : l'état contient les secrets en clair, et il circule.
  • Une valeur éphémère (variable, sortie de module, ressource ephemeral) n'est écrite ni dans le plan ni dans l'état, et Terraform la refuse partout où elle serait persistée (Invalid use of ephemeral value). Terraform 1.10 ; OpenTofu 1.11.
  • Un argument en écriture seule (password_wo, data_wo) reçoit le secret sans le garder, apparu avec Terraform 1.11 et OpenTofu 1.11 ; son numéro de version est le seul moyen de déclencher une rotation, parce que Terraform ne compare pas la valeur.
  • Pour lire un secret, la ressource ephemeral "scaleway_secret_version" remplace la source de données, qui l'écrit dans l'état.
  • Le bloc store de terraform_data conserve une valeur éphémère dans l'état : jamais pour un secret.
  • OpenTofu chiffre l'état et les plans (depuis la 1.7) avec un fournisseur de clé et une méthode aes_gcm ; la rotation passe par fallback, et la perte de la clé est définitive.
  • Une fuite passée se traite par une rotation, pas par un correctif de code : les versions antérieures de l'état conservent la valeur.

Pour aller plus loin

  • Le guide « Manage sensitive data » de la documentation de Terraform, la page du bloc ephemeral et les notes de version 1.10 et 1.11.
  • Les notes de version d'OpenTofu 1.11 et la page « State and Plan Encryption », avec la section sur les migrations et les fournisseurs de clés.
  • Le guide des arguments en écriture seule du fournisseur Scaleway, et la page de scaleway_rdb_instance pour la liste à jour des arguments _wo.
  • La leçon 6 pour une politique qui refuse un secret ordinaire, la leçon 8 pour réduire le rayon d'impact d'un état, et le chapitre « Sécuriser » : gestion des secrets à l'échelle de l'organisation (cours à venir).
Voir ma constellation →

Sources