diff --git a/hashicorp-vault/iac/modules/app_policy/main.tf b/hashicorp-vault/iac/modules/app_policy/main.tf index 402f5e1..5cc39c1 100644 --- a/hashicorp-vault/iac/modules/app_policy/main.tf +++ b/hashicorp-vault/iac/modules/app_policy/main.tf @@ -36,10 +36,8 @@ data "vault_policy_document" "ops" { path = "kvv1/google/credentials" capabilities = ["read"] } - # Provisionneur MinIO : permet à l'app de créer SES buckets et SON compte de - # service depuis son propre dépôt (module tools//minio/iac/modules/minio_app), - # sans qu'on lui confie le root de MinIO. Ce compte ne peut QUE provisionner — - # aucun droit de lecture ni d'écriture sur les objets. + # Provisionneur MinIO — l'app crée ses buckets depuis son dépôt sans le root. + # ADR : factory/doc/adr/20260726-stockage-objet-minio.md rule { path = "kvv2/data/minio/provisioner" capabilities = ["read"] @@ -187,27 +185,16 @@ data "vault_policy_document" "app" { path = "postgres/creds/${local.name}*" capabilities = ["read"] } - # Identifiants de SON compte de service MinIO, provisionné par le pipeline - # `minio` (tools/minio/iac/consumers.tf) dans l'espace Vault de MinIO. - # - # INCONDITIONNEL, et c'est voulu : le chemin porte le nom de l'app, donc cette - # règle ne peut jamais exposer que ses PROPRES clés. Une app qui ne stocke rien - # lit un chemin qui n'existe pas — une règle inerte, pas un privilège. - # - # Pourquoi ici plutôt que dans `kv_read_paths` de chaque app : savoir lire ses - # propres identifiants de stockage est une propriété de la PLATEFORME, pas une - # exception par application. `kv_read_paths` est la trappe pour un secret - # appartenant à une AUTRE app (les creds GCS de Longhorn pour l'ERP) ; y ranger - # un motif standard le rendrait invisible et obligerait à le redéclarer partout. - # Conséquence pratique : déclarer un consommateur MinIO se fait à UN seul - # endroit — `var.consumers` du pipeline minio. Aucune synchronisation à tenir. + # Ses identifiants MinIO. INCONDITIONNEL : le chemin porte le nom de l'app, + # donc la règle ne peut exposer que ses propres clés ; une app sans stockage + # lit un chemin qui n'existe pas. + # ADR : factory/doc/adr/20260726-stockage-objet-minio.md rule { path = "kvv2/data/minio/${local.name}/*" capabilities = ["read", "list"] } rule { - # Le secret est écrit à `kvv2/minio/` (sans sous-chemin) : la règle - # ci-dessus couvre les descendants, celle-ci le document lui-même. + # Le document lui-même (la règle ci-dessus ne couvre que ses descendants). path = "kvv2/data/minio/${local.name}" capabilities = ["read", "list"] } @@ -237,8 +224,7 @@ data "vault_policy_document" "app_non_prod" { path = "postgres/creds/${each.key}*" capabilities = ["read"] } - # Même règle qu'en prod (voir le commentaire de vault_policy_document.app) : - # chaque instance lit les identifiants MinIO portant SON nom. + # Idem prod : chaque instance lit les identifiants MinIO portant SON nom. rule { path = "kvv2/data/minio/${each.key}/*" capabilities = ["read", "list"] diff --git a/minio/iac/modules/minio_app/main.tf b/minio/iac/modules/minio_app/main.tf index 6d69f22..55a6a03 100644 --- a/minio/iac/modules/minio_app/main.tf +++ b/minio/iac/modules/minio_app/main.tf @@ -1,38 +1,13 @@ -# ── Module `minio_app` — une app déclare SES buckets, depuis SON dépôt ─────── -# -# Chacun son périmètre (fondateur 2026-07-26) : les buckets d'une application -# appartiennent au dépôt de cette application, pas à l'infra partagée. `tools` -# ne fait que STANDARDISER la déclaration — il ne la détient pas. Sans ça, -# chaque bucket de chaque app deviendrait une PR sur le dépôt commun. -# -# Usage, depuis l'`iac/` de l'app : -# -# module "stockage" { -# source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main" -# app = "kadans" -# buckets = ["kadans-videos"] -# providers = { minio = minio } -# } -# -# Ce que le module garantit : -# · les buckets sont PRIVÉS — l'accès passe par des URL présignées ; -# · le compte de service ne peut RIEN toucher d'autre que ces buckets-là ; -# · ses clés atterrissent dans `kvv2/minio/`, que le module Vault central -# autorise déjà l'app à lire (règle inconditionnelle : le chemin porte le nom -# de l'app, donc il ne peut exposer que ses propres clés). +# Module `minio_app` — une app déclare SES buckets depuis SON dépôt. +# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md resource "minio_s3_bucket" "app" { - for_each = toset(var.buckets) - bucket = each.key - acl = "private" - # Les objets sont DÉRIVÉS (rendus de transfert) et régénérables depuis les - # originaux restés sur l'appareil : un `force_destroy` accidentel ne détruit - # donc rien d'irremplaçable, mais on l'interdit quand même — la destruction - # d'un bucket doit être un geste explicite, pas un effet de bord d'un plan. - force_destroy = false + for_each = toset(var.buckets) + bucket = each.key + acl = "private" # l'accès passe par des URL présignées + force_destroy = false # détruire un bucket doit être un geste explicite } -# La politique : SES buckets, rien d'autre. Ni les autres, ni l'administration. resource "minio_iam_policy" "app" { name = "${var.app}-app" policy = jsonencode({ @@ -44,8 +19,6 @@ resource "minio_iam_policy" "app" { Resource = [for b in var.buckets : "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 var.buckets : "arn:aws:s3:::${b}"] @@ -60,12 +33,9 @@ resource "random_password" "app" { } resource "minio_iam_user" "app" { - name = "${var.app}-app" - # Le mot de passe EST la clé secrète S3 : généré ici, jamais choisi. - secret = random_password.app.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 + name = "${var.app}-app" + secret = random_password.app.result + force_destroy = false # une rotation ne recrée pas l'utilisateur : les objets gardent leur propriétaire } resource "minio_iam_user_policy_attachment" "app" { @@ -73,9 +43,7 @@ resource "minio_iam_user_policy_attachment" "app" { policy_name = minio_iam_policy.app.id } -# Les clés, dans l'espace Vault de MinIO — lu par le pod de l'app. -# `MINIO_BUCKETS` les liste toutes pour que l'app n'ait pas à les redéclarer ; -# `MINIO_BUCKET` reste le premier, pour celles qui n'en ont qu'un. +# Lu par le pod de l'app — `app_policy` lui accorde déjà ce chemin. resource "vault_kv_secret_v2" "app" { mount = "kvv2" name = "minio/${var.app}" diff --git a/minio/iac/modules/minio_app/outputs.tf b/minio/iac/modules/minio_app/outputs.tf index d4f2ba0..d2313cb 100644 --- a/minio/iac/modules/minio_app/outputs.tf +++ b/minio/iac/modules/minio_app/outputs.tf @@ -1,9 +1,9 @@ output "vault_path" { value = "kvv2/minio/${var.app}" - description = "Où le pod lira ses identifiants (VaultStaticSecret). Le module Vault central autorise déjà l'app à lire ce chemin." + description = "Où le pod lira ses identifiants (VaultStaticSecret)." } output "buckets" { value = var.buckets - description = "Écho des buckets créés — utile pour poser la valeur `bucket` du chart sans la réécrire." + description = "Écho des buckets créés." } diff --git a/minio/iac/modules/minio_app/variables.tf b/minio/iac/modules/minio_app/variables.tf index f205996..306ca73 100644 --- a/minio/iac/modules/minio_app/variables.tf +++ b/minio/iac/modules/minio_app/variables.tf @@ -1,15 +1,15 @@ variable "app" { type = string - description = "Nom de l'app — celui de son rôle Vault. Décide du chemin du secret (kvv2/minio/) et du nom du compte de service." + description = "Nom de l'app (= son rôle Vault). Décide du chemin du secret et du nom du compte de service." } variable "buckets" { type = list(string) - description = "Les buckets de cette app. Ils sont CRÉÉS ici (privés) et le compte de service n'a de droits que sur eux. Plusieurs sont possibles — deux contenus aux cycles de vie différents méritent deux buckets." + description = "Ses buckets, créés ici (privés). Le compte de service n'a de droits que sur eux." } variable "endpoint" { type = string default = "s3.arcodange.fr" - description = "Hôte de l'API S3, SANS schéma. Public : le runner CI n'est pas dans le LAN et `.lab` ne s'y résout pas." + description = "Hôte de l'API S3, sans schéma. Public : le runner CI n'est pas dans le LAN." } diff --git a/minio/iac/providers.tf b/minio/iac/providers.tf index 40d183c..9911aaa 100644 --- a/minio/iac/providers.tf +++ b/minio/iac/providers.tf @@ -23,9 +23,7 @@ provider "vault" { } } -# 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 — crée le compte de provisionnement (provisioner.tf). provider "minio" { minio_server = var.minio_endpoint minio_user = local.config.rootUser diff --git a/minio/iac/provisioner.tf b/minio/iac/provisioner.tf index 0aa3724..43be8c1 100644 --- a/minio/iac/provisioner.tf +++ b/minio/iac/provisioner.tf @@ -1,21 +1,10 @@ -# ── Le compte PROVISIONNEUR : ce que `tools` doit fournir en plus du module ─── +# Compte de PROVISIONNEMENT : crée buckets, politiques et comptes de service — +# aucun droit sur les objets. C'est lui que lisent les rôles CI des apps, pour +# qu'elles déclarent leurs buckets sans qu'on leur confie le root. +# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md # -# Une app qui déclare ses buckets depuis son propre dépôt doit pouvoir les -# créer — donc disposer de droits d'ADMINISTRATION sur MinIO. Lui donner le -# ROOT serait absurde : le root lit et écrit TOUS les objets de TOUTES les apps. -# -# Ce compte-ci ne peut que PROVISIONNER : créer un bucket, une politique, un -# compte de service, et les attacher. Il n'a AUCUN droit de lecture ou -# d'écriture sur les objets. Une app compromise pourrait créer des buckets — -# une nuisance —, pas lire les vidéos d'une autre. -# -# Le root, lui, reste dans `kvv2/minio/config`, que seul le rôle CI `minio` -# peut lire. -# -# ⚠ NON VÉRIFIÉ CONTRE LE SERVEUR : les noms d'actions d'administration MinIO -# ci-dessous viennent de la documentation, pas d'un essai — je n'ai pas -# d'identifiants admin en main. Le PREMIER `apply` les confirmera ou les -# corrigera ; c'est le seul point de cette PR que je ne peux pas prouver ici. +# ⚠ Noms d'actions issus de la documentation MinIO, NON éprouvés contre le +# serveur : le premier apply les confirmera ou les corrigera. resource "minio_iam_policy" "provisioner" { name = "provisioner" policy = jsonencode({ @@ -37,8 +26,7 @@ resource "minio_iam_policy" "provisioner" { Resource = ["arn:aws:s3:::*"] }, { - # Créer et inspecter un bucket — mais PAS lire ni écrire ses objets : - # `s3:GetObject` et `s3:PutObject` sont volontairement absents. + # s3:GetObject / s3:PutObject volontairement ABSENTS. Effect = "Allow" Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"] Resource = ["arn:aws:s3:::*"] @@ -63,9 +51,6 @@ resource "minio_iam_user_policy_attachment" "provisioner" { policy_name = minio_iam_policy.provisioner.id } -# Lisible par le rôle CI de CHAQUE app (règle générique ajoutée à `app_policy`) : -# c'est ce qui permet à une app de provisionner ses propres buckets sans qu'on -# lui confie le root. resource "vault_kv_secret_v2" "provisioner" { mount = "kvv2" name = "minio/provisioner" diff --git a/minio/iac/variables.tf b/minio/iac/variables.tf index adfdbcf..5b91383 100644 --- a/minio/iac/variables.tf +++ b/minio/iac/variables.tf @@ -1,9 +1,5 @@ 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." + description = "Hôte de l'API S3, sans schéma. Public : le runner CI n'est pas dans le LAN." } - -# `consumers` a été SUPPRIMÉE (fondateur 2026-07-26) : la liste se lit dans -# `values.yaml` du chart — créer un bucket EST la déclaration. Une liste de plus -# aurait été une liste à tenir synchronisée, donc une liste à oublier. diff --git a/minio/templates/ingress-public.yaml b/minio/templates/ingress-public.yaml index 4d94fa4..0ab4d0d 100644 --- a/minio/templates/ingress-public.yaml +++ b/minio/templates/ingress-public.yaml @@ -1,19 +1,11 @@ -# Exposition PUBLIQUE s3.arcodange.fr, EN PLUS du .lab interne (qui reste -# inchangé). Même motif que `grafana/templates/ingress-public.yaml` : entrypoint -# `web` — le TLS est terminé en amont par le tunnel Cloudflare (wildcard -# *.arcodange.fr → traefik.kube-system.svc:80) — et middleware crowdsec. -# -# POURQUOI c'est nécessaire (et pas un confort) : la PWA est servie en -# https://kadans.arcodange.fr. Un navigateur REFUSE une requête vers http:// -# depuis une page https (contenu mixte), et `.lab` n'est de toute façon pas -# résolvable hors du LAN. Sans cet ingress, le téléversement d'une vidéo ne peut -# marcher NI en déplacement, NI même à la maison — c'est le maillon qui manque -# à kadans-api#23 (URL présignées). +# Exposition PUBLIQUE s3.arcodange.fr — TLS terminé par le tunnel Cloudflare, +# middleware crowdsec. Le `.lab` interne reste inchangé. # # ⚠ PAS de basic-auth ici, contrairement à kadans-public : une requête S3 porte -# sa propre signature (SigV4) dans ses en-têtes ou son URL. Un défi HTTP Basic -# casserait le PUT présigné du navigateur, qui n'a aucun moyen d'y répondre. -# L'autorisation vient de l'URL signée, avec sa durée de vie courte. +# sa propre signature (SigV4), et un défi HTTP Basic casserait le PUT présigné +# auquel le navigateur ne peut pas répondre. +# +# ADR : factory/doc/adr/20260726-stockage-objet-minio.md apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/minio/values.yaml b/minio/values.yaml index 26eccad..9c0d545 100644 --- a/minio/values.yaml +++ b/minio/values.yaml @@ -61,11 +61,9 @@ 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. - # AUCUN bucket ici : chaque application déclare LES SIENS depuis son propre - # dépôt, via le module `iac/modules/minio_app` (fondateur 2026-07-26 : « on ne - # va pas modifier le repo tools à chaque changement d'application, chacun son - # périmètre »). `tools` fournit le serveur, le provisionneur et le module — - # pas la liste. + # AUCUN bucket ici : chaque app déclare les siens depuis son dépôt, via le + # module `iac/modules/minio_app`. + # ADR : factory/doc/adr/20260726-stockage-objet-minio.md buckets: [] # Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le @@ -75,13 +73,8 @@ minio: &minio_config enabled: false # pas d'opérateur Prometheus ici : scrape par annotation environment: MINIO_PROMETHEUS_AUTH_TYPE: "public" - # CORS : le navigateur téléverse DIRECTEMENT vers MinIO depuis la PWA (URL - # présignées, kadans-api#23) — donc requête inter-origines, donc préflight - # OPTIONS. Sans cette liste, le PUT est refusé par le navigateur avant même - # de partir. Origines EXACTES, jamais « * » : une URL présignée qui fuite - # serait rejouable depuis n'importe quel site. - # .fr = la PWA publique (le cas réel, en déplacement comme à la maison) - # .lab = la même app servie en interne, pour la mise au point + # CORS : le navigateur téléverse directement (URL présignées) — origines + # EXACTES, jamais « * » : une URL qui fuite serait sinon rejouable partout. MINIO_API_CORS_ALLOW_ORIGIN: "https://kadans.arcodange.fr,https://kadans.arcodange.lab" tool: