feat(minio) — un compte de service par app consommatrice, borné à son bucket
Helm Charts / Detect changed charts (push) Successful in 57s
MinIO / Auth with gitea for vault (push) Failing after 8m34s
MinIO / Tofu - minio IAC (push) Has been skipped
Hashicorp Vault / Auth with gitea for vault (push) Failing after 8m33s
Hashicorp Vault / Tofu - Vault IAC (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 1m1s
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
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

« On peut adopter ce pattern à toutes les apps qui pourraient avoir besoin de
MinIO, et ainsi modifier les app roles / app policy générique. » (fondateur, 26/07)

Bonne nouvelle : les modules centraux n'ont PAS eu à changer. `kv_read_paths`
existe déjà dans `app_policy` et fait exactement ça — l'ERP s'en sert pour lire
les creds GCS de Longhorn. Le motif générique était donc à moitié construit ;
il manquait la moitié MinIO.

CE QUI EST AJOUTÉ : `minio/iac/consumers.tf` provisionne, pour chaque app
déclarée dans `var.consumers`, une politique MinIO bornée à SON bucket
(GetObject/PutObject/DeleteObject sur les objets, ListBucket sur le bucket seul
— ni les autres buckets, ni l'administration), un compte de service, et écrit
ses clés dans `kvv2/minio/<app>`.

POURQUOI LES CLÉS VIVENT CHEZ MINIO ET PAS CHEZ L'APP : seul ce pipeline possède
les identifiants ROOT. 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 d'ici, et l'app ne reçoit qu'une clé qui ne peut rien lire
d'autre que son bucket. Un compte de service qui fuite ne donne accès qu'aux
objets qu'il gérait déjà.

Ajouter une app = trois lignes déclaratives, documentées au README : son bucket
dans values.yaml, son entrée dans `var.consumers`, son `kv_read_paths` au tfvars
central. Aucun module à toucher.

Le provider parle à `s3.arcodange.fr` — l'endpoint PUBLIC, parce que le runner
CI n'est pas dans le LAN et que `.lab` ne s'y résout pas. C'est l'exposition
HTTPS mergée ce matin qui rend ce plan applicable.

⚠ Erreur attrapée par le validateur, et corrigée : je relisais `kvv2/minio/config`
alors que `local.config` de main.tf porte déjà les identifiants root — une
lecture qui aurait créé une dépendance circulaire avec l'écriture faite par ce
même plan.

Vérifié : `tofu fmt` propre, `tofu validate` réussi (avec TERRAFORM_VAULT_AUTH_JWT
posé — sans lui, le provider Vault échoue déjà sur main, ce n'est pas mon fait).
Je n'ai lancé AUCUN apply : c'est le workflow qui le fera, et la revue t'appartient.

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:12:09 +02:00
co-authored by Claude Opus 5
parent f49a79e393
commit 4ca4a05370
5 changed files with 162 additions and 1 deletions
+38
View File
@@ -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/<app>` 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["<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.
+81
View File
@@ -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/<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.
#
# 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
})
}
+18
View File
@@ -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
}
+16
View File
@@ -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/<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."
}