diff --git a/minio/README.md b/minio/README.md index 56f7aac..77cdb4f 100644 --- a/minio/README.md +++ b/minio/README.md @@ -25,7 +25,7 @@ application vivent avec cette application. | Ressources | requests 512 Mi / 100 m · limit 2 Gi — la limite protège les voisins de `tools`, pas MinIO | | API S3 | `s3.arcodange.lab` (interne) **et `s3.arcodange.fr`** (public, tunnel Cloudflare → entrypoint `web` + crowdsec) — voir « Pourquoi une exposition publique » | | Console | `minio.arcodange.lab` (Traefik) | -| Buckets | **un par app, nommé comme elle** (`kadans`…), tous **privés** — l'accès passe par des URL signées (ADR-0002 du dossier produit). Le bucket est la SEULE déclaration : compte de service et droits Vault en découlent | +| Buckets | **privés**, chacun déclarant son app (`app:`) — l'accès passe par des URL signées (ADR-0002 du dossier produit). Le bucket est la SEULE déclaration : compte de service et droits Vault en découlent. Une app peut en avoir plusieurs | | Identifiants | **jamais dans le dépôt** : `iac/` les génère dans Vault (`kvv2/minio/config`), le Vault Secrets Operator les matérialise en secret `minio-config`, le chart les lit via `existingSecret` | Le ServiceAccount du pod est nommé `minio` (et non le `minio-sa` par défaut du @@ -96,20 +96,21 @@ une phrase claire, qu'échouer au milieu d'un téléversement. ## Donner à une app l'accès au stockage -**Une seule chose à faire** : ajouter son bucket dans `values.yaml`, **nommé -comme l'app**. +**Une seule chose à faire** : déclarer son bucket dans `values.yaml`, en disant +à quelle app il appartient. ```yaml buckets: - - name: mon-app - policy: none # privé : l'accès passe par des URL signées + - name: mon-app-fichiers + app: mon-app # ← la seule déclaration + policy: none # privé : l'accès passe par des URL signées ``` -Tout le reste en découle, sans autre déclaration nulle part : +Tout le reste en découle, sans rien écrire ailleurs : -- `iac/consumers.tf` **lit ce même fichier** et crée un compte de service - `mon-app-app`, borné à ce seul bucket, dont les clés atterrissent dans - `kvv2/minio/mon-app` ; +- `iac/consumers.tf` **lit ce même fichier**, groupe les buckets par app, et crée + **un** compte de service `mon-app-app` autorisé sur **tous ses buckets et eux + seuls**. Ses clés atterrissent dans `kvv2/minio/mon-app` ; - le module Vault central `app_policy` accorde **déjà** à toute app la lecture de `kvv2/data/minio/` — inconditionnellement, parce que le chemin porte le nom de l'app et ne peut donc jamais exposer que ses propres clés. Une app @@ -117,6 +118,16 @@ Tout le reste en découle, sans autre déclaration nulle part : Côté app, il reste à écrire une `VaultStaticSecret` sur `kvv2/minio/` et à injecter les variables dans son Deployment (voir `kadans-api` pour l'exemple). +Le secret porte `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, +`MINIO_BUCKET` (le premier) et `MINIO_BUCKETS` (tous, séparés par des virgules). + +### Plusieurs buckets pour une même app + +C'est prévu, et c'est le cas courant : deux contenus aux **cycles de vie +différents** méritent deux buckets aux politiques de purge différentes. Kadans y +viendra avec les deux paliers de l'ADR-018 — l'aperçu 240p, régénérable, et le +rendu de travail 360p, à garder. Il suffit d'une seconde entrée avec le même +`app:` ; le compte de service existant gagne l'accès, sans nouvelle clé. > **Il n'y a volontairement AUCUNE liste de consommateurs.** Une liste de plus > serait une liste à tenir synchronisée avec les buckets — donc une liste à diff --git a/minio/iac/consumers.tf b/minio/iac/consumers.tf index bd4e395..7db0766 100644 --- a/minio/iac/consumers.tf +++ b/minio/iac/consumers.tf @@ -1,35 +1,42 @@ -# ── Un compte de service par BUCKET, sans rien déclarer de plus ────────────── +# ── 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. Créer un bucket EST la 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. +# 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. # -# La convention qui rend ça possible : UN BUCKET PAR APP, NOMMÉ COMME ELLE. -# Tout en découle sans être écrit nulle part ailleurs : -# · le compte de service `-app`, borné à ce seul bucket ; -# · le secret `kvv2/minio/` ; -# · la lecture, que le module central `app_policy` accorde déjà à TOUTE app -# sur `kvv2/data/minio/`, inconditionnellement — une app qui ne -# stocke rien y lit un chemin qui n'existe pas, ce qui n'est pas un droit. +# 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 son bucket. Un compte qui fuite ne donne accès qu'aux objets qu'il -# gérait déjà. +# 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 son bucket. - buckets = [for b in yamldecode(file("${path.module}/../values.yaml")).minio.buckets : b.name] + # 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 : SON bucket, rien d'autre. Ni listing des autres -# buckets, ni administration. +# La politique d'accès : SES buckets, rien d'autre. Ni les autres buckets, ni +# l'administration. resource "minio_iam_policy" "app" { - for_each = toset(local.buckets) + for_each = local.buckets_par_app name = "${each.key}-app" policy = jsonencode({ Version = "2012-10-17" @@ -37,27 +44,27 @@ resource "minio_iam_policy" "app" { { Effect = "Allow" Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] - Resource = ["arn:aws:s3:::${each.key}/*"] + Resource = [for b in each.value : "arn:aws:s3:::${b}/*"] }, { - # Nécessaire pour qu'un client S3 vérifie l'existence du bucket et liste - # SES objets — jamais ceux d'un autre. + # 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 = ["arn:aws:s3:::${each.key}"] + Resource = [for b in each.value : "arn:aws:s3:::${b}"] }, ] }) } resource "random_password" "app" { - for_each = toset(local.buckets) + 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 = toset(local.buckets) + 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 @@ -67,20 +74,24 @@ resource "minio_iam_user" "app" { } resource "minio_iam_user_policy_attachment" "app" { - for_each = toset(local.buckets) + 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 = toset(local.buckets) + for_each = local.buckets_par_app mount = "kvv2" name = "minio/${each.key}" data_json = jsonencode({ MINIO_ENDPOINT = var.minio_endpoint - MINIO_BUCKET = each.key + 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 }) diff --git a/minio/values.yaml b/minio/values.yaml index 6682be7..c14e0cc 100644 --- a/minio/values.yaml +++ b/minio/values.yaml @@ -61,12 +61,18 @@ minio: &minio_config # Buckets créés au déploiement. `versioning: false` assumé : ces objets sont # DÉRIVÉS et re-générables depuis le master local — versionner doublerait le # stockage pour un filet dont on n'a pas besoin. - # ⚠ UN BUCKET PAR APP, NOMMÉ COMME ELLE. Tout en découle sans rien redéclarer : - # le compte de service (iac/consumers.tf), le chemin Vault `kvv2/minio/`, - # et la policy que le module central accorde déjà à `kvv2/data/minio/`. - # Créer un bucket ici EST la déclaration — il n'y a pas d'autre liste à tenir. + # Chaque bucket dit À QUELLE APP il appartient (`app:`) — et c'est la SEULE + # déclaration à faire : `iac/consumers.tf` lit ce fichier, groupe les buckets + # par app, et provisionne UN compte de service par app, autorisé sur tous SES + # buckets. Ses clés atterrissent dans `kvv2/minio/`, que le module Vault + # central autorise déjà l'app à lire. + # + # Une app peut donc avoir PLUSIEURS buckets, aux cycles de vie différents — + # l'ADR-018 de Kadans prévoit déjà deux paliers (aperçu 240p régénérable, + # travail 360p à garder), qui n'ont pas la même politique de purge. buckets: - - name: kadans + - name: kadans-videos + app: kadans policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier) purge: false versioning: false