Leçon 6 — Le code Terraform, couche par couche

05. `20_data_protection.tf` : CMEK et secrets des connecteurs

20 min de lecture

Ce fichier crée les clés de chiffrement du client (CMEK), les enregistre dans Gemini Enterprise, et autorise Gemini Enterprise à lire les secrets des connecteurs sans jamais en connaître la valeur dans Terraform. Notions de base : 00-bases-terraform.md.


1. À quoi sert ce fichier

Tout ce que Gemini Enterprise stocke (index des connecteurs en ingestion, conversations, données de l'app) est chiffré avec une clé qui appartient au projet du client, et non avec une clé gérée par Google. Le client peut désactiver cette clé à tout moment : les données deviennent illisibles (« crypto-shredding »). Le fichier gère aussi les secrets des connecteurs (identifiants OAuth SharePoint, GitHub…) : Terraform ne connaît que leur nom, jamais leur valeur. Le « pourquoi » complet est dans le guide : README §6.1 et README §6.2.

Vocabulaire Google Cloud :

TermeEn clair
KMS (Cloud Key Management Service)Le service Google qui garde les clés de chiffrement. La clé ne sort jamais de KMS : les services lui demandent de chiffrer/déchiffrer.
CMEK (Customer-Managed Encryption Key)« Clé gérée par le client » : un service Google chiffre vos données avec votre clé KMS.
Key ring (trousseau)Un dossier qui regroupe des clés, à un emplacement donné (europe, europe-west1…).
Crypto keyLa clé elle-même (en réalité, une suite de versions ; la version « primaire » sert à chiffrer).
IAMLe système de droits de Google Cloud : « tel compte a tel rôle sur telle ressource ».
Service agentUn compte technique créé et géré par Google, qui agit pour un service dans votre projet (ex. le compte de Gemini Enterprise).
Secret ManagerLe coffre-fort à secrets de Google Cloud. Un secret a un nom et des versions (les valeurs).

2. Vue d'ensemble

BlocType de blocRôle en une phrase
google_kms_key_ring.geresource (avec count)Trousseau multi-région (europe) de la clé principale.
google_kms_crypto_key.geresource (avec count)Clé principale qui chiffre les données de Gemini Enterprise ; protégée contre la destruction.
google_kms_key_ring.regionalresource (avec for_each)Un trousseau par région (europe-west1, europe-west4, europe-north1).
google_kms_crypto_key.regionalresource (avec for_each)Une clé régionale par trousseau, exigée pour les connecteurs tiers en ingestion.
data.google_storage_project_service_account.gcsdataLit l'adresse du service agent Cloud Storage du projet.
locals (all_keys, key_users)localsListe de toutes les clés et des deux comptes qui ont le droit de les utiliser.
google_kms_crypto_key_iam_member.key_usersresource (avec for_each)Donne le droit « chiffrer/déchiffrer » à chaque couple (clé, compte).
google_discovery_engine_cmek_config.geresource (avec count)Enregistre les clés dans Gemini Enterprise, comme clés par défaut.
locals (secret_ids)localsListe (sans doublon) de tous les noms de secrets cités par les connecteurs.
data.google_secret_manager_secret.connectordata (avec for_each)Vérifie que chaque secret existe et lit son identifiant (pas sa valeur).
google_secret_manager_secret_iam_member.ge_readsresource (avec for_each)Autorise Gemini Enterprise à lire chaque secret, un par un.

3. Le code expliqué, bloc par bloc

Valeurs d'exemple utilisées tout au long de la fiche (celles de terraform.tfvars.example + défauts de variables.tf) :

project_id   = "acme-gemini-enterprise-prd"
ge_location  = "eu"                 # défaut
enable_cmek  = true                 # défaut
kms_key_ring = "gemini-enterprise"  # défaut

Et les valeurs calculées dans 00_foundation.tf (voir 03-foundation.md) :

local.kms_location           = "europe"                                        # car ge_location = "eu"
local.regional_kms_locations = ["europe-west1", "europe-west4", "europe-north1"]
local.ge_sa                  = "serviceAccount:service-123456789012@gcp-sa-discoveryengine.iam.gserviceaccount.com"
# 123456789012 = numéro de projet fictif, utilisé dans toute la fiche

3.1 Le trousseau principal

# COUCHE 2 - PROTECTION DES DONNÉES : CMEK (clés du client) + secrets.

# 2.1 Clé principale, multi-région "europe" pour une app "eu".
#     Rotation MANUELLE : exigence Gemini Enterprise (pas de rotation_period).
resource "google_kms_key_ring" "ge" {
  count      = var.enable_cmek ? 1 : 0
  name       = var.kms_key_ring
  location   = local.kms_location
  depends_on = [google_project_service.apis]
}
  • count = var.enable_cmek ? 1 : 0 : première notion. count dit combien d'exemplaires créer. L'expression condition ? A : B est un ternaire (« si… alors… sinon… ») : 1 exemplaire si CMEK est activé, 0 sinon (la ressource n'existe alors pas). Avec l'exemple, enable_cmek = true → 1 trousseau. On y fera référence avec [0] : google_kms_key_ring.ge[0]. Voir 00-bases §8 et §12.1.
  • name = var.kms_key_ring : le nom du trousseau, "gemini-enterprise" par défaut.
  • location = local.kms_location : "europe". C'est une multi-région KMS (plusieurs régions européennes). Règle Google : une app Gemini Enterprise eu exige une clé dans la multi-région europe.
  • depends_on = [google_project_service.apis] : force Terraform à activer d'abord les APIs (dont cloudkms.googleapis.com, dans 00_foundation.tf). Sans cela, Terraform pourrait tenter de créer le trousseau avant que l'API KMS soit active. Voir 00-bases §9.
  • Pas de rotation_period (ici ni sur la clé) : Gemini Enterprise exige une rotation manuelle (le commentaire le rappelle).

Identifiant obtenu (google_kms_key_ring.ge[0].id) : projects/acme-gemini-enterprise-prd/locations/europe/keyRings/gemini-enterprise.

3.2 La clé principale

resource "google_kms_crypto_key" "ge" {
  count    = var.enable_cmek ? 1 : 0
  name     = "gemini-enterprise-data"
  key_ring = google_kms_key_ring.ge[0].id
  purpose  = "ENCRYPT_DECRYPT"
  labels   = { usage = "gemini-enterprise", owner = "security" }

  # Détruire cette clé = rendre TOUTES les données indexées illisibles (crypto-shredding).
  # Terraform refuse donc de la supprimer. Voir le guide, section "Désactiver la clé".
  lifecycle {
    prevent_destroy = true
  }
}
  • count : même logique que le trousseau (les deux existent ensemble ou pas du tout).
  • name = "gemini-enterprise-data" : nom fixe de la clé.
  • key_ring = google_kms_key_ring.ge[0].id : référence à l'identifiant du trousseau créé juste avant. Grâce à cette référence, Terraform sait qu'il doit créer le trousseau avant la clé (pas besoin de depends_on). Voir 00-bases §7.
  • purpose = "ENCRYPT_DECRYPT" : clé symétrique de chiffrement, le type attendu par CMEK.
  • labels : étiquettes libres (map) pour retrouver la clé (inventaire, facturation, audit).
  • lifecycle { prevent_destroy = true } : Terraform refuse tout plan/apply qui détruirait cette clé (y compris un terraform destroy). Détruire la clé rendrait les données illisibles pour toujours. Pour révoquer les données, on désactive la version de clé, à la main, selon une procédure (README §14.3). Voir 00-bases §11.

Identifiant obtenu (google_kms_crypto_key.ge[0].id) : projects/acme-gemini-enterprise-prd/locations/europe/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-data.

3.3 Les trousseaux régionaux

# 2.2 Clés régionales : exigées pour les connecteurs TIERS en mode ingestion.
resource "google_kms_key_ring" "regional" {
  for_each   = var.enable_cmek ? toset(local.regional_kms_locations) : toset([])
  name       = var.kms_key_ring
  location   = each.value
  depends_on = [google_project_service.apis]
}
  • for_each : crée un exemplaire par élément d'un ensemble (set) ou d'une map. Ici :
    • toset(...) transforme la liste en ensemble (pas de doublon, pas d'ordre), le type accepté par for_each.
    • Le ternaire donne les 3 régions si CMEK est activé, un ensemble vide toset([]) sinon (donc 0 trousseau). Les deux branches ont le même type (un set), ce que Terraform exige.
    • Avec l'exemple : toset(["europe-west1", "europe-west4", "europe-north1"]) → 3 trousseaux.
  • each.value : l'élément en cours de la boucle. Pour un set de textes, each.key et each.value sont identiques ("europe-west1", puis "europe-west4", puis "europe-north1").
  • name = var.kms_key_ring : même nom "gemini-enterprise" que le trousseau principal. C'est possible car un nom de trousseau n'est unique que dans sa région.
  • Adresses obtenues : google_kms_key_ring.regional["europe-west1"], ["europe-west4"], ["europe-north1"].

Voir 00-bases §8.2 et §12.4 pour toset.

3.4 Les clés régionales

resource "google_kms_crypto_key" "regional" {
  for_each = google_kms_key_ring.regional
  name     = "gemini-enterprise-connectors"
  key_ring = each.value.id
  purpose  = "ENCRYPT_DECRYPT"
  labels   = { usage = "gemini-enterprise-connectors", owner = "security" }
  lifecycle {
    prevent_destroy = true
  }
}
  • for_each = google_kms_key_ring.regional : astuce utile. Une ressource créée avec for_each est une map { clé => ressource }. On peut donc boucler directement sur une autre ressource : « une clé par trousseau régional ». Ici each.key = la région ("europe-west1"…) et each.value = l'objet trousseau complet.
  • key_ring = each.value.id : l'identifiant du trousseau en cours, par exemple projects/acme-gemini-enterprise-prd/locations/europe-west1/keyRings/gemini-enterprise.
  • name = "gemini-enterprise-connectors" : nom distinct de la clé principale, pour lire facilement à quoi elle sert.
  • prevent_destroy = true : même protection que la clé principale.
  • Résultat : 3 clés, adresses google_kms_crypto_key.regional["europe-west1"], etc.

3.5 Le service agent Cloud Storage

# 2.3 Seuls Gemini Enterprise et le stockage temporaire d'import peuvent utiliser les clés.
data "google_storage_project_service_account" "gcs" {}
  • Un bloc data lit une information, il ne crée rien de durable. Voir 00-bases §6.2.
  • Celui-ci renvoie l'adresse du service agent Cloud Storage du projet, dans l'attribut email_address, de la forme service-123456789012@gs-project-accounts.iam.gserviceaccount.com.
  • Pourquoi : lors d'un import, Gemini Enterprise passe par un bucket temporaire Cloud Storage. Ce bucket est chiffré avec la même clé : le service agent Cloud Storage doit donc pouvoir l'utiliser (règle Google, README §6.1).
  • {} vide : aucun argument, le projet est celui du provider.

3.6 Les valeurs calculées : toutes les clés, tous les utilisateurs de clés

locals {
  all_keys = var.enable_cmek ? merge(
    { main = google_kms_crypto_key.ge[0].id },
    { for loc, k in google_kms_crypto_key.regional : loc => k.id },
  ) : {}
  key_users = {
    gemini_enterprise = local.ge_sa
    storage           = "serviceAccount:${data.google_storage_project_service_account.gcs.email_address}"
  }
}

Un bloc locals donne un nom à un calcul pour le réutiliser (lecture : local.all_keys). Voir 00-bases §6.4.

all_keys : une map « nom court → identifiant de clé », pour les 4 clés.

  • { for loc, k in google_kms_crypto_key.regional : loc => k.id } est une expression for qui transforme une map en une autre map. loc reçoit la clé (la région), k la ressource ; on garde loc => k.id. Voir 00-bases §12.2.
  • merge(m1, m2) fusionne deux maps en une seule.
  • Le ternaire renvoie {} (map vide) si CMEK est désactivé. Important : sans ce ternaire, google_kms_crypto_key.ge[0] provoquerait une erreur, car l'élément [0] n'existe pas quand count = 0.

Résultat avec l'exemple :

local.all_keys = {
  "main"          = "projects/acme-gemini-enterprise-prd/locations/europe/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-data"
  "europe-west1"  = "projects/acme-gemini-enterprise-prd/locations/europe-west1/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-connectors"
  "europe-west4"  = "projects/acme-gemini-enterprise-prd/locations/europe-west4/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-connectors"
  "europe-north1" = "projects/acme-gemini-enterprise-prd/locations/europe-north1/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-connectors"
}

key_users : les deux seuls comptes autorisés à utiliser les clés.

  • gemini_enterprise = local.ge_sa : le service agent de Gemini Enterprise, déjà au format IAM (serviceAccount:...), calculé dans 00_foundation.tf.
  • storage = "serviceAccount:${...email_address}" : l'adresse lue au §3.5, à laquelle on ajoute le préfixe serviceAccount: exigé par IAM. ${ ... } est une interpolation (insérer une valeur dans un texte).
local.key_users = {
  "gemini_enterprise" = "serviceAccount:service-123456789012@gcp-sa-discoveryengine.iam.gserviceaccount.com"
  "storage"           = "serviceAccount:service-123456789012@gs-project-accounts.iam.gserviceaccount.com"
}

3.7 Les droits sur les clés : toutes les combinaisons avec setproduct

resource "google_kms_crypto_key_iam_member" "key_users" {
  for_each      = { for pair in setproduct(keys(local.all_keys), keys(local.key_users)) : "${pair[0]}|${pair[1]}" => { key = local.all_keys[pair[0]], member = local.key_users[pair[1]] } }
  crypto_key_id = each.value.key
  role          = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
  member        = each.value.member
  depends_on    = [terraform_data.service_agents]
}

Le besoin : donner le droit à chaque compte sur chaque clé, soit 4 clés × 2 comptes = 8 droits. La ligne for_each construit ces 8 combinaisons. Décomposons-la, de l'intérieur vers l'extérieur.

  1. keys(local.all_keys) renvoie la liste des clés de la map, triées : ["europe-north1", "europe-west1", "europe-west4", "main"].
  2. keys(local.key_users) : ["gemini_enterprise", "storage"].
  3. setproduct(l1, l2) renvoie toutes les combinaisons (produit cartésien), chaque combinaison étant une liste de 2 éléments :
    [
      ["europe-north1", "gemini_enterprise"], ["europe-north1", "storage"],
      ["europe-west1",  "gemini_enterprise"], ["europe-west1",  "storage"],
      ["europe-west4",  "gemini_enterprise"], ["europe-west4",  "storage"],
      ["main",          "gemini_enterprise"], ["main",          "storage"],
    ]
  4. { for pair in ... : CLÉ => VALEUR } transforme cette liste en map, car for_each a besoin d'une clé stable et lisible pour chaque élément :
    • la clé est "${pair[0]}|${pair[1]}", par exemple "main|gemini_enterprise" (pair[0] = 1er élément, pair[1] = 2e) ; le | est juste un séparateur choisi par l'auteur ;
    • la valeur est un petit objet { key = ..., member = ... } qui va rechercher l'identifiant complet dans local.all_keys et le compte complet dans local.key_users.

Résultat (extrait) :

{
  "europe-north1|gemini_enterprise" = {
    key    = "projects/acme-gemini-enterprise-prd/locations/europe-north1/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-connectors"
    member = "serviceAccount:service-123456789012@gcp-sa-discoveryengine.iam.gserviceaccount.com"
  }
  "europe-north1|storage" = { key = "...europe-north1...", member = "serviceAccount:service-123456789012@gs-project-accounts.iam.gserviceaccount.com" }
  "europe-west1|gemini_enterprise" = { ... }
  "europe-west1|storage"           = { ... }
  "europe-west4|gemini_enterprise" = { ... }
  "europe-west4|storage"           = { ... }
  "main|gemini_enterprise"         = { key = "...locations/europe/.../cryptoKeys/gemini-enterprise-data", member = "...gcp-sa-discoveryengine..." }
  "main|storage"                   = { ... }
}

Adresse d'un droit : google_kms_crypto_key_iam_member.key_users["main|storage"]. Si CMEK est désactivé, local.all_keys = {}, donc la map est vide et aucun droit n'est créé.

Les arguments :

  • crypto_key_id = each.value.key : la clé concernée.
  • role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" : seulement chiffrer et déchiffrer. Ce rôle ne permet ni de gérer, ni de désactiver, ni de détruire la clé (moindre privilège).
  • member = each.value.member : le compte qui reçoit le droit.
  • depends_on = [terraform_data.service_agents] : les service agents de Google sont créés « à la demande ». La ressource terraform_data.service_agents (00_foundation.tf) force leur création. Sans cet ordre, IAM refuserait un compte qui n'existe pas encore.

Pourquoi google_kms_crypto_key_iam_member (et non ..._iam_policy) : _member ajoute un droit sans toucher aux autres. _policy remplacerait tous les droits de la clé (dangereux si l'équipe sécurité en a ajouté).

3.8 L'enregistrement de la clé dans Gemini Enterprise

# 2.4 Enregistrement de la clé dans Gemini Enterprise : AVANT toute app ou connecteur,
#     sinon ceux créés avant restent chiffrés par Google.
resource "google_discovery_engine_cmek_config" "ge" {
  count          = var.enable_cmek ? 1 : 0
  location       = var.ge_location
  cmek_config_id = "default-cmek"
  kms_key        = google_kms_crypto_key.ge[0].id
  set_default    = true

  dynamic "single_region_keys" {
    for_each = google_kms_crypto_key.regional
    content {
      kms_key = single_region_keys.value.id
    }
  }

  depends_on = [google_kms_crypto_key_iam_member.key_users]
}

Créer une clé KMS ne suffit pas : il faut dire à Gemini Enterprise de l'utiliser. C'est le rôle de ce bloc (« Discovery Engine » est le nom technique de l'API de Gemini Enterprise).

  • location = var.ge_location : "eu", l'emplacement de l'app.

  • cmek_config_id = "default-cmek" : nom de cette configuration.

  • kms_key = google_kms_crypto_key.ge[0].id : la clé principale (multi-région europe).

  • set_default = true : cette clé devient la clé par défaut pour tout ce qui sera créé ensuite dans eu.

  • dynamic "single_region_keys" : bloc dynamique. Le sous-bloc single_region_keys { kms_key = ... } doit être répété une fois par clé régionale. dynamic génère ces répétitions :

    • for_each = google_kms_crypto_key.regional : la map des 3 clés régionales ;
    • dans content, l'élément en cours s'appelle comme le bloc : single_region_keys.value est l'objet clé, et .id son identifiant.

    Avec l'exemple, c'est équivalent à écrire à la main :

    single_region_keys { kms_key = "projects/acme-gemini-enterprise-prd/locations/europe-north1/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-connectors" }
    single_region_keys { kms_key = "projects/.../locations/europe-west1/.../cryptoKeys/gemini-enterprise-connectors" }
    single_region_keys { kms_key = "projects/.../locations/europe-west4/.../cryptoKeys/gemini-enterprise-connectors" }

    Voir 00-bases §10.

  • depends_on = [google_kms_crypto_key_iam_member.key_users] : les droits doivent exister avant l'enregistrement. Sinon Gemini Enterprise ne pourrait pas utiliser la clé au moment où on la lui confie. Cette dépendance est invisible dans le code (aucun attribut des droits n'est utilisé), d'où le depends_on.

Point clé : l'app (50_app.tf) et les connecteurs (40_connectors.tf) déclarent depends_on = [google_discovery_engine_cmek_config.ge]. Tout ce qui est créé avant l'enregistrement reste chiffré par Google.

3.9 La liste des secrets cités par les connecteurs

# 2.5 Secrets des connecteurs : JAMAIS dans le code ni dans l'état Terraform.
#     L'équipe sécurité crée les secrets (valeurs) ; Terraform ne fait que les référencer
#     et autoriser Gemini Enterprise à les lire, secret par secret.
locals {
  secret_ids = toset(flatten([for c in var.connectors : values(c.secret_params)]))
}

Chaque connecteur de var.connectors peut avoir un champ secret_params : une map « nom du paramètre → nom du secret dans Secret Manager ». On veut la liste de tous les noms de secrets, sans doublon.

  1. [for c in var.connectors : ...] : boucle sur les connecteurs (pour une map, avec une seule variable, c reçoit la valeur de chaque entrée).
  2. values(c.secret_params) : les valeurs de la map, donc les noms de secrets. Pour sharepoint : ["sharepoint-client-id", "sharepoint-client-secret"]. Pour gdrive (pas de secret_params, défaut {}) : [].
  3. On obtient une liste de listes : [[], [], ["github-client-id", "github-client-secret"], ["sharepoint-client-id", "sharepoint-client-secret"], ["teams-client-id", "teams-client-secret"]].
  4. flatten(...) l'aplatit en une seule liste de 6 noms.
  5. toset(...) retire les éventuels doublons (deux connecteurs qui partagent un secret) et donne un set utilisable par for_each.
local.secret_ids = toset([
  "github-client-id", "github-client-secret",
  "sharepoint-client-id", "sharepoint-client-secret",
  "teams-client-id", "teams-client-secret",
])

3.10 La lecture des secrets (métadonnées seulement)

data "google_secret_manager_secret" "connector" {
  for_each  = local.secret_ids
  secret_id = each.value
}
  • for_each = local.secret_ids : une lecture par secret (6 avec l'exemple).
  • secret_id = each.value : le nom du secret, par ex. "sharepoint-client-secret".
  • Cette source lit le secret (son nom, son identifiant, sa réplication), pas ses versions, donc jamais la valeur. La valeur ne se retrouve ni dans le code, ni dans l'état Terraform.
  • Effet secondaire utile : si un secret n'existe pas, terraform plan échoue tout de suite, avec un message clair.
  • Attributs utilisés ensuite : .id (ici, dans les droits) et .name (dans 40_connectors.tf, au format projects/<numéro>/secrets/<nom>).

3.11 Le droit de lecture de Gemini Enterprise, secret par secret

resource "google_secret_manager_secret_iam_member" "ge_reads" {
  for_each   = local.secret_ids
  secret_id  = data.google_secret_manager_secret.connector[each.value].id
  role       = "roles/secretmanager.secretAccessor"
  member     = local.ge_sa
  depends_on = [terraform_data.service_agents]
}
  • for_each = local.secret_ids : un droit par secret (6 avec l'exemple).
  • secret_id = data.google_secret_manager_secret.connector[each.value].id : l'identifiant du secret lu juste avant, retrouvé par sa clé (connector["sharepoint-client-secret"]).
  • role = "roles/secretmanager.secretAccessor" : droit de lire la valeur du secret, rien d'autre.
  • member = local.ge_sa : le service agent de Gemini Enterprise. Le droit est posé sur chaque secret, pas sur le projet : Gemini Enterprise ne peut lire que les secrets des connecteurs. Le choix de ce compte est à vérifier au premier déploiement (README §16).
  • depends_on = [terraform_data.service_agents] : même raison qu'au §3.7.

4. Ce que Terraform crée concrètement

Avec terraform.tfvars.example (CMEK activé, app eu, 5 connecteurs dont 3 avec secrets) :

Ce qui apparaît dans Google CloudNombre
Trousseau KMS gemini-enterprise dans europe1
Clé gemini-enterprise-data (dans europe)1
Trousseaux gemini-enterprise dans europe-west1, europe-west4, europe-north13
Clés gemini-enterprise-connectors (une par région)3
Droits « chiffrer/déchiffrer » (4 clés × 2 comptes)8
Configuration CMEK default-cmek dans Gemini Enterprise (eu)1
Droits « lire le secret » pour Gemini Enterprise6
Total ressources gérées23

Plus 7 lectures (data) : le service agent Cloud Storage et les 6 secrets. Aucun secret n'est créé : l'équipe sécurité les crée avant (voir README §7.6).

Dans la console : Sécurité → Key Management (trousseaux et clés), Secret Manager → onglet Autorisations de chaque secret, et Gemini Enterprise → Paramètres pour la clé CMEK enregistrée.

Avec enable_cmek = false : seules les 6 autorisations de secrets sont créées.


5. Dépendances avec les autres fichiers

Ce fichier utilise :

ValeurVient de
var.enable_cmek, var.kms_key_ring, var.ge_location, var.connectorsvariables.tf (02-variables.md) et terraform.tfvars (12-tfvars.md)
local.kms_location, local.regional_kms_locations, local.ge_sa00_foundation.tf (03-foundation.md)
google_project_service.apis, terraform_data.service_agents00_foundation.tf

Ce fichier est utilisé par :

ValeurUtilisée dans
google_kms_crypto_key.ge[0].id50_app.tf (kms_key_name de l'app, 08-app.md), 40_connectors.tf (connecteurs en ingestion, 07-connectors.md), outputs.tf (cmek_key, 11-outputs.md)
google_discovery_engine_cmek_config.gedepends_on de l'app (50_app.tf) et des connecteurs (40_connectors.tf)
data.google_secret_manager_secret.connector[...].namejson_params des connecteurs (40_connectors.tf)
google_secret_manager_secret_iam_member.ge_readsdepends_on des connecteurs (40_connectors.tf)

L'alerte « garde-fou modifié » de 70_detection.tf (10-detection.md) surveille les opérations sensibles sur les clés KMS (destruction de version, changement de version primaire, droits).


6. Pour reproduire / adapter

Besoin type : chiffrer un autre service avec une clé CMEK (ici un bucket Cloud Storage, à adapter).

# 1. Le trousseau : choisir le MÊME emplacement que la ressource à chiffrer.
resource "google_kms_key_ring" "mon_service" {
  name     = "mon-service"            # unique dans l'emplacement ; NE PEUT PAS être supprimé ensuite
  location = "europe-west1"
}

# 2. La clé.
resource "google_kms_crypto_key" "mon_service" {
  name     = "mon-service-data"
  key_ring = google_kms_key_ring.mon_service.id
  purpose  = "ENCRYPT_DECRYPT"
  # rotation_period = "7776000s"      # 90 jours, SI le service accepte la rotation automatique
  lifecycle {
    prevent_destroy = true            # jamais de destruction par erreur
  }
}

# 3. Le service agent du service qui chiffrera (ici Cloud Storage).
data "google_storage_project_service_account" "gcs" {}

# 4. Le droit minimal : chiffrer / déchiffrer, sur CETTE clé seulement.
resource "google_kms_crypto_key_iam_member" "mon_service" {
  crypto_key_id = google_kms_crypto_key.mon_service.id
  role          = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
  member        = "serviceAccount:${data.google_storage_project_service_account.gcs.email_address}"
}

# 5. La ressource chiffrée, créée APRÈS le droit.
resource "google_storage_bucket" "donnees" {
  name     = "mon-projet-donnees-chiffrees"
  location = "EUROPE-WEST1"
  encryption {
    default_kms_key_name = google_kms_crypto_key.mon_service.id
  }
  depends_on = [google_kms_crypto_key_iam_member.mon_service]
}

Besoin type : donner à un service l'accès à un secret existant (sans jamais manipuler la valeur) :

data "google_secret_manager_secret" "s" {
  secret_id = "mon-secret"            # créé AVANT par l'équipe sécurité (gcloud secrets create ...)
}

resource "google_secret_manager_secret_iam_member" "s" {
  secret_id = data.google_secret_manager_secret.s.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:mon-compte@mon-projet.iam.gserviceaccount.com"
}
# Référence à passer au service : "${data.google_secret_manager_secret.s.name}/versions/latest"

Étapes :

  1. Lire la doc CMEK du service à chiffrer : quel emplacement de clé (région ou multi-région) ? quel service agent ? rotation automatique autorisée ?
  2. Créer trousseau + clé avec prevent_destroy : https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_key_ring et https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key
  3. Donner cryptoKeyEncrypterDecrypter au service agent, sur la clé uniquement : https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key_iam
  4. Brancher la clé dans la ressource (argument kms_key_name, encryption… selon le service), avec un depends_on sur le droit. Pour Gemini Enterprise : https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/discovery_engine_cmek_config
  5. Pour les secrets : https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/secret_manager_secret et https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_iam
  6. terraform plan : vérifier qu'aucune ligne - ou -/+ ne touche une clé existante.

7. Pièges et bonnes pratiques

  • Enregistrer la clé AVANT l'app et les connecteurs. Ce qui est créé avant google_discovery_engine_cmek_config reste chiffré par Google. Le depends_on du côté app/connecteurs le garantit. Activer CMEK après coup ne rechiffre pas l'existant : il faut le décider dès le premier apply.
  • CMEK impossible en global : seulement eu ou us (ge_location est validé dans variables.tf).
  • prevent_destroy bloque aussi terraform destroy. C'est voulu (README §15). Pour révoquer les données, on désactive la version de clé (réversible 30 jours), on ne la détruit pas (README §14.3).
  • Un trousseau KMS ne peut jamais être supprimé dans Google Cloud (une clé non plus, seules ses versions peuvent être détruites). Si Terraform « supprime » un trousseau, il l'oublie simplement dans l'état, et le nom reste pris pour toujours dans cet emplacement. Choisissez les noms avec soin.
  • Rotation manuelle uniquement pour la clé de Gemini Enterprise : pas de rotation_period. Pour tourner : nouvelle version, la rendre primaire, garder l'ancienne active 30 jours. Ne pas faire tourner les clés régionales (README §14.4).
  • count et [0] : toute référence à google_kms_crypto_key.ge[0] doit être protégée par var.enable_cmek ? ... : ..., sinon l'erreur « index 0 out of range » apparaît quand CMEK est désactivé.
  • _iam_member, pas _iam_policy : _member ajoute un droit ; _policy/_binding écrasent ceux posés par d'autres équipes.
  • Les secrets doivent exister avant plan : sinon la source data échoue. Ordre : l'équipe sécurité crée les secrets, puis on lance Terraform (README §7.6).
  • Ne jamais utiliser google_secret_manager_secret_version (ressource ou data) pour les secrets de connecteurs : la valeur finirait en clair dans l'état Terraform. Ici on ne lit que le secret, et on passe une référence .../versions/latest ; une rotation ne demande aucun redéploiement (README §14.5).
  • Le compte qui lit les secrets (service agent Gemini Enterprise) est une hypothèse : à vérifier au premier déploiement (README §16).
  • Clés for_each stables : les clés du type "main|storage" ne changent pas si on ajoute une région ou un compte. Avec count, un ajout décalerait les index et Terraform voudrait recréer des droits.
Suivant