Files
factory/doc/adr/20260726-stockage-objet-minio.md
T
arcodangeandClaude Opus 5 e0cd93c6d3 docs(adr) — stockage objet : le point « non vérifié » est tranché par le réel
L'ADR annonçait que les noms d'actions MinIO de la politique du provisionneur
venaient de la documentation, pas d'un essai. Le premier apply (kadans,
2026-07-26) a répondu : tout le bloc admin passe, il manquait `s3:ListBucket`
côté S3 — le provider interroge l'existence du bucket avant de le créer.

La conséquence devient un constat, avec ce que ListBucket concède (la vue des
clés) et ce qu'il ne concède pas (leur contenu).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 11:43:47 +02:00

8.2 KiB

← ADRs · factory · 20260726 — stockage objet (MinIO) : qui déclare quoi

Cross-references (bidirectionnel : chaque fichier listé doit citer cette ADR en tête)

ADR 20260726 : stockage objet (MinIO) — qui déclare quoi, et qui détient quoi

Status

Proposed

Context

MinIO est déployé dans le namespace tools (chart officiel, standalone, volume Longhorn). Le premier consommateur est Kadans, qui doit téléverser des rendus vidéo depuis le navigateur pour qu'ils suivent l'utilisateur d'un appareil à l'autre.

Trois questions se posaient, et elles sont indépendantes :

  1. Qui déclare les buckets d'une application ?
  2. Qui détient les identifiants capables de les créer ?
  3. Comment l'application lit les siens à l'exécution ?

Une première version faisait tout porter par tools : une liste de consommateurs dans son Terraform, les buckets dans son chart. Elle a été rejetée — à ce rythme, chaque bucket de chaque application devient une PR sur l'infra partagée, et le dépôt commun devient le goulot de tout le monde.

Decision

1. Chacun son périmètre

Une application déclare ses buckets depuis son propre dépôt. tools fournit le serveur, un module de standardisation, et un compte de provisionnement — pas la liste.

# iac/main.tf de l'application
module "stockage" {
  source    = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
  app       = "kadans"
  buckets   = ["kadans-videos"]
  providers = { minio = minio }
}

Le module crée les buckets (privés), une politique bornée à ces buckets, un compte de service, et écrit ses clés dans kvv2/minio/<app>.

2. Trois identités, trois portées

Identité Peut Ne peut pas Qui la lit
root MinIO tout le seul pipeline minio (kvv2/minio/config)
provisionneur créer bucket, politique, compte de service lire ou écrire un objet le rôle CI de chaque app (kvv2/minio/provisioner)
compte de service d'une app lire/écrire dans ses buckets tout le reste le pod de l'app (kvv2/minio/<app>)

C'est la pièce qui rend le point 1 possible. Provisionner demande des droits d'administration ; confier le root aurait donné à chaque application la lecture des objets de toutes les autres. Le provisionneur, lui, peut créer des buckets — une nuisance si une app est compromise — mais pas lire les vidéos d'une autre.

3. La lecture est une propriété de la plateforme

Le module Vault central app_policy accorde à toute application la lecture de kvv2/data/minio/<son nom>, inconditionnellement.

Pas de drapeau, pas de déclaration par app : le chemin porte le nom de l'application, donc la règle ne peut 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, pas un privilège.

Conséquence pratique : déclarer un consommateur se fait à un seul endroit, son propre iac/. Rien à synchroniser, donc rien à oublier.

4. Les octets ne passent pas par l'API

L'application signe des URL présignées ; le navigateur téléverse directement vers MinIO. Faire transiter 50 à 200 Mo par un pod applicatif doublerait le transit et exposerait l'API à un seul gros fichier.

Corollaires :

  • l'endpoint signé doit être joignable par le navigateur, donc public (s3.arcodange.fr) — une page servie en HTTPS ne peut pas téléverser vers http:// (contenu mixte), et un TLD interne ne se résout pas hors du LAN ;
  • CORS liste les origines exactes de l'application, jamais * : une URL présignée qui fuiterait serait sinon rejouable depuis n'importe quel site ;
  • l'ingress public ne porte pas de basic-auth, contrairement aux autres : une requête S3 porte sa propre signature, et un défi HTTP Basic casserait un PUT présigné auquel le navigateur ne peut pas répondre.

5. Un bucket par cycle de vie, pas par application

Une application peut avoir plusieurs buckets. Deux contenus aux durées de vie différentes méritent deux politiques de purge — Kadans en aura deux (un rendu de travail à garder, un aperçu régénérable).

Le compte de service est par application : ajouter un bucket ne crée aucune clé, le compte existant gagne l'accès.

Consequences

  • Le dépôt tools n'est plus modifié quand une application change ses buckets. C'était l'objet de la décision.
  • Ordre de déploiement contraint : le module doit exister sur main de tools avant qu'une application l'appelle (?ref=main), et le provisionneur doit exister avant le premier plan d'application.
  • Le provisionneur est un secret partagé entre les rôles CI. Sa compromission permet de créer des buckets et des comptes, pas de lire des objets. Si ce risque devient inacceptable, la suite est une identité de provisionnement par application, bornée par préfixe de bucket — MinIO ne le permet pas simplement aujourd'hui.
  • Vérifié par le premier apply (kadans, 2026-07-26) : les actions d'administration passent telles quelles — compte de service, politique et attachement ont été créés. Une seule correction a été nécessaire côté S3, s3:ListBucket : le provider interroge l'existence du bucket (HeadBucket) avant de le créer, et MinIO répond Access Denied sans cette action. Elle donne au provisionneur la vue des clés d'un bucket, jamais leur contenu — la garantie « ne peut pas lire les vidéos d'une autre app » tient toujours.

Alternatives Considered

Option Pourquoi non
tools détient la liste des consommateurs Chaque bucket de chaque app devient une PR sur l'infra partagée — rejeté par le fondateur, et c'est le cœur de cette ADR
Les buckets déclarés dans le chart de MinIO (values.yaml) Même défaut : la déclaration vit chez l'infra, pas chez l'application
Chaque app crée son compte de service avec le root Le root lit et écrit tous les objets de toutes les apps : le distribuer à chaque rôle CI revient à ne plus avoir de cloisonnement
Déclarer la lecture Vault par app (kv_read_paths) Mécanisme réel, mais c'est la trappe pour lire un secret appartenant à une autre app (creds GCS de Longhorn pour l'ERP). Y ranger un motif standard le rend invisible et oblige à le redéclarer partout
Un drapeau object_storage = true par app Une déclaration de plus à tenir synchronisée avec le iac/ de l'app — donc une à oublier. La règle inerte ne coûte rien
Une identité de provisionnement par app Souhaitable, mais MinIO ne borne pas simplement les actions d'administration par préfixe. À reconsidérer si le modèle de menace change

Success Metrics

  • Ajouter une application consommatrice ne touche aucun fichier de tools.
  • Un compte de service compromis ne donne accès qu'aux objets qu'il gérait déjà.
  • Le root de MinIO n'apparaît dans aucune politique Vault en dehors du pipeline minio.