refactor(minio) — chacun son périmètre : les buckets se déclarent depuis le dépôt de l'app
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Detect changed charts (pull_request) Successful in 16s
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
MinIO / Auth with gitea for vault (push) Failing after 8m31s
MinIO / Tofu - minio IAC (push) Has been skipped
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
Hashicorp Vault / Auth with gitea for vault (push) Has been cancelled
Hashicorp Vault / Tofu - Vault IAC (push) Has been cancelled
Helm Charts / Library charts tool (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 / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Detect changed charts (pull_request) Successful in 16s
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
MinIO / Auth with gitea for vault (push) Failing after 8m31s
MinIO / Tofu - minio IAC (push) Has been skipped
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
Hashicorp Vault / Auth with gitea for vault (push) Has been cancelled
Hashicorp Vault / Tofu - Vault IAC (push) Has been cancelled
Helm Charts / Library charts tool (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 / Application charts pgcat (pull_request) Has been skipped
« Les buckets sont à déclarer dans le repo de kadans-api. On ne va pas modifier le repo tools à chaque changement d'application. Chacun son périmètre. Tools peut proposer un module pour standardiser la déclaration de buckets à la limite, mais c'est tout. » (fondateur, 26/07) C'est une erreur de fond de ma part : j'avais fait de `tools` le PROPRIÉTAIRE de déclarations qui appartiennent aux applications. À ce rythme, chaque nouveau bucket de n'importe quelle app devenait une PR sur l'infra partagée. CE QUI CHANGE. `consumers.tf` disparaît, et la liste de buckets du chart se vide. À la place, un module réutilisable `iac/modules/minio_app` : une app lui donne son nom et ses buckets, et reçoit des buckets privés, un compte de service qui ne peut rien toucher d'autre, et ses clés dans `kvv2/minio/<app>`. L'OBSTACLE, ET SA RÉPONSE. Déclarer ses buckets depuis son propre dépôt suppose des droits d'ADMINISTRATION sur MinIO. Confier le root serait absurde : il lit et écrit tous les objets de toutes les apps. `tools` fournit donc un compte PROVISIONNEUR aux droits minimaux — créer un bucket, une politique, un compte de service — et AUCUN droit sur les objets. Une app compromise pourrait créer des buckets (une nuisance), pas lire les vidéos d'une autre. Le root, lui, ne sort toujours pas de ce pipeline. Le rôle CI de chaque app gagne la lecture de `kvv2/data/minio/provisioner` dans `app_policy` — générique, et c'est exactement le genre de standardisation qui appartient au dépôt commun. ⚠ CE QUE JE N'AI PAS PU PROUVER : les noms d'actions d'administration MinIO de la politique du provisionneur 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 non vérifié de cette PR, et il est signalé dans le code à l'endroit exact. tofu fmt propre · tofu validate réussi sur minio/iac ET hashicorp-vault/iac. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
This commit is contained in:
@@ -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