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 :
| Terme | En 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 key | La clé elle-même (en réalité, une suite de versions ; la version « primaire » sert à chiffrer). |
| IAM | Le système de droits de Google Cloud : « tel compte a tel rôle sur telle ressource ». |
| Service agent | Un compte technique créé et géré par Google, qui agit pour un service dans votre projet (ex. le compte de Gemini Enterprise). |
| Secret Manager | Le coffre-fort à secrets de Google Cloud. Un secret a un nom et des versions (les valeurs). |
2. Vue d'ensemble
| Bloc | Type de bloc | Rôle en une phrase |
|---|---|---|
google_kms_key_ring.ge | resource (avec count) | Trousseau multi-région (europe) de la clé principale. |
google_kms_crypto_key.ge | resource (avec count) | Clé principale qui chiffre les données de Gemini Enterprise ; protégée contre la destruction. |
google_kms_key_ring.regional | resource (avec for_each) | Un trousseau par région (europe-west1, europe-west4, europe-north1). |
google_kms_crypto_key.regional | resource (avec for_each) | Une clé régionale par trousseau, exigée pour les connecteurs tiers en ingestion. |
data.google_storage_project_service_account.gcs | data | Lit l'adresse du service agent Cloud Storage du projet. |
locals (all_keys, key_users) | locals | Liste de toutes les clés et des deux comptes qui ont le droit de les utiliser. |
google_kms_crypto_key_iam_member.key_users | resource (avec for_each) | Donne le droit « chiffrer/déchiffrer » à chaque couple (clé, compte). |
google_discovery_engine_cmek_config.ge | resource (avec count) | Enregistre les clés dans Gemini Enterprise, comme clés par défaut. |
locals (secret_ids) | locals | Liste (sans doublon) de tous les noms de secrets cités par les connecteurs. |
data.google_secret_manager_secret.connector | data (avec for_each) | Vérifie que chaque secret existe et lit son identifiant (pas sa valeur). |
google_secret_manager_secret_iam_member.ge_reads | resource (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éfautEt 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 fiche3.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.countdit combien d'exemplaires créer. L'expressioncondition ? A : Best 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 Enterpriseeuexige une clé dans la multi-régioneurope.depends_on = [google_project_service.apis]: force Terraform à activer d'abord les APIs (dontcloudkms.googleapis.com, dans00_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 dedepends_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 toutplan/applyqui détruirait cette clé (y compris unterraform 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é parfor_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.keyeteach.valuesont 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 avecfor_eachest une map{ clé => ressource }. On peut donc boucler directement sur une autre ressource : « une clé par trousseau régional ». Icieach.key= la région ("europe-west1"…) eteach.value= l'objet trousseau complet.key_ring = each.value.id: l'identifiant du trousseau en cours, par exempleprojects/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
datalit 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 formeservice-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 expressionforqui transforme une map en une autre map.locreçoit la clé (la région),kla ressource ; on gardeloc => 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 quandcount = 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é dans00_foundation.tf.storage = "serviceAccount:${...email_address}": l'adresse lue au §3.5, à laquelle on ajoute le préfixeserviceAccount: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.
keys(local.all_keys)renvoie la liste des clés de la map, triées :["europe-north1", "europe-west1", "europe-west4", "main"].keys(local.key_users):["gemini_enterprise", "storage"].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"], ]{ for pair in ... : CLÉ => VALEUR }transforme cette liste en map, carfor_eacha 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 danslocal.all_keyset le compte complet danslocal.key_users.
- la clé est
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 ressourceterraform_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égioneurope). -
set_default = true: cette clé devient la clé par défaut pour tout ce qui sera créé ensuite danseu. -
dynamic "single_region_keys": bloc dynamique. Le sous-blocsingle_region_keys { kms_key = ... }doit être répété une fois par clé régionale.dynamicgé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.valueest l'objet clé, et.idson 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ù ledepends_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.
[for c in var.connectors : ...]: boucle sur les connecteurs (pour une map, avec une seule variable,creçoit la valeur de chaque entrée).values(c.secret_params): les valeurs de la map, donc les noms de secrets. Poursharepoint:["sharepoint-client-id", "sharepoint-client-secret"]. Pourgdrive(pas desecret_params, défaut{}) :[].- On obtient une liste de listes :
[[], [], ["github-client-id", "github-client-secret"], ["sharepoint-client-id", "sharepoint-client-secret"], ["teams-client-id", "teams-client-secret"]]. flatten(...)l'aplatit en une seule liste de 6 noms.toset(...)retire les éventuels doublons (deux connecteurs qui partagent un secret) et donne un set utilisable parfor_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(dans40_connectors.tf, au formatprojects/<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 Cloud | Nombre |
|---|---|
Trousseau KMS gemini-enterprise dans europe | 1 |
Clé gemini-enterprise-data (dans europe) | 1 |
Trousseaux gemini-enterprise dans europe-west1, europe-west4, europe-north1 | 3 |
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 Enterprise | 6 |
| Total ressources gérées | 23 |
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 :
| Valeur | Vient de |
|---|---|
var.enable_cmek, var.kms_key_ring, var.ge_location, var.connectors | variables.tf (02-variables.md) et terraform.tfvars (12-tfvars.md) |
local.kms_location, local.regional_kms_locations, local.ge_sa | 00_foundation.tf (03-foundation.md) |
google_project_service.apis, terraform_data.service_agents | 00_foundation.tf |
Ce fichier est utilisé par :
| Valeur | Utilisée dans |
|---|---|
google_kms_crypto_key.ge[0].id | 50_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.ge | depends_on de l'app (50_app.tf) et des connecteurs (40_connectors.tf) |
data.google_secret_manager_secret.connector[...].name | json_params des connecteurs (40_connectors.tf) |
google_secret_manager_secret_iam_member.ge_reads | depends_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 :
- Lire la doc CMEK du service à chiffrer : quel emplacement de clé (région ou multi-région) ? quel service agent ? rotation automatique autorisée ?
- 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 - Donner
cryptoKeyEncrypterDecrypterau service agent, sur la clé uniquement : https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key_iam - Brancher la clé dans la ressource (argument
kms_key_name,encryption… selon le service), avec undepends_onsur le droit. Pour Gemini Enterprise : https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/discovery_engine_cmek_config - 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
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_configreste chiffré par Google. Ledepends_ondu côté app/connecteurs le garantit. Activer CMEK après coup ne rechiffre pas l'existant : il faut le décider dès le premierapply. - CMEK impossible en
global: seulementeuouus(ge_locationest validé dansvariables.tf). prevent_destroybloque aussiterraform 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). countet[0]: toute référence àgoogle_kms_crypto_key.ge[0]doit être protégée parvar.enable_cmek ? ... : ..., sinon l'erreur « index 0 out of range » apparaît quand CMEK est désactivé._iam_member, pas_iam_policy:_memberajoute un droit ;_policy/_bindingécrasent ceux posés par d'autres équipes.- Les secrets doivent exister avant
plan: sinon la sourcedataé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_eachstables : les clés du type"main|storage"ne changent pas si on ajoute une région ou un compte. Aveccount, un ajout décalerait les index et Terraform voudrait recréer des droits.