refactor(minio) — le BUCKET est la seule déclaration : plus de liste à tenir
Helm Charts / Detect changed charts (push) Successful in 14s
Helm Charts / Detect changed charts (pull_request) Successful in 14s
Helm Charts / Application charts pgcat (push) Has been cancelled
Helm Charts / Library charts tool (push) Has been cancelled
MinIO / Tofu - minio IAC (push) Has been cancelled
MinIO / Auth with gitea for vault (push) Has been cancelled
MinIO / Auth with gitea for vault (pull_request) Failing after 8m31s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
Hashicorp Vault / Auth with gitea for vault (pull_request) Failing after 8m32s
Hashicorp Vault / Tofu - Vault IAC (pull_request) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped

« Je ne vois pas le mal à donner la permission de lire sur un chemin qui n'existe
pas. Je préfère ne pas m'embêter avec consumers ou autre. » (fondateur, 26/07)

`var.consumers` disparaît. Le plan LIT `values.yaml` du chart — le même fichier
qu'Helm consomme — et provisionne un compte de service par bucket. Créer un
bucket EST la déclaration : il devient impossible d'avoir un bucket sans son
compte, ou un compte sans son bucket. Une liste de plus aurait été une liste à
tenir synchronisée, donc une liste à oublier.

La convention qui rend ça possible : UN BUCKET PAR APP, NOMMÉ COMME ELLE. Le
bucket passe donc de `kadans-videos` à `kadans`. Il est VIDE aujourd'hui — le
renommer maintenant ne coûte rien ; dans un mois ce serait une migration.

Tout en découle sans être écrit ailleurs : le compte `<app>-app` borné à ce seul
bucket, le secret `kvv2/minio/<app>`, et la lecture que `app_policy` accorde
déjà à toute app sur `kvv2/data/minio/<son nom>`.

Vérifié plutôt que supposé : `yamldecode` lit bien ce values.yaml, ancres YAML
comprises (testé en isolation avant d'écrire le plan). tofu fmt propre, tofu
validate réussi.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
This commit is contained in:
2026-07-26 09:38:24 +02:00
co-authored by Claude Opus 5
parent 91a0f09b49
commit 287e3dcf1e
4 changed files with 81 additions and 79 deletions
+47 -41
View File
@@ -1,29 +1,35 @@
# ── Les apps qui STOCKENT des objets dans MinIO ──────────────────────────────
# ── Un compte de service par BUCKET, sans rien déclarer de plus ──────────────
#
# 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/<app>` ;
# 2. côté Vault central : l'app déclare `kv_read_paths = ["kvv2/data/minio/<app>"]`
# 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.
# 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.
#
# 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.
# 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>-app`, borné à ce seul bucket ;
# · le secret `kvv2/minio/<app>` ;
# · la lecture, que le module central `app_policy` accorde déjà à TOUTE app
# sur `kvv2/data/minio/<son nom>`, inconditionnellement — une app qui ne
# stocke rien y lit un chemin qui n'existe pas, ce qui n'est pas un droit.
#
# 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.
# 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à.
# 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à.
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]
}
# La politique d'accès : SON bucket, rien d'autre. Ni listing des autres
# buckets, ni administration.
resource "minio_iam_policy" "app" {
for_each = { for c in var.consumers : c.app => c }
for_each = toset(local.buckets)
name = "${each.key}-app"
policy = jsonencode({
Version = "2012-10-17"
@@ -31,50 +37,50 @@ resource "minio_iam_policy" "app" {
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
Resource = ["arn:aws:s3:::${each.value.bucket}/*"]
Resource = ["arn:aws:s3:::${each.key}/*"]
},
{
# Nécessaire pour qu'un client S3 puisse vérifier l'existence du bucket
# et lister SES objets — jamais ceux d'un autre.
# Nécessaire pour qu'un client S3 vérifie l'existence du bucket et liste
# SES objets — jamais ceux d'un autre.
Effect = "Allow"
Action = ["s3:ListBucket", "s3:GetBucketLocation"]
Resource = ["arn:aws:s3:::${each.value.bucket}"]
Resource = ["arn:aws:s3:::${each.key}"]
},
]
})
}
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 }
for_each = toset(local.buckets)
length = 40
special = false # les outils S3 transportent mal certains caractères en URL
}
resource "minio_iam_user" "app" {
for_each = toset(local.buckets)
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 = { for c in var.consumers : c.app => c }
for_each = toset(local.buckets)
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).
# 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/<son nom>`.
resource "vault_kv_secret_v2" "app" {
for_each = { for c in var.consumers : c.app => c }
for_each = toset(local.buckets)
mount = "kvv2"
name = "minio/${each.key}"
data_json = jsonencode({
MINIO_ENDPOINT = var.minio_endpoint
MINIO_BUCKET = each.value.bucket
MINIO_BUCKET = each.key
MINIO_ACCESS_KEY = minio_iam_user.app[each.key].id
MINIO_SECRET_KEY = random_password.app[each.key].result
})
+3 -10
View File
@@ -4,13 +4,6 @@ variable "minio_endpoint" {
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/<app>) 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/<app>. Ajouter une app ici ne suffit pas : elle doit aussi déclarer kv_read_paths = [\"kvv2/data/minio/<app>\"] dans hashicorp-vault/iac/terraform.tfvars."
}
# `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.