Files
tools/minio/README.md
T
arcodangeandClaude Opus 5 0e1b6e1062
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
refactor(minio) — lever la restriction « un seul bucket par app »
« 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

8.0 KiB
Raw Blame History

MinIO — stockage objet S3 du homelab

Brique partagée du namespace tools, au même titre que pgbouncer ou clickhouse. Le serveur vit ici ; les buckets, quotas et identifiants d'une application vivent avec cette application.

Premier consommateur : Kadans

  • ADR-012 « MinIO local d'abord » — bascule vers Cloudflare R2 prévue aux seuils : 100+ utilisateurs actifs, > 10 To/mois, ou dispersion géographique.
  • ADR-013 le gratuit est local-first (la vidéo ne quitte pas l'appareil) ; MinIO sert les paliers payants.
  • ADR-018 ce qui transite est dérivé (aperçu 240p ~50 Ko, travail 360p ~3,4 Mo/min) — le master reste chez l'utilisateur. D'où le dimensionnement ci-dessous.

Ce que ce chart pose

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 longhorn250 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 (interne) et s3.arcodange.fr (public, tunnel Cloudflare → entrypoint web + crowdsec) — voir « Pourquoi une exposition publique »
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
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 chart amont) parce que le module Vault app_roles borne l'authentification au SA portant le nom de l'app : un seul SA, rien à réconcilier.

Première mise en service

L'ordre compte, et il compte deux fois :

  1. Workflow Hashicorp Vault — MinIO doit d'abord figurer dans hashicorp-vault/iac/terraform.tfvars (c'est fait) : c'est que naît le rôle CI gitea_cicd_minio, et non dans minio/iac. Sans cette étape, le workflow MinIO échoue sur role "gitea_cicd_minio" could not be found — il essaie de s'authentifier avec un rôle que personne n'a encore créé.
  2. Workflow MinIO — applique minio/iac : rôle Kubernetes pour le Vault Secrets Operator, et génération du mot de passe root dans kvv2/minio/config.
  3. ArgoCD synchronise l'application (déclarée dans chart/values.yaml).
  4. Vérifier : kubectl -n tools get vaultstaticsecret minio (secret matérialisé) puis kubectl -n tools get pods -l app=minio.

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

Une seule chose à faire : déclarer son bucket dans values.yaml, en disant à quelle app il appartient.

buckets:
  - name: mon-app-fichiers
    app: mon-app          # ← la seule déclaration
    policy: none          # privé : l'accès passe par des URL signées

Tout le reste en découle, sans rien écrire ailleurs :

  • iac/consumers.tf lit ce même fichier, groupe les buckets par app, et crée 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 à injecter les variables dans son Deployment (voir kadans-api pour l'exemple). Le secret porte MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET (le premier) et MINIO_BUCKETS (tous, séparés par des virgules).

Plusieurs buckets pour une même app

C'est prévu, et c'est le cas courant : deux contenus aux cycles de vie 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

Détruire random_password.app["<app>"] et relancer le plan suffit : la clé change, force_destroy = false garde le compte, et les objets déjà déposés conservent leur propriétaire.