# J'ai noté mon propre serveur MCP

Langue : fr

Identifiant : article:auditer-ses-propres-mcp

Noa Matout

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.

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](https://github.com/NoaMatout/mcp-reach). 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](https://www.nsa.gov/Portals/75/documents/Cybersecurity/CSI_MCP_SECURITY.pdf),
  « 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](https://labs.cloudsecurityalliance.org/research/csa-research-note-mcp-tool-poisoning-auto-execution-20260701/),
  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

exposureBefore : 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.

meterExposure : 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.

exposureFirstPass : 1,6 sur 10. Note de la première version durcie, qui lisait encore les clés de l'agent.

exposureAfter : 1,5 sur 10. Note de la version finale, où les clés et la mémoire de l'agent sont hors d'atteinte.

## Je vérifie ce qui passe.

Même sonde, même machine. Seul le confinement change.

### Sans confinement

- Ouvrir une connexion vers internet : atteint.

- Lire les clés d’API de l’agent : lues.

- Écrire hors de son périmètre : réussie.

### Dans un confinement réel

- Ouvrir une connexion vers internet : injoignable.

- Lire les clés d’API de l’agent : inexistantes pour ce processus.

- Écrire hors de son périmètre : refusée.

[Consulter le code de l'outil qui mesure ce qu'un serveur peut atteindre](https://github.com/NoaMatout/mcp-reach)
