Quand vous construisez un serveur MCP qui permet aux agents IA de gérer une infrastructure de production, la question n'est pas l'agent peut-il redémarrer votre application -- c'est devrait-il le faire, et qui l'a autorisé.
Le serveur MCP de sh0 a commencé avec 12 outils en lecture seule. Un agent IA pouvait lister les applications, vérifier l'état du serveur, lire les logs. Utile, mais limité. La Phase 3 ajoute les opérations d'écriture : redémarrer, déployer, mettre à l'échelle, sauvegarder, déclencher des tâches cron, et oui -- supprimer des applications et des bases de données.
Le défi : comment donner un vrai pouvoir aux agents IA sans créer un piège ?
Le modèle de sécurité à trois couches
Couche 1 : clés API à portée limitée
Chaque connexion MCP s'authentifie avec une clé API. Chaque clé porte désormais une portée : read, standard ou admin.
La portée détermine à quel niveau de risque d'outils la clé peut accéder :
| Portée | Outils de lecture | Outils d'écriture | Outils destructifs |
|---|---|---|---|
read | Oui | Non | Non |
standard | Oui | Oui | Non |
admin | Oui | Oui | Oui (avec confirmation) |
L'implémentation est délibérément simple. Pas de matrices RBAC complexes, pas de listes de permissions par outil. Trois niveaux, trois catégories de risque, une matrice directe. C'est important parce que les personnes qui configurent ces clés doivent comprendre le modèle de sécurité en 30 secondes.
Pour la rétrocompatibilité, les clés API existantes ont par défaut la portée admin. Les utilisateurs authentifiés par JWT (le tableau de bord) contournent entièrement l'application des portées -- ils ont déjà un accès complet.
Couche 2 : classification des risques
Chaque outil MCP a un niveau de risque :
- Read :
list_apps,get_server_status,server_metrics-- ne peut pas modifier l'état - Write :
restart_app,deploy_app,scale_app-- modifie l'état mais est réversible - Destructive :
delete_app,delete_database-- permanent, irréversible
Le niveau de risque est déclaré dans la spécification OpenAPI via les extensions x-mcp-risk, le même mécanisme que la Phase 2 a introduit pour l'auto-génération des définitions d'outils. Mais pour l'application, nous utilisons une simple fonction tool_risk() avec un match dans la couche transport. La spécification est la source de vérité pour la documentation ; le match est la source de vérité pour l'application. Délibérément découplés -- on ne peut pas accidentellement modifier l'application en éditant un commentaire de documentation.
Couche 3 : jetons de confirmation
Même avec la portée admin, les opérations destructives ne s'exécutent pas immédiatement. À la place :
- L'agent IA appelle
delete_appavecapp_id: "myapp" - Le serveur MCP renvoie un message :
- > Cette action va SUPPRIMER l'application 'myapp' et son conteneur. Cette action est irréversible. Pour confirmer, appelez confirm_action avec le jeton :
abc-123-def - L'agent doit appeler
confirm_actionavec ce jeton - C'est seulement alors que la suppression s'exécute
Le jeton est :
- À usage unique : supprimé après confirmation
- Limité dans le temps : TTL de 5 minutes avec éviction paresseuse
- Lié à l'utilisateur : l'utilisateur qui confirme doit correspondre à l'utilisateur qui a fait la demande
- En mémoire : pas de persistance, pas de cron de nettoyage -- juste un HashMap avec des vérifications de TTL
Cela donne à l'IA (ou à l'humain qui la supervise) une pause délibérée. L'agent voit une description claire des conséquences et doit faire un second appel explicite. Pour les workflows agentiques où un humain examine les actions de l'agent, cela crée un point de contrôle visible.
Pourquoi ne pas simplement utiliser un middleware ?
L'alternative était un middleware Axum qui intercepte toutes les requêtes MCP et vérifie les permissions. Nous avons rejeté cette approche pour trois raisons :
- Les outils MCP ne correspondent pas 1:1 aux endpoints REST. L'outil
confirm_actionn'a pas d'équivalent REST. L'outilget_app_logsappelle Docker directement, pas un handler REST.
- Le flux de confirmation est spécifique à MCP. Les clients REST utilisent l'interface du tableau de bord pour la confirmation. Les clients MCP ont besoin d'un mécanisme au niveau du protocole.
- La sémantique des portées diffère entre REST et MCP. Une clé API REST pourrait avoir des permissions fines par endpoint. Les portées MCP sont plus grossières par conception -- les agents IA ont besoin de modèles d'accès simples et prévisibles.
La piste d'audit
Chaque opération MCP d'écriture et destructive est journalisée dans le système d'audit avec un préfixe mcp: :
mcp:restart_app app app-123 myapp
mcp:deploy_app deploy dep-456 myapp
mcp:delete_app app app-123 myappCela rend triviale la réponse à « qu'a fait l'agent IA ? » après coup. Filtrez les logs d'audit par mcp:* et vous avez un historique complet des mutations initiées par l'IA.
Ce que nous avons appris
Simple bat configurable. Trois portées, trois niveaux de risque, une matrice. Tout le monde peut comprendre. La tentation était de construire des permissions par outil, des hiérarchies de rôles, des fenêtres d'accès temporelles. Toute cette complexité servirait des cas particuliers tout en rendant le cas commun plus difficile à raisonner.
Les jetons de confirmation sont étonnamment efficaces. Ils résolvent deux problèmes : empêcher la destruction accidentelle, et créer un point de décision visible dans l'audit. Ils fonctionnent aussi naturellement avec la façon dont les agents IA opèrent -- l'agent voit le message d'avertissement dans son contexte et peut décider s'il continue.
La colonne scopes existante a évité une migration. La table des clés API avait déjà un champ scopes de forme libre. Nous l'avons réutilisé avec une sémantique définie (read, standard, admin) et une valeur par défaut rétrocompatible. Aucun changement de schéma, aucune migration, aucun risque d'interruption.
Prochaines étapes
La Phase 3 est terminée et entre dans deux tours d'audits indépendants. Les auditeurs examineront : - La correction de l'application des portées (pas de chemins de contournement) - La sécurité des jetons de confirmation (pas de rejeu, pas d'inter-utilisateur, pas d'attaques de timing) - La sécurité de l'exécuteur d'écriture (pas d'unwrap, gestion correcte des erreurs, appels Docker corrects) - La rétrocompatibilité (outils de lecture existants inchangés, clés existantes continuent de fonctionner)
Après les audits, nous testerons avec Claude Desktop comme client MCP pour valider l'expérience de bout en bout.