feat(minio) — un compte de service par app consommatrice, borné à son bucket #21

Merged
arcodange merged 6 commits from arcodange/minio-comptes-de-service into main 2026-07-26 10:46:45 +02:00
5 changed files with 162 additions and 1 deletions
Showing only changes of commit 4ca4a05370 - Show all commits
+9 -1
View File
@@ -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"]
},
]
+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."
}