01 / Mes articles / Publié
J'ai noté mon propre serveur MCP
Tout le monde audite les serveurs des autres. J'ai mesuré le mien, et une bonne note ne voulait pas dire une porte fermée.
Rédigé le
La NSA a publié en mai 2026 une fiche de recommandations sur la sécurité des serveurs MCP. En juillet, la Cloud Security Alliance relevait que les principaux environnements de développement lancent automatiquement les serveurs MCP déclarés dans un projet, avec les droits du développeur et sans isolation de processus.
Mon premier réflexe a été de me demander lesquels, parmi les serveurs que j'utilise, étaient concernés. C'était le mauvais réflexe. J'en écris un moi-même, et il tourne en production sur mon serveur.
La première mesure
systemd-analyze security donne une note d'exposition à un service, de zéro à
dix, en lisant les directives de confinement qu'il déclare. Plus la note est
basse, mieux c'est. Mon serveur MCP, celui qui interroge mes données
d'entraînement, obtient 9,2.
Overall exposure level: 9.2 UNSAFE
Aucune directive de durcissement. Il tourne sous le compte principal de mon agent, avec un accès complet au système de fichiers, sans limite mémoire, sans filtre d'appels système. Je l'avais écrit avec soin côté code, et lancé sans aucune précaution côté système.
Pour situer, j'ai mesuré au passage un service que j'avais écrit cette semaine en faisant attention, avec utilisateur dynamique, système de fichiers protégé et mémoire plafonnée. Il obtient 8,1.
Overall exposure level: 8.1 EXPOSED
Huit sur dix en appliquant les bonnes pratiques. La note est sévère et elle a raison de l'être : un service qui garde l'accès au réseau, aucun filtre d'appels système et toutes les capacités par défaut reste une surface large, même propre par ailleurs.
La deuxième mesure, qui m'a fait arrêter de sourire
Une note d'exposition lit ce qu'un service déclare. Elle ne vérifie jamais ce qu'un processus peut atteindre. J'ai voulu combler l'écart avec une sonde : un petit programme qui essaie d'ouvrir une connexion vers internet, de lire les clés d'API de mon agent, et d'écrire hors de son périmètre. Je l'ai lancée deux fois : sans confinement, puis dans un service transitoire déclarant tout ce qu'il faut, réseau privé, répertoire personnel en lecture seule, système de fichiers protégé, espace temporaire isolé.
Sans confinement :
RESEAU ATTEINT
CONFIG AGENT LUE
ECRITURE /tmp REUSSIE
Avec confinement :
RESEAU ATTEINT
CONFIG AGENT LUE
ECRITURE /tmp REUSSIE
Strictement identique. Le confinement n'avait rien confiné.
Ce que le journal disait, et que personne ne lit
L'explication était là depuis le début, dans le journal système, dans une ligne que je n'avais aucune raison d'aller chercher :
PrivateNetwork=yes is configured, but the kernel does not support
or we lack privileges for network namespace, proceeding without.
« Proceeding without ». Il avertit, puis il continue sans la protection. La commande a réussi, le service a démarré, le code de retour valait zéro, et la protection demandée n'existait pas.
C'est une question de privilèges : un gestionnaire de session utilisateur ne peut pas créer d'espace de noms réseau. Les mêmes directives appliquées par l'administrateur fonctionnent, et la sonde rapporte alors un réseau injoignable. Le confinement est donc correct, ce sont les droits qui manquaient.
J'ai essayé l'autre voie, celle qui ne demande théoriquement aucun privilège :
bubblewrap, l'outil d'isolation qui fait tourner Flatpak. Refus immédiat :
bwrap: setting up uid map: Permission denied
Les versions récentes d'Ubuntu restreignent les espaces de noms utilisateur non privilégiés, par un réglage actif par défaut. En le levant, bubblewrap fait le travail parfaitement : réseau coupé, clés carrément inexistantes puisque le répertoire personnel est remplacé par un système de fichiers vide. Mais lever ce réglage demande l'administrateur, et il protège de vraies attaques.
Ce que ça veut dire
Sur une machine récente, un utilisateur non privilégié ne peut pas confiner un processus sans une intervention préalable de l'administrateur. Ni par systemd, ni par bubblewrap.
Or c'est exactement la situation que décrit la Cloud Security Alliance : un développeur clone un dépôt, ce dépôt embarque une configuration MCP, son environnement lance les serveurs déclarés avec ses droits à lui. Il n'est pas administrateur de sa machine de travail, et l'outil qui lui promettrait un bac à sable sans privilèges lui rendrait un bac à sable qui n'existe pas.
Le pire n'est donc pas l'absence de confinement. C'est le confinement déclaré et silencieusement abandonné, parce qu'il produit exactement le même fichier de configuration, la même absence d'erreur, et une confiance qui n'a aucun fondement.
Ce que ça donne une fois corrigé
Sur ce serveur, l'administrateur c'est moi, donc le problème de privilèges n'existe pas : il suffit d'en faire un service système correctement durci. J'ai écrit un fichier de surcharge plutôt que de réécrire l'unité, et les chemins ouverts en écriture ne sont pas devinés, ils sont relevés sur les descripteurs que le processus avait réellement ouverts.
La note passe de 9,2 à 1,5.
avant 9.2 UNSAFE
apres 1.5 OK
L'ingestion fonctionne toujours, la synchronisation Garmin a repris sans une
erreur, et la preuve ne tient pas à la note. Elle tient à ce que le processus
en cours montre de lui-même : aucune capacité, no_new_privs actif, un espace
de montage distinct de celui du système, un filtre d'appels système en place.
Le détour qui apprend le plus
Ma première version marquait 1,6 et me paraissait finie. Puis j'ai relancé ma sonde dans ce confinement-là :
LU <configuration de l'agent, avec ses clés>
LU <un fichier de mémoire de l'agent>
refuse ecriture hors perimetre
Une note excellente, et le serveur MCP lisait toujours les clés d'API de mon agent et sa mémoire. J'avais mis le répertoire personnel en lecture seule, ce qui empêche d'écrire et pas de lire. Or pour voler une clé, lire suffit.
La correction consiste à masquer le répertoire personnel par un système de fichiers vide et à n'y réexposer que le lanceur, puis à rendre explicitement inaccessibles les dossiers de mémoire de l'agent, qui ne regardent pas un service d'ingestion sportive. Les journaux partent au journal système, ce qui supprime le dernier besoin d'écriture là-bas. Même sonde, après :
refuse <configuration de l'agent> (FileNotFoundError)
refuse <fichier de mémoire> (PermissionError)
ECRIT sa base, normal
RESEAU garmin joignable, normal
Le fichier de configuration ne renvoie pas une erreur de permission, il n'existe tout simplement plus pour ce processus.
Le gain de note entre 1,6 et 1,5 est dérisoire. Le gain réel est la différence entre un serveur qui peut lire mes clés et un serveur qui ne le peut pas. Une bonne note n'est pas une porte fermée.
Et cette fois, l'échec est bruyant
En chemin, j'ai laissé une incohérence : un chemin autorisé en écriture qui n'existait plus une fois le répertoire personnel masqué. Le service a refusé de démarrer.
Failed to set up mount namespacing: No such file or directory
Main process exited, code=exited, status=226/NAMESPACE
C'est l'exact contraire du comportement non privilégié décrit plus haut. Au niveau système, systemd échoue fermé : pas de protection, pas de démarrage. Au niveau utilisateur, il démarre sans la protection et écrit une ligne dans un journal. La même demande, deux philosophies opposées, et seule la première est défendable.
Ce que j'écris ensuite
Un outil pour la partie qui m'a le plus dérangé : vérifier ce qu'un serveur MCP peut réellement atteindre, en lançant une sonde dans les mêmes conditions de démarrage plutôt qu'en relisant les directives déclarées. Sans jamais exécuter le serveur lui-même, pour qu'auditer un serveur hostile ne consiste pas à lui donner la main.
Il est publié : Consulter le code de l'outil qui mesure ce qu'un serveur peut atteindre. Deux de ses tests sont des contrôles négatifs, et ce sont eux qui le portent : une sonde lancée dans un confinement réel doit rapporter le réseau injoignable, et sans confinement doit le rapporter joignable. Un outil qui répondrait la même chose partout passerait toute sa suite en étant inutile.
La leçon, qui dépasse MCP
Une protection qu'on n'a pas vue refuser quelque chose n'existe pas.
J'avais écrit les bonnes directives. Je ne les avais jamais vues bloquer quoi que ce soit. Entre les deux, il y a précisément la différence entre une configuration et une garantie.
Ce que je n'ai pas pu vérifier
Si le comportement est identique sur d'autres distributions : mes mesures portent sur une seule, dans une version récente. Le réglage d'Ubuntu sur les espaces de noms lui est propre, même si la limitation de privilèges de systemd, elle, ne l'est pas.
Je n'ai pas non plus audité les serveurs MCP que j'utilise sans les avoir écrits. C'est le prochain sur ma liste.
Sources
- Fiche de la NSA sur la sécurité de MCP, « Model Context Protocol (MCP): Security Design Considerations for AI-Driven Automation », publiée en mai 2026.
- Note de la Cloud Security Alliance sur l'exécution automatique, note de recherche du 1er juillet 2026, pour l'exécution automatique par les environnements de développement.
Les mesures sur mon propre serveur sont les miennes, reproductibles avec les commandes citées. Les noms de services et les chemins ont été retirés des sorties reproduites.
Périmètre de la mesure
9,2 sur 10. Note d'exposition de mon serveur MCP avant durcissement, relevée par l'outil d'analyse de systemd. Plus elle est basse, mieux c'est.
8,1 sur 10. Note d'exposition d'un autre de mes services, écrit avec les bonnes pratiques habituelles, relevée au même moment.
1,6 sur 10. Note de la première version durcie, qui lisait encore les clés de l'agent.
1,5 sur 10. Note de la version finale, où les clés et la mémoire de l'agent sont hors d'atteinte.