Author SHA1 Message Date
arcodangeandClaude Opus 5 7074a94d7e docs(minio) — le raisonnement part dans l'ADR, le code garde ce qui protège
Helm Charts / Detect changed charts (push) Successful in 22s
Helm Charts / Detect changed charts (pull_request) Successful in 16s
MinIO / Auth with gitea for vault (push) Failing after 8m33s
MinIO / Tofu - minio IAC (push) Has been skipped
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 (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Hashicorp Vault / Auth with gitea for vault (push) Failing after 8m31s
Hashicorp Vault / Tofu - Vault IAC (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Les décisions de conception (qui déclare, qui détient, pourquoi les octets ne
passent pas par l'API) vivent désormais dans
`factory/doc/adr/20260726-stockage-objet-minio.md`.

Le code ne garde que ce qu'un relecteur ne peut pas deviner et qui l'empêcherait
de casser quelque chose : l'absence VOLONTAIRE de s3:GetObject/PutObject dans la
politique du provisionneur, l'absence VOLONTAIRE de basic-auth sur l'ingress
S3, le caractère inconditionnel de la règle Vault, et l'avertissement sur les
noms d'actions MinIO non éprouvés. Chaque fichier pointe l'ADR.

283 → 230 lignes sur minio/iac. tofu fmt propre, tofu validate réussi sur les
deux racines.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 10:19:28 +02:00
arcodangeandClaude Opus 5 ec71571b98 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
« 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
2026-07-26 10:07:05 +02:00
arcodangeandClaude Opus 5 0e1b6e1062 refactor(minio) — lever la restriction « un seul bucket par app »
Helm Charts / Detect changed charts (pull_request) Successful in 17s
Helm Charts / Detect changed charts (push) Successful in 16s
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 / 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 started running
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
« Pourquoi sommes-nous restreints sur les buckets ? » (fondateur, 26/07) — parce
que JE l'avais décidé, pour rendre tout dérivable d'un seul nom. Rien ne
l'imposait, et la restriction aurait mordu au pas suivant : l'ADR-018 de Kadans
prévoit DEUX paliers de transfert (aperçu 240p régénérable, travail 360p à
garder), donc deux cycles de vie, donc potentiellement deux buckets aux
politiques de purge différentes.

Chaque bucket déclare désormais son app (`app: kadans`). C'est toujours la SEULE
déclaration, au même endroit — mais le nom du bucket redevient libre, et
`kadans-videos` retrouve un nom qui dit ce qu'il contient.

Le compte de service devient par APP et non par bucket : une seule clé, autorisée
sur tous ses buckets et eux seuls. Ajouter un bucket à une app existante ne crée
donc aucune nouvelle clé — le compte existant gagne l'accès. Le secret porte
`MINIO_BUCKETS` (tous) en plus de `MINIO_BUCKET` (le premier), pour que l'app
n'ait pas à les redéclarer de son côté.

Un bucket sans `app:` est ignoré plutôt que de faire échouer le plan : il n'aura
simplement pas de compte de service, ce qui se voit immédiatement et ne casse
rien d'existant.

Vérifié sur le VRAI fichier, pas en théorie : le groupement rend bien
{"kadans" = ["kadans-videos"]}, et avec un second bucket de la même app,
{"kadans" = ["kadans-videos", "kadans-apercus"]} — le cas ADR-018 exact.
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
2026-07-26 09:46:32 +02:00
arcodangeandClaude Opus 5 287e3dcf1e 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
« 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
2026-07-26 09:38:24 +02:00
arcodangeandClaude Opus 5 91a0f09b49 refactor(vault) — lire ses identifiants MinIO devient une propriété de la plateforme
Helm Charts / Detect changed charts (pull_request) Successful in 15s
Helm Charts / Detect changed charts (push) Successful in 15s
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) Failing after 8m35s
Hashicorp Vault / Tofu - Vault IAC (push) Has been skipped
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Hashicorp Vault / Auth with gitea for vault (pull_request) Failing after 8m36s
Hashicorp Vault / Tofu - Vault IAC (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Retour fondateur : « je pensais que tools#21 contribuerait à app_policy pour une
policy kvv2/minio/<app name> ». Il a raison, et mon choix initial était le plus
faible des deux.

Mon objection — ne donner le droit qu'aux apps qui en ont besoin — ne tient pas
à l'examen : la règle porte le NOM de l'app, donc elle ne peut jamais exposer
que ses propres clés. Il n'y a aucun privilège à préserver. Une app qui ne
stocke rien lit un chemin qui n'existe pas : une règle inerte, pas un droit.

Son argument, lui, porte : savoir lire ses propres identifiants de stockage est
une propriété de la PLATEFORME, pas une exception par application. Et
`kv_read_paths` est documenté comme la trappe pour un secret appartenant à une
AUTRE app (les creds GCS de Longhorn pour l'ERP) — y ranger un motif standard
l'aurait rendu invisible et aurait obligé à le redéclarer à chaque app.

La règle passe donc dans `app_policy`, en prod ET pour chaque instance non-prod
(symétrie stricte), sur deux chemins : le document `kvv2/data/minio/<app>` et
ses descendants.

Un cran plus loin que la demande : la règle est INCONDITIONNELLE, sans drapeau.
Conséquence — déclarer un consommateur MinIO se fait désormais à UN SEUL
endroit, `var.consumers` du pipeline minio. Aucune synchronisation à tenir entre
deux fichiers, donc rien à oublier. Le `kv_read_paths` que j'avais ajouté à
kadans est retiré : il faisait double emploi.

tofu fmt propre · tofu validate réussi sur hashicorp-vault/iac.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 09:26:05 +02:00
arcodangeandClaude Opus 5 4ca4a05370 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
2026-07-26 09:12:09 +02:00
arcodange f49a79e393 Merge pull request 'feat(minio) — exposer l'API S3 en HTTPS public (s3.arcodange.fr) + CORS de la PWA' (#20) from arcodange/minio-public-fr into main
Helm Charts / Detect changed charts (push) Successful in 42s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Reviewed-on: #20
2026-07-26 07:34:48 +02:00
arcodangeandClaude Opus 5 1490f4c514 feat(minio) — exposer l'API S3 en HTTPS public (s3.arcodange.fr) + CORS de la PWA
Helm Charts / Detect changed charts (push) Successful in 1m6s
Helm Charts / Detect changed charts (pull_request) Successful in 1m5s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Sans ça, la synchronisation des vidéos ne peut pas marcher, et ce sont des faits
du navigateur : une page servie en https ne peut pas émettre de requête vers
http:// (contenu mixte), et `.lab` n'est pas résolvable hors du LAN. L'ingress
existant est en entrypoint `web` sans TLS, sur un domaine interne — donc
inutilisable depuis kadans.arcodange.fr, en déplacement COMME à la maison.

Même motif que grafana/templates/ingress-public.yaml : entrypoint `web`, TLS
terminé par le tunnel Cloudflare (wildcard *.arcodange.fr), middleware crowdsec.
Le `.lab` interne reste inchangé.

PAS de basic-auth, contrairement à kadans-public : une requête S3 porte sa propre
signature (SigV4), et un défi HTTP Basic casserait le PUT présigné auquel le
navigateur ne peut pas répondre. L'autorisation vient de l'URL signée et de sa
durée de vie courte.

CORS déclaré sur les origines EXACTES de la PWA (.fr et .lab), jamais « * » :
une URL présignée qui fuiterait serait sinon rejouable depuis n'importe quel
site. Sans cette liste, le préflight OPTIONS échoue et le PUT ne part même pas.

⚠ Signalé, NON mesuré : le tunnel Cloudflare plafonne probablement le corps
d'une requête (~100 Mo sur les offres gratuites). À éprouver avec un vrai
téléversement. Contexte utile : sur le corpus réel du fondateur (707 vidéos,
~2 ans), la durée MOYENNE est de 53 s et deux vidéos seulement dépassent 5 min —
au palier « travail » de l'ADR-018, 100 Mo valent ~29 min de cours.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 01:56:35 +02:00
arcodange dce19141a8 Merge pull request 'fix(minio) — déclarer minio dans la liste centrale des applications Vault' (#19) from claude/minio-role into main
Helm Charts / Detect changed charts (push) Successful in 29s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Hashicorp Vault / Auth with gitea for vault (push) Successful in 1m28s
Hashicorp Vault / Tofu - Vault IAC (push) Successful in 1m19s
Reviewed-on: #19
2026-07-25 18:37:45 +02:00
13 changed files with 393 additions and 8 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
locals {
factory_crowdsec_conf_sa_name = "factory-ansible-tool-crowdsec-traefik-plugin"
factory_crowdsec_conf_sa_name = "factory-ansible-tool-crowdsec-traefik-plugin"
}
@@ -36,6 +36,13 @@ data "vault_policy_document" "ops" {
path = "kvv1/google/credentials"
capabilities = ["read"]
}
# Provisionneur MinIO — l'app crée ses buckets depuis son dépôt sans le root.
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
rule {
path = "kvv2/data/minio/provisioner"
capabilities = ["read"]
}
# read cloudflare related secrets
rule {
path = "kvv1/cloudflare/${local.name}*"
@@ -178,6 +185,19 @@ data "vault_policy_document" "app" {
path = "postgres/creds/${local.name}*"
capabilities = ["read"]
}
# Ses identifiants MinIO. INCONDITIONNEL : le chemin porte le nom de l'app,
# donc la règle ne peut exposer que ses propres clés ; une app sans stockage
# lit un chemin qui n'existe pas.
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
rule {
path = "kvv2/data/minio/${local.name}/*"
capabilities = ["read", "list"]
}
rule {
# Le document lui-même (la règle ci-dessus ne couvre que ses descendants).
path = "kvv2/data/minio/${local.name}"
capabilities = ["read", "list"]
}
# Extra shared paths this app's prod runtime may read (e.g. backup creds).
dynamic "rule" {
for_each = var.kv_read_paths
@@ -204,6 +224,15 @@ data "vault_policy_document" "app_non_prod" {
path = "postgres/creds/${each.key}*"
capabilities = ["read"]
}
# Idem prod : chaque instance lit les identifiants MinIO portant SON nom.
rule {
path = "kvv2/data/minio/${each.key}/*"
capabilities = ["read", "list"]
}
rule {
path = "kvv2/data/minio/${each.key}"
capabilities = ["read", "list"]
}
}
resource "vault_policy" "app_non_prod" {
for_each = toset(local.non_prod_instances)
+93 -2
View File
@@ -23,9 +23,9 @@ application vivent avec cette application.
| Mode | **standalone** (1 réplique) — la donnée est dérivée et Longhorn réplique déjà le volume ; l'erasure coding distribué coûterait de la RAM que des Pi 5 n'ont pas à dépenser pour ça |
| Volume | **50 Gi** sur `longhorn`**250 h de cours** au palier « travail ». ⚠ Longhorn réplique : compter **×3** sur la capacité du cluster avant d'augmenter |
| Ressources | requests 512 Mi / 100 m · limit 2 Gi — la limite protège les voisins de `tools`, pas MinIO |
| API S3 | `s3.arcodange.lab` (Traefik) |
| 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) |
| Bucket | `kadans-videos`, **privé** l'accès passe par des URL signées (ADR-0002 du dossier produit) |
| 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` |
Le ServiceAccount du pod est nommé `minio` (et non le `minio-sa` par défaut du
@@ -52,3 +52,94 @@ L'ordre compte, et il compte **deux fois** :
> [!NOTE]
> Sans le secret `minio-config`, le pod ne démarre pas. C'est voulu — mieux
> vaut un pod en attente qu'un MinIO ouvert avec des identifiants par défaut.
## Pourquoi une exposition publique (`s3.arcodange.fr`)
La PWA Kadans est servie en `https://kadans.arcodange.fr` et téléverse ses vidéos
**directement** vers MinIO, avec des URL présignées émises par kadans-api
(kadans-api#23) : les octets ne passent jamais par l'API.
Deux raisons rendent le `.lab` inutilisable pour ça, et ce sont des faits du
navigateur, pas des préférences :
1. **Contenu mixte** — une page servie en `https` ne peut pas émettre une requête
vers `http://`. L'ingress `.lab` est en entrypoint `web` sans TLS.
2. **`.lab` n'est pas résolvable hors du LAN** — la synchronisation ne marcherait
qu'à la maison, ce qui vide de son sens « retrouver mes vidéos sur mon autre
appareil ».
**Pas de basic-auth** sur cet ingress, contrairement à `kadans-public` : une
requête S3 porte sa propre signature (SigV4). Un défi HTTP Basic casserait le PUT
présigné, auquel le navigateur ne peut pas répondre. L'autorisation vient de
l'URL signée et de sa durée de vie courte (15 min pour déposer, 1 h pour lire).
**CORS** (`MINIO_API_CORS_ALLOW_ORIGIN`) liste les origines EXACTES de la PWA —
jamais `*` : une URL présignée qui fuiterait serait sinon rejouable depuis
n'importe quel site.
### ⚠ À vérifier avant de s'y fier : la taille maximale d'une requête
Le trafic public passe par un **tunnel Cloudflare**. Les offres gratuites de
Cloudflare plafonnent la taille du corps d'une requête proxifiée (de l'ordre de
**100 Mo**) — ce plafond n'a **pas** été mesuré ici, il doit l'être avec un vrai
téléversement avant d'annoncer une limite aux utilisateurs.
Ce qu'on sait, en revanche, et qui rend le sujet peu urgent : sur le corpus réel
du fondateur (707 vidéos, ~2 ans), **la durée moyenne est de 53 secondes** et
**deux vidéos seulement dépassent 5 minutes**. Au palier « travail » de l'ADR-018
(360p ≈ 3,4 Mo/min), 100 Mo représentent ~29 minutes de cours : le corpus entier
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
**Rien à faire ici.** Chaque application déclare **ses** buckets **depuis son
propre dépôt**, avec le module que ce dépôt-ci fournit :
```hcl
# iac/main.tf de l'application
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::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
app = "mon-app"
buckets = ["mon-app-fichiers"]
providers = { minio = minio }
}
```
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.
### Ce que `tools` fournit, et pourquoi
| Pièce | Rôle |
|---|---|
| Le serveur | le chart, son volume, ses ingress (interne + public) |
| 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 |
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
nuisance si une app est compromise — mais **pas lire les vidéos d'une autre**.
### Rotation
Depuis l'`iac/` de l'app : détruire `module.stockage.random_password.app` et
relancer son plan. La clé change, `force_destroy = false` garde le compte, et
les objets déjà déposés conservent leur propriétaire.
+56
View File
@@ -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://[email protected]: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.
+57
View File
@@ -0,0 +1,57 @@
# Module `minio_app` — une app déclare SES buckets depuis SON dépôt.
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
resource "minio_s3_bucket" "app" {
for_each = toset(var.buckets)
bucket = each.key
acl = "private" # l'accès passe par des URL présignées
force_destroy = false # détruire un bucket doit être un geste explicite
}
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}/*"]
},
{
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"
secret = random_password.app.result
force_destroy = false # une rotation ne recrée pas l'utilisateur : les objets gardent leur propriétaire
}
resource "minio_iam_user_policy_attachment" "app" {
user_name = minio_iam_user.app.id
policy_name = minio_iam_policy.app.id
}
# Lu par le pod de l'app — `app_policy` lui accorde déjà ce chemin.
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
})
}
+9
View File
@@ -0,0 +1,9 @@
output "vault_path" {
value = "kvv2/minio/${var.app}"
description = "Où le pod lira ses identifiants (VaultStaticSecret)."
}
output "buckets" {
value = var.buckets
description = "Écho des buckets créés."
}
+14
View File
@@ -0,0 +1,14 @@
terraform {
required_providers {
minio = {
source = "aminueza/minio"
configuration_aliases = [minio]
}
vault = {
source = "hashicorp/vault"
}
random = {
source = "hashicorp/random"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
variable "app" {
type = string
description = "Nom de l'app (= son rôle Vault). Décide du chemin du secret et du nom du compte de service."
}
variable "buckets" {
type = list(string)
description = "Ses buckets, créés ici (privés). Le compte de service n'a de droits que sur eux."
}
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."
}
+16
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,11 @@ provider "vault" {
role = "gitea_cicd_minio"
}
}
# Provider MinIO crée le compte de provisionnement (provisioner.tf).
provider "minio" {
minio_server = var.minio_endpoint
minio_user = local.config.rootUser
minio_password = local.config.rootPassword
minio_ssl = true
}
+62
View File
@@ -0,0 +1,62 @@
# Compte de PROVISIONNEMENT : crée buckets, politiques et comptes de service
# aucun droit sur les objets. C'est lui que lisent les rôles CI des apps, pour
# qu'elles déclarent leurs buckets sans qu'on leur confie le root.
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
#
# Noms d'actions issus de la documentation MinIO, NON éprouvés contre le
# serveur : le premier apply les confirmera ou les corrigera.
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:::*"]
},
{
# s3:GetObject / s3:PutObject 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
}
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
})
}
+5
View File
@@ -0,0 +1,5 @@
variable "minio_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."
}
+29
View File
@@ -0,0 +1,29 @@
# Exposition PUBLIQUE s3.arcodange.fr — TLS terminé par le tunnel Cloudflare,
# middleware crowdsec. Le `.lab` interne reste inchangé.
#
# ⚠ PAS de basic-auth ici, contrairement à kadans-public : une requête S3 porte
# sa propre signature (SigV4), et un défi HTTP Basic casserait le PUT présigné
# auquel le navigateur ne peut pas répondre.
#
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-public
namespace: tools
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
traefik.ingress.kubernetes.io/router.middlewares: kube-system-crowdsec@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: s3.arcodange.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port:
number: 9000
+7 -5
View File
@@ -61,11 +61,10 @@ minio: &minio_config
# 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
# stockage pour un filet dont on n'a pas besoin.
buckets:
- name: kadans-videos
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
purge: false
versioning: false
# AUCUN bucket ici : chaque app déclare les siens depuis son dépôt, via le
# module `iac/modules/minio_app`.
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
buckets: []
# Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le
# laptop (ADR-0014 du dossier) — MinIO rejoint la même vue.
@@ -74,6 +73,9 @@ minio: &minio_config
enabled: false # pas d'opérateur Prometheus ici : scrape par annotation
environment:
MINIO_PROMETHEUS_AUTH_TYPE: "public"
# CORS : le navigateur téléverse directement (URL présignées) — origines
# EXACTES, jamais « * » : une URL qui fuite serait sinon rejouable partout.
MINIO_API_CORS_ALLOW_ORIGIN: "https://kadans.arcodange.fr,https://kadans.arcodange.lab"
tool:
# kind: 'SubChart' or 'HelmChart', if subchart then uncomment Chart.yaml dependency, else comment and use tool library with helm chart template