# ── Un compte de service par APP, sans liste à tenir ───────────────────────── # # Il n'y a PAS de liste de consommateurs : elle se lit dans `values.yaml` du # chart. Chaque bucket y dit à quelle app il appartient (`app:`) — et c'est la # SEULE déclaration (fondateur 2026-07-26 : « je préfère ne pas m'embêter avec # consumers ou autre »). Rien à synchroniser entre deux fichiers, donc rien à # oublier, et aucune divergence possible : c'est le fichier qu'Helm consomme. # # UNE APP PEUT AVOIR PLUSIEURS BUCKETS, et c'est important : rien ne justifiait # de l'en empêcher. L'ADR-018 de Kadans prévoit déjà deux paliers de transfert # (aperçu 240p régénérable, travail 360p à garder) — deux cycles de vie, donc # potentiellement deux buckets aux politiques de purge différentes. Le compte de # service est donc par APP, autorisé sur TOUS ses buckets et sur eux seuls. # # POURQUOI LES CLÉS VIVENT ICI ET PAS CHEZ L'APP : seul ce pipeline possède les # identifiants ROOT de MinIO. Si chaque app créait son propre compte de service, # il faudrait donner ce root à chaque rôle CI — c'est-à-dire à tout le monde. # Ici il ne sort jamais, et l'app ne reçoit qu'une clé qui ne peut rien lire # d'autre que ses propres buckets. Un compte qui fuite ne donne accès qu'aux # objets qu'il gérait déjà. locals { # Le MÊME fichier que celui qu'Helm consomme : impossible de créer un bucket # sans son compte de service, ou un compte sans ses buckets. buckets_declares = yamldecode(file("${path.module}/../values.yaml")).minio.buckets # app → ses buckets. `try` : un bucket sans `app:` est ignoré plutôt que de # faire échouer tout le plan — il n'aura simplement aucun compte de service, # ce qui se voit tout de suite et ne casse rien d'existant. buckets_par_app = { for app in distinct([for b in local.buckets_declares : try(b.app, null) if try(b.app, null) != null]) : app => [for b in local.buckets_declares : b.name if try(b.app, null) == app] } } # La politique d'accès : SES buckets, rien d'autre. Ni les autres buckets, ni # l'administration. resource "minio_iam_policy" "app" { for_each = local.buckets_par_app name = "${each.key}-app" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] Resource = [for b in each.value : "arn:aws:s3:::${b}/*"] }, { # Nécessaire pour qu'un client S3 vérifie l'existence d'un bucket et # liste SES objets — jamais ceux d'une autre app. Effect = "Allow" Action = ["s3:ListBucket", "s3:GetBucketLocation"] Resource = [for b in each.value : "arn:aws:s3:::${b}"] }, ] }) } resource "random_password" "app" { for_each = local.buckets_par_app length = 40 special = false # les outils S3 transportent mal certains caractères en URL } resource "minio_iam_user" "app" { for_each = local.buckets_par_app name = "${each.key}-app" # Le mot de passe EST la clé secrète S3 : généré ici, jamais choisi. secret = random_password.app[each.key].result # `false` : une rotation du mot de passe ne recrée pas l'utilisateur, donc les # objets déjà déposés gardent leur propriétaire. force_destroy = false } resource "minio_iam_user_policy_attachment" "app" { for_each = local.buckets_par_app user_name = minio_iam_user.app[each.key].id policy_name = minio_iam_policy.app[each.key].id } # Les clés, dans l'espace Vault de MINIO. L'app y accède par la règle que # `app_policy` accorde à toutes : `kvv2/data/minio/`. # # `MINIO_BUCKETS` liste ses buckets pour que l'app n'ait pas à les redéclarer de # son côté ; `MINIO_BUCKET` reste le premier, pour les apps qui n'en ont qu'un. resource "vault_kv_secret_v2" "app" { for_each = local.buckets_par_app mount = "kvv2" name = "minio/${each.key}" data_json = jsonencode({ MINIO_ENDPOINT = var.minio_endpoint MINIO_BUCKET = each.value[0] MINIO_BUCKETS = join(",", each.value) MINIO_ACCESS_KEY = minio_iam_user.app[each.key].id MINIO_SECRET_KEY = random_password.app[each.key].result }) }