diff --git a/hashicorp-vault/iac/terraform.tfvars b/hashicorp-vault/iac/terraform.tfvars index b0921e7..14cdff4 100644 --- a/hashicorp-vault/iac/terraform.tfvars +++ b/hashicorp-vault/iac/terraform.tfvars @@ -24,5 +24,13 @@ applications = [ service_account_namespaces = ["tools"] }, { name = "prospection" }, - { name = "kadans" }, + { + name = "kadans" + # Le pod lit les clés de SON compte de service MinIO, écrites par le pipeline + # `minio` dans son propre espace (minio/iac/consumers.tf). Le mécanisme est + # celui qui existait déjà pour l'ERP et les creds GCS de Longhorn — aucun + # module central n'a eu à changer. Le root de MinIO, lui, ne sort jamais du + # pipeline minio : l'app ne reçoit qu'une clé bornée à son bucket. + kv_read_paths = ["kvv2/data/minio/kadans"] + }, ] diff --git a/minio/README.md b/minio/README.md index a84d3cd..2e8276d 100644 --- a/minio/README.md +++ b/minio/README.md @@ -92,3 +92,41 @@ du fondateur (707 vidéos, ~2 ans), **la durée moyenne est de 53 secondes** et passe très largement. Si la limite se confirme, le plafond de 200 Mio annoncé côté API mérite d'être ramené sous celle du tunnel — mieux vaut refuser tôt, avec une phrase claire, qu'échouer au milieu d'un téléversement. + + +## Donner à une app l'accès au stockage + +Le motif tient en **trois pièces**, et il est générique — ajouter une app ne +demande aucune modification des modules Vault centraux. + +1. **Le bucket** — dans `values.yaml` de ce chart (bloc `buckets`), privé. +2. **Le compte de service** — une entrée dans `var.consumers` de `iac/consumers.tf` : + ```hcl + { app = "mon-app", bucket = "mon-app-fichiers" } + ``` + Le plan crée une politique MinIO **bornée à ce bucket**, un compte de service, + et écrit ses clés dans `kvv2/minio/mon-app`. +3. **La lecture** — dans `hashicorp-vault/iac/terraform.tfvars`, l'app déclare : + ```hcl + { name = "mon-app", kv_read_paths = ["kvv2/data/minio/mon-app"] } + ``` + `kv_read_paths` **existait déjà** (l'ERP s'en sert pour les creds GCS de + Longhorn) : rien à changer dans `app_policy` ni `app_roles`. + +Puis, côté app : une `VaultStaticSecret` sur `kvv2/minio/` et l'injection +des variables dans le Deployment. + +### 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 son bucket** : ni les autres buckets, ni +l'administration. Un compte de service qui fuite ne donne accès qu'aux objets +qu'il gérait déjà. + +### Rotation + +Détruire `random_password.app[""]` et relancer le plan suffit : la clé +change, `force_destroy = false` garde le compte et les objets déjà déposés +conservent leur propriétaire. diff --git a/minio/iac/consumers.tf b/minio/iac/consumers.tf new file mode 100644 index 0000000..7d3546a --- /dev/null +++ b/minio/iac/consumers.tf @@ -0,0 +1,81 @@ +# ── Les apps qui STOCKENT des objets dans MinIO ────────────────────────────── +# +# Le motif, générique, en trois pièces : +# 1. ICI : un compte de service MinIO par app, borné à SON bucket, dont les +# clés sont écrites dans `kvv2/minio/` ; +# 2. côté Vault central : l'app déclare `kv_read_paths = ["kvv2/data/minio/"]` +# dans `hashicorp-vault/iac/terraform.tfvars` — le mécanisme existait DÉJÀ +# (même recette que `kvv2/data/longhorn/gcs-backup` pour l'ERP), donc aucun +# module central n'a eu besoin d'être modifié ; +# 3. côté app : une VaultStaticSecret qui matérialise ce chemin en Secret k8s. +# +# POURQUOI les clés vivent chez MINIO et pas chez l'app : seul ce pipeline-ci +# possède les identifiants ROOT. Si chaque app créait son propre compte de +# service, il faudrait donner le root de MinIO à chaque rôle CI — c'est-à-dire +# à tout le monde. Ici il ne sort jamais d'ici, et l'app ne reçoit qu'une clé +# qui ne peut rien lire d'autre que son bucket. +# +# AJOUTER UNE APP = deux lignes : une entrée dans `var.consumers` ci-dessous +# (avec son bucket, qui doit exister dans `values.yaml` du chart), et un +# `kv_read_paths` dans le tfvars central. + +# La politique d'accès : LE bucket de l'app, rien d'autre. Ni listing des autres +# buckets, ni administration — un compte de service qui fuite ne donne accès +# qu'aux objets qu'il gérait déjà. +resource "minio_iam_policy" "app" { + for_each = { for c in var.consumers : c.app => c } + name = "${each.key}-app" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] + Resource = ["arn:aws:s3:::${each.value.bucket}/*"] + }, + { + # Nécessaire pour qu'un client S3 puisse vérifier l'existence du bucket + # et lister SES objets — jamais ceux d'un autre. + Effect = "Allow" + Action = ["s3:ListBucket", "s3:GetBucketLocation"] + Resource = ["arn:aws:s3:::${each.value.bucket}"] + }, + ] + }) +} + +resource "minio_iam_user" "app" { + for_each = { for c in var.consumers : c.app => c } + 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` = on ne recrée pas l'utilisateur à chaque rotation du mot de passe : + # les objets déjà déposés gardent leur propriétaire. + force_destroy = false +} + +resource "random_password" "app" { + for_each = { for c in var.consumers : c.app => c } + length = 40 + special = false # les outils S3 transportent mal certains caractères en URL +} + +resource "minio_iam_user_policy_attachment" "app" { + for_each = { for c in var.consumers : c.app => c } + user_name = minio_iam_user.app[each.key].id + policy_name = minio_iam_policy.app[each.key].id +} + +# Les clés, déposées dans l'espace Vault de MINIO — l'app y accède par le +# `kv_read_paths` déclaré au tfvars central (pièce 2 du motif, ci-dessus). +resource "vault_kv_secret_v2" "app" { + for_each = { for c in var.consumers : c.app => c } + mount = "kvv2" + name = "minio/${each.key}" + data_json = jsonencode({ + MINIO_ENDPOINT = var.minio_endpoint + MINIO_BUCKET = each.value.bucket + MINIO_ACCESS_KEY = minio_iam_user.app[each.key].id + MINIO_SECRET_KEY = random_password.app[each.key].result + }) +} diff --git a/minio/iac/providers.tf b/minio/iac/providers.tf index 501193f..40d183c 100644 --- a/minio/iac/providers.tf +++ b/minio/iac/providers.tf @@ -4,6 +4,14 @@ terraform { source = "vault" version = "4.4.0" } + minio = { + source = "aminueza/minio" + version = "3.3.0" + } + random = { + source = "hashicorp/random" + version = "3.6.3" + } } } @@ -14,3 +22,13 @@ provider "vault" { role = "gitea_cicd_minio" } } + +# Provider MinIO — pour créer les COMPTES DE SERVICE des apps consommatrices. +# Il parle à l'API S3 publique (s3.arcodange.fr, exposée par le chart) : le +# runner CI n'est pas dans le LAN, et `.lab` ne s'y résout pas. +provider "minio" { + minio_server = var.minio_endpoint + minio_user = local.config.rootUser + minio_password = local.config.rootPassword + minio_ssl = true +} diff --git a/minio/iac/variables.tf b/minio/iac/variables.tf new file mode 100644 index 0000000..4759165 --- /dev/null +++ b/minio/iac/variables.tf @@ -0,0 +1,16 @@ +variable "minio_endpoint" { + type = string + default = "s3.arcodange.fr" + description = "Hôte de l'API S3, SANS schéma (le provider ajoute https via minio_ssl). Public : le runner CI n'est pas dans le LAN et `.lab` ne s'y résout pas." +} + +variable "consumers" { + type = list(object({ + app = string # nom de l'app — doit correspondre à son nom Vault (kvv2/) et à son rôle k8s + bucket = string # son bucket, qui doit exister dans `values.yaml` du chart (bloc `buckets`) + })) + default = [ + { app = "kadans", bucket = "kadans-videos" }, + ] + description = "Apps autorisées à stocker des objets. Chacune reçoit un compte de service borné à SON bucket, dont les clés atterrissent dans kvv2/minio/. Ajouter une app ici ne suffit pas : elle doit aussi déclarer kv_read_paths = [\"kvv2/data/minio/\"] dans hashicorp-vault/iac/terraform.tfvars." +}