feat(minio) — un compte de service par app consommatrice, borné à son bucket #21
@@ -36,6 +36,15 @@ data "vault_policy_document" "ops" {
|
|||||||
path = "kvv1/google/credentials"
|
path = "kvv1/google/credentials"
|
||||||
capabilities = ["read"]
|
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.
|
||||||
|
rule {
|
||||||
|
path = "kvv2/data/minio/provisioner"
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
# read cloudflare related secrets
|
# read cloudflare related secrets
|
||||||
rule {
|
rule {
|
||||||
path = "kvv1/cloudflare/${local.name}*"
|
path = "kvv1/cloudflare/${local.name}*"
|
||||||
|
|||||||
+39
-42
@@ -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) |
|
||||||
| Buckets | **privés**, chacun déclarant son app (`app:`) — 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. Une app peut en avoir plusieurs |
|
| Buckets | **aucun ici** — chaque app déclare les siens depuis son dépôt (module `minio_app`). Tous privés : l'accès passe par des URL signées (ADR-0002 du dossier produit) |
|
||||||
| 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,53 +96,50 @@ 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
|
||||||
|
|
||||||
**Une seule chose à faire** : déclarer son bucket dans `values.yaml`, en disant
|
**Rien à faire ici.** Chaque application déclare **ses** buckets **depuis son
|
||||||
à quelle app il appartient.
|
propre dépôt**, avec le module que ce dépôt-ci fournit :
|
||||||
|
|
||||||
```yaml
|
```hcl
|
||||||
buckets:
|
# iac/main.tf de l'application
|
||||||
- name: mon-app-fichiers
|
data "vault_kv_secret_v2" "minio_provisioner" {
|
||||||
app: mon-app # ← la seule déclaration
|
mount = "kvv2"
|
||||||
policy: none # privé : l'accès passe par des URL signées
|
name = "minio/provisioner"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "minio" {
|
||||||
|
minio_server = "s3.arcodange.fr"
|
||||||
|
minio_user = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_ACCESS_KEY"]
|
||||||
|
minio_password = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_SECRET_KEY"]
|
||||||
|
minio_ssl = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "stockage" {
|
||||||
|
source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
|
||||||
|
app = "mon-app"
|
||||||
|
buckets = ["mon-app-fichiers"]
|
||||||
|
providers = { minio = minio }
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Tout le reste en découle, sans rien écrire ailleurs :
|
Voir `iac/modules/minio_app/README.md`. **Chacun son périmètre** : `tools`
|
||||||
|
fournit le serveur, le provisionneur et le module — pas la liste des buckets.
|
||||||
|
Sans ça, chaque bucket de chaque app deviendrait une PR sur l'infra partagée.
|
||||||
|
|
||||||
- `iac/consumers.tf` **lit ce même fichier**, groupe les buckets par app, et crée
|
### Ce que `tools` fournit, et pourquoi
|
||||||
**un** compte de service `mon-app-app` autorisé sur **tous ses buckets et eux
|
|
||||||
seuls**. Ses clés atterrissent dans `kvv2/minio/mon-app` ;
|
|
||||||
- le module Vault central `app_policy` accorde **déjà** à toute app la lecture de
|
|
||||||
`kvv2/data/minio/<son nom>` — inconditionnellement, parce que le chemin porte
|
|
||||||
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 à
|
| Pièce | Rôle |
|
||||||
injecter les variables dans son Deployment (voir `kadans-api` pour l'exemple).
|
|---|---|
|
||||||
Le secret porte `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`,
|
| Le serveur | le chart, son volume, ses ingress (interne + public) |
|
||||||
`MINIO_BUCKET` (le premier) et `MINIO_BUCKETS` (tous, séparés par des virgules).
|
| Le **root** | généré ici, écrit dans `kvv2/minio/config`, **ne sort jamais** de ce pipeline |
|
||||||
|
| Le **provisionneur** | un compte aux droits d'administration MINIMAUX (créer bucket, politique, compte de service) et **aucun droit sur les objets** — lisible par le rôle CI de chaque app |
|
||||||
|
| Le **module** | `minio_app` : standardise la déclaration, sans la détenir |
|
||||||
|
|
||||||
### Plusieurs buckets pour une même app
|
Donner le root aux apps aurait été absurde : il lit et écrit **tous** les objets
|
||||||
|
de **toutes** les apps. Le provisionneur, lui, peut créer des buckets — une
|
||||||
C'est prévu, et c'est le cas courant : deux contenus aux **cycles de vie
|
nuisance si une app est compromise — mais **pas lire les vidéos d'une autre**.
|
||||||
différents** méritent deux buckets aux politiques de purge différentes. Kadans y
|
|
||||||
viendra avec les deux paliers de l'ADR-018 — l'aperçu 240p, régénérable, et le
|
|
||||||
rendu de travail 360p, à garder. Il suffit d'une seconde entrée avec le même
|
|
||||||
`app:` ; le compte de service existant gagne l'accès, sans nouvelle clé.
|
|
||||||
|
|
||||||
> **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
|
|
||||||
|
|
||||||
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 de service qui
|
|
||||||
fuite ne donne accès qu'aux objets qu'il gérait déjà.
|
|
||||||
|
|
||||||
### Rotation
|
### Rotation
|
||||||
|
|
||||||
Détruire `random_password.app["<app>"]` et relancer le plan suffit : la clé
|
Depuis l'`iac/` de l'app : détruire `module.stockage.random_password.app` et
|
||||||
change, `force_destroy = false` garde le compte, et les objets déjà déposés
|
relancer son plan. La clé change, `force_destroy = false` garde le compte, et
|
||||||
conservent leur propriétaire.
|
les objets déjà déposés conservent leur propriétaire.
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# ── Un compte de service par APP, sans liste à tenir ─────────────────────────
|
|
||||||
#
|
|
||||||
# Il n'y a PAS de liste de consommateurs : elle se lit dans `values.yaml` du
|
|
||||||
# chart. Chaque bucket y dit à quelle app il appartient (`app:`) — et c'est la
|
|
||||||
# SEULE 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 : c'est le fichier qu'Helm consomme.
|
|
||||||
#
|
|
||||||
# UNE APP PEUT AVOIR PLUSIEURS BUCKETS, et c'est important : rien ne justifiait
|
|
||||||
# de l'en empêcher. L'ADR-018 de Kadans prévoit déjà deux paliers de transfert
|
|
||||||
# (aperçu 240p régénérable, travail 360p à garder) — deux cycles de vie, donc
|
|
||||||
# potentiellement deux buckets aux politiques de purge différentes. Le compte de
|
|
||||||
# service est donc par APP, autorisé sur TOUS ses buckets et sur eux seuls.
|
|
||||||
#
|
|
||||||
# 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 ses propres buckets. Un compte 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 ses buckets.
|
|
||||||
buckets_declares = yamldecode(file("${path.module}/../values.yaml")).minio.buckets
|
|
||||||
|
|
||||||
# app → ses buckets. `try` : un bucket sans `app:` est ignoré plutôt que de
|
|
||||||
# faire échouer tout le plan — il n'aura simplement aucun compte de service,
|
|
||||||
# ce qui se voit tout de suite et ne casse rien d'existant.
|
|
||||||
buckets_par_app = {
|
|
||||||
for app in distinct([for b in local.buckets_declares : try(b.app, null) if try(b.app, null) != null]) :
|
|
||||||
app => [for b in local.buckets_declares : b.name if try(b.app, null) == app]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# La politique d'accès : SES buckets, rien d'autre. Ni les autres buckets, ni
|
|
||||||
# l'administration.
|
|
||||||
resource "minio_iam_policy" "app" {
|
|
||||||
for_each = local.buckets_par_app
|
|
||||||
name = "${each.key}-app"
|
|
||||||
policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Effect = "Allow"
|
|
||||||
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
|
|
||||||
Resource = [for b in each.value : "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 each.value : "arn:aws:s3:::${b}"]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "random_password" "app" {
|
|
||||||
for_each = local.buckets_par_app
|
|
||||||
length = 40
|
|
||||||
special = false # les outils S3 transportent mal certains caractères en URL
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_iam_user" "app" {
|
|
||||||
for_each = local.buckets_par_app
|
|
||||||
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 = local.buckets_par_app
|
|
||||||
user_name = minio_iam_user.app[each.key].id
|
|
||||||
policy_name = minio_iam_policy.app[each.key].id
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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>`.
|
|
||||||
#
|
|
||||||
# `MINIO_BUCKETS` liste ses buckets pour que l'app n'ait pas à les redéclarer de
|
|
||||||
# son côté ; `MINIO_BUCKET` reste le premier, pour les apps qui n'en ont qu'un.
|
|
||||||
resource "vault_kv_secret_v2" "app" {
|
|
||||||
for_each = local.buckets_par_app
|
|
||||||
mount = "kvv2"
|
|
||||||
name = "minio/${each.key}"
|
|
||||||
data_json = jsonencode({
|
|
||||||
MINIO_ENDPOINT = var.minio_endpoint
|
|
||||||
MINIO_BUCKET = each.value[0]
|
|
||||||
MINIO_BUCKETS = join(",", each.value)
|
|
||||||
MINIO_ACCESS_KEY = minio_iam_user.app[each.key].id
|
|
||||||
MINIO_SECRET_KEY = random_password.app[each.key].result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# `minio_app` — déclarer ses buckets depuis SON dépôt
|
||||||
|
|
||||||
|
Chacun son périmètre : les buckets d'une application appartiennent au dépôt de
|
||||||
|
cette application. Ce module **standardise** la déclaration, il ne la détient
|
||||||
|
pas — sans lui, chaque bucket de chaque app deviendrait une PR sur `tools`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Dans l'`iac/` de l'app :
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Le provisionneur MinIO : des droits d'administration MINIMAUX (créer un
|
||||||
|
# bucket, un compte de service, une politique), jamais le root — qui, lui, ne
|
||||||
|
# sort pas du pipeline `minio`.
|
||||||
|
data "vault_kv_secret_v2" "minio_provisioner" {
|
||||||
|
mount = "kvv2"
|
||||||
|
name = "minio/provisioner"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "minio" {
|
||||||
|
minio_server = "s3.arcodange.fr"
|
||||||
|
minio_user = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_ACCESS_KEY"]
|
||||||
|
minio_password = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_SECRET_KEY"]
|
||||||
|
minio_ssl = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "stockage" {
|
||||||
|
source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
|
||||||
|
app = "mon-app" # = le nom de son rôle Vault
|
||||||
|
buckets = ["mon-app-fichiers"]
|
||||||
|
providers = { minio = minio }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis, côté chart : une `VaultStaticSecret` sur `kvv2/minio/<app>` et l'injection
|
||||||
|
des variables dans le Deployment.
|
||||||
|
|
||||||
|
## 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 vont dans `kvv2/minio/<app>`, 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).
|
||||||
|
|
||||||
|
## Plusieurs buckets
|
||||||
|
|
||||||
|
C'est le cas courant : deux contenus aux **cycles de vie différents** méritent
|
||||||
|
deux buckets. Il suffit de les lister — le compte de service existant gagne
|
||||||
|
l'accès, **sans nouvelle clé**.
|
||||||
|
|
||||||
|
## Ce que le module ne fait PAS
|
||||||
|
|
||||||
|
Il ne pose ni quota, ni règle de cycle de vie, ni versioning : ces choix
|
||||||
|
appartiennent à l'app et varient d'un bucket à l'autre. À ajouter le jour où
|
||||||
|
un besoin réel apparaît, pas avant.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# ── 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/<app>`, 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).
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# La politique : SES buckets, rien d'autre. Ni les autres, ni l'administration.
|
||||||
|
resource "minio_iam_policy" "app" {
|
||||||
|
name = "${var.app}-app"
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
|
||||||
|
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}"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "app" {
|
||||||
|
length = 40
|
||||||
|
special = false # les outils S3 transportent mal certains caractères en URL
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_user_policy_attachment" "app" {
|
||||||
|
user_name = minio_iam_user.app.id
|
||||||
|
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.
|
||||||
|
resource "vault_kv_secret_v2" "app" {
|
||||||
|
mount = "kvv2"
|
||||||
|
name = "minio/${var.app}"
|
||||||
|
data_json = jsonencode({
|
||||||
|
MINIO_ENDPOINT = var.endpoint
|
||||||
|
MINIO_BUCKET = var.buckets[0]
|
||||||
|
MINIO_BUCKETS = join(",", var.buckets)
|
||||||
|
MINIO_ACCESS_KEY = minio_iam_user.app.id
|
||||||
|
MINIO_SECRET_KEY = random_password.app.result
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +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."
|
||||||
|
}
|
||||||
|
|
||||||
|
output "buckets" {
|
||||||
|
value = var.buckets
|
||||||
|
description = "Écho des buckets créés — utile pour poser la valeur `bucket` du chart sans la réécrire."
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
minio = {
|
||||||
|
source = "aminueza/minio"
|
||||||
|
configuration_aliases = [minio]
|
||||||
|
}
|
||||||
|
vault = {
|
||||||
|
source = "hashicorp/vault"
|
||||||
|
}
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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/<app>) 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."
|
||||||
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# ── Le compte PROVISIONNEUR : ce que `tools` doit fournir en plus du module ───
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
resource "minio_iam_policy" "provisioner" {
|
||||||
|
name = "provisioner"
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"admin:CreateUser",
|
||||||
|
"admin:DeleteUser",
|
||||||
|
"admin:ListUsers",
|
||||||
|
"admin:GetUser",
|
||||||
|
"admin:CreatePolicy",
|
||||||
|
"admin:DeletePolicy",
|
||||||
|
"admin:GetPolicy",
|
||||||
|
"admin:ListUserPolicies",
|
||||||
|
"admin:AttachUserOrGroupPolicy",
|
||||||
|
]
|
||||||
|
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.
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"]
|
||||||
|
Resource = ["arn:aws:s3:::*"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "provisioner" {
|
||||||
|
length = 40
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_user" "provisioner" {
|
||||||
|
name = "provisioner"
|
||||||
|
secret = random_password.provisioner.result
|
||||||
|
force_destroy = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_user_policy_attachment" "provisioner" {
|
||||||
|
user_name = minio_iam_user.provisioner.id
|
||||||
|
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"
|
||||||
|
data_json = jsonencode({
|
||||||
|
MINIO_ENDPOINT = var.minio_endpoint
|
||||||
|
MINIO_ACCESS_KEY = minio_iam_user.provisioner.id
|
||||||
|
MINIO_SECRET_KEY = random_password.provisioner.result
|
||||||
|
})
|
||||||
|
}
|
||||||
+6
-15
@@ -61,21 +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.
|
||||||
# Chaque bucket dit À QUELLE APP il appartient (`app:`) — et c'est la SEULE
|
# AUCUN bucket ici : chaque application déclare LES SIENS depuis son propre
|
||||||
# déclaration à faire : `iac/consumers.tf` lit ce fichier, groupe les buckets
|
# dépôt, via le module `iac/modules/minio_app` (fondateur 2026-07-26 : « on ne
|
||||||
# par app, et provisionne UN compte de service par app, autorisé sur tous SES
|
# va pas modifier le repo tools à chaque changement d'application, chacun son
|
||||||
# buckets. Ses clés atterrissent dans `kvv2/minio/<app>`, que le module Vault
|
# périmètre »). `tools` fournit le serveur, le provisionneur et le module —
|
||||||
# central autorise déjà l'app à lire.
|
# pas la liste.
|
||||||
#
|
buckets: []
|
||||||
# Une app peut donc avoir PLUSIEURS buckets, aux cycles de vie différents —
|
|
||||||
# l'ADR-018 de Kadans prévoit déjà deux paliers (aperçu 240p régénérable,
|
|
||||||
# travail 360p à garder), qui n'ont pas la même politique de purge.
|
|
||||||
buckets:
|
|
||||||
- name: kadans-videos
|
|
||||||
app: kadans
|
|
||||||
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
|
|
||||||
purge: false
|
|
||||||
versioning: false
|
|
||||||
|
|
||||||
# Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le
|
# Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le
|
||||||
# laptop (ADR-0014 du dossier) — MinIO rejoint la même vue.
|
# laptop (ADR-0014 du dossier) — MinIO rejoint la même vue.
|
||||||
|
|||||||
Reference in New Issue
Block a user