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
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:
+26
-27
@@ -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 |
|
| 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 » |
|
| 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) |
|
| Console | `minio.arcodange.lab` (Traefik) |
|
||||||
| Bucket | `kadans-videos`, **privé** — l'accès passe par des URL signées (ADR-0002 du dossier produit) |
|
| 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 |
|
||||||
| 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` |
|
| 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
|
Le ServiceAccount du pod est nommé `minio` (et non le `minio-sa` par défaut du
|
||||||
@@ -96,43 +96,42 @@ une phrase claire, qu'échouer au milieu d'un téléversement.
|
|||||||
|
|
||||||
## Donner à une app l'accès au stockage
|
## 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
|
**Une seule chose à faire** : ajouter son bucket dans `values.yaml`, **nommé
|
||||||
demande aucune modification des modules Vault centraux.
|
comme l'app**.
|
||||||
|
|
||||||
1. **Le bucket** — dans `values.yaml` de ce chart (bloc `buckets`), privé.
|
```yaml
|
||||||
2. **Le compte de service** — une entrée dans `var.consumers` de `iac/consumers.tf` :
|
buckets:
|
||||||
```hcl
|
- name: mon-app
|
||||||
{ app = "mon-app", bucket = "mon-app-fichiers" }
|
policy: none # privé : l'accès passe par des URL signées
|
||||||
```
|
```
|
||||||
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** — *rien à faire.* Le module central `app_policy` accorde à
|
|
||||||
**toute** app la lecture de `kvv2/data/minio/<son nom>`. La règle est
|
|
||||||
inconditionnelle et c'est voulu : le chemin porte le nom de l'app, donc elle
|
|
||||||
ne peut jamais exposer que ses propres clés ; une app qui ne stocke rien lit
|
|
||||||
un chemin qui n'existe pas.
|
|
||||||
|
|
||||||
Puis, côté app : une `VaultStaticSecret` sur `kvv2/minio/<app>` et l'injection
|
Tout le reste en découle, sans autre déclaration nulle part :
|
||||||
des variables dans le Deployment.
|
|
||||||
|
|
||||||
> **Un seul endroit déclare un consommateur** : `var.consumers`, ici. Rien à
|
- `iac/consumers.tf` **lit ce même fichier** et crée un compte de service
|
||||||
> synchroniser dans le tfvars central, donc rien à oublier. C'est la raison pour
|
`mon-app-app`, borné à ce seul bucket, dont les clés atterrissent dans
|
||||||
> laquelle la règle vit dans le module plutôt que dans `kv_read_paths` — cette
|
`kvv2/minio/mon-app` ;
|
||||||
> dernière est la trappe pour lire un secret appartenant à une AUTRE app (les
|
- le module Vault central `app_policy` accorde **déjà** à toute app la lecture de
|
||||||
> creds GCS de Longhorn pour l'ERP) ; y ranger un motif standard le rendrait
|
`kvv2/data/minio/<son nom>` — inconditionnellement, parce que le chemin porte
|
||||||
> invisible et obligerait à le redéclarer à chaque fois.
|
le nom de l'app et ne peut donc jamais exposer que ses propres clés. Une app
|
||||||
|
qui ne stocke rien y lit un chemin qui n'existe pas : une règle inerte.
|
||||||
|
|
||||||
|
Côté app, il reste à écrire une `VaultStaticSecret` sur `kvv2/minio/<app>` et à
|
||||||
|
injecter les variables dans son Deployment (voir `kadans-api` pour l'exemple).
|
||||||
|
|
||||||
|
> **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 à
|
||||||
|
> oublier. Le bucket fait foi.
|
||||||
|
|
||||||
### Pourquoi les clés vivent ICI et pas chez l'app
|
### 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
|
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 —
|
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
|
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
|
clé qui **ne peut rien lire d'autre que son bucket**. Un compte de service qui
|
||||||
l'administration. Un compte de service qui fuite ne donne accès qu'aux objets
|
fuite ne donne accès qu'aux objets qu'il gérait déjà.
|
||||||
qu'il gérait déjà.
|
|
||||||
|
|
||||||
### Rotation
|
### Rotation
|
||||||
|
|
||||||
Détruire `random_password.app["<app>"]` et relancer le plan suffit : la clé
|
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
|
change, `force_destroy = false` garde le compte, et les objets déjà déposés
|
||||||
conservent leur propriétaire.
|
conservent leur propriétaire.
|
||||||
|
|||||||
+47
-41
@@ -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 :
|
# Il n'y a PAS de liste de consommateurs : elle se lit dans `values.yaml` du
|
||||||
# 1. ICI : un compte de service MinIO par app, borné à SON bucket, dont les
|
# chart. Créer un bucket EST la déclaration (fondateur 2026-07-26 : « je préfère
|
||||||
# clés sont écrites dans `kvv2/minio/<app>` ;
|
# ne pas m'embêter avec consumers ou autre »). Rien à synchroniser entre deux
|
||||||
# 2. côté Vault central : l'app déclare `kv_read_paths = ["kvv2/data/minio/<app>"]`
|
# fichiers, donc rien à oublier — et aucune divergence possible.
|
||||||
# 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
|
# La convention qui rend ça possible : UN BUCKET PAR APP, NOMMÉ COMME ELLE.
|
||||||
# possède les identifiants ROOT. Si chaque app créait son propre compte de
|
# Tout en découle sans être écrit nulle part ailleurs :
|
||||||
# service, il faudrait donner le root de MinIO à chaque rôle CI — c'est-à-dire
|
# · le compte de service `<app>-app`, borné à ce seul bucket ;
|
||||||
# à tout le monde. Ici il ne sort jamais d'ici, et l'app ne reçoit qu'une clé
|
# · le secret `kvv2/minio/<app>` ;
|
||||||
# qui ne peut rien lire d'autre que son bucket.
|
# · 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
|
# POURQUOI LES CLÉS VIVENT ICI ET PAS CHEZ L'APP : seul ce pipeline possède les
|
||||||
# (avec son bucket, qui doit exister dans `values.yaml` du chart), et un
|
# identifiants ROOT de MinIO. Si chaque app créait son propre compte de service,
|
||||||
# `kv_read_paths` dans le tfvars central.
|
# 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
|
locals {
|
||||||
# buckets, ni administration — un compte de service qui fuite ne donne accès
|
# Le MÊME fichier que celui qu'Helm consomme : impossible de créer un bucket
|
||||||
# qu'aux objets qu'il gérait déjà.
|
# 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" {
|
resource "minio_iam_policy" "app" {
|
||||||
for_each = { for c in var.consumers : c.app => c }
|
for_each = toset(local.buckets)
|
||||||
name = "${each.key}-app"
|
name = "${each.key}-app"
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
Version = "2012-10-17"
|
Version = "2012-10-17"
|
||||||
@@ -31,50 +37,50 @@ resource "minio_iam_policy" "app" {
|
|||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
|
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
|
# Nécessaire pour qu'un client S3 vérifie l'existence du bucket et liste
|
||||||
# et lister SES objets — jamais ceux d'un autre.
|
# SES objets — jamais ceux d'un autre.
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["s3:ListBucket", "s3:GetBucketLocation"]
|
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" {
|
resource "random_password" "app" {
|
||||||
for_each = { for c in var.consumers : c.app => c }
|
for_each = toset(local.buckets)
|
||||||
length = 40
|
length = 40
|
||||||
special = false # les outils S3 transportent mal certains caractères en URL
|
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" {
|
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
|
user_name = minio_iam_user.app[each.key].id
|
||||||
policy_name = minio_iam_policy.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
|
# Les clés, dans l'espace Vault de MINIO. L'app y accède par la règle que
|
||||||
# `kv_read_paths` déclaré au tfvars central (pièce 2 du motif, ci-dessus).
|
# `app_policy` accorde à toutes : `kvv2/data/minio/<son nom>`.
|
||||||
resource "vault_kv_secret_v2" "app" {
|
resource "vault_kv_secret_v2" "app" {
|
||||||
for_each = { for c in var.consumers : c.app => c }
|
for_each = toset(local.buckets)
|
||||||
mount = "kvv2"
|
mount = "kvv2"
|
||||||
name = "minio/${each.key}"
|
name = "minio/${each.key}"
|
||||||
data_json = jsonencode({
|
data_json = jsonencode({
|
||||||
MINIO_ENDPOINT = var.minio_endpoint
|
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_ACCESS_KEY = minio_iam_user.app[each.key].id
|
||||||
MINIO_SECRET_KEY = random_password.app[each.key].result
|
MINIO_SECRET_KEY = random_password.app[each.key].result
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-10
@@ -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."
|
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" {
|
# `consumers` a été SUPPRIMÉE (fondateur 2026-07-26) : la liste se lit dans
|
||||||
type = list(object({
|
# `values.yaml` du chart — créer un bucket EST la déclaration. Une liste de plus
|
||||||
app = string # nom de l'app — doit correspondre à son nom Vault (kvv2/<app>) et à son rôle k8s
|
# aurait été une liste à tenir synchronisée, donc une liste à oublier.
|
||||||
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."
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-1
@@ -61,8 +61,12 @@ minio: &minio_config
|
|||||||
# Buckets créés au déploiement. `versioning: false` assumé : ces objets sont
|
# 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
|
# 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.
|
# 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/<nom>`,
|
||||||
|
# et la policy que le module central accorde déjà à `kvv2/data/minio/<app>`.
|
||||||
|
# Créer un bucket ici EST la déclaration — il n'y a pas d'autre liste à tenir.
|
||||||
buckets:
|
buckets:
|
||||||
- name: kadans-videos
|
- name: kadans
|
||||||
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
|
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
|
||||||
purge: false
|
purge: false
|
||||||
versioning: false
|
versioning: false
|
||||||
|
|||||||
Reference in New Issue
Block a user