Héberger son propre serveur MCP : la sécurité d'abord
Installer un serveur MCP chez soi pour exposer la météo, c’est facile. Le mettre en production pour un agent qui manipule votre base de données, c’est une autre histoire. Dans mon article sur les différences entre MCP et function calling, j’expliquais quand adopter le protocole. Ici, je rentre dans le concret : comment j’ai déployé mes propres serveurs MCP auto-hébergés, et surtout comment je les ai sécurisés.
Parce que réfléchissons une seconde : un serveur MCP, c’est un processus qui expose des actions à un modèle. Un modèle qui peut être trompé (je vous ai montré comment avec la prompt injection). Donc chaque outil exposé est une porte d’entrée potentielle. Le protocole est simple, mais la sécurité ne l’est pas.
Pourquoi auto-héberger un serveur MCP
Les registres publics de serveurs MCP fleurissent en 2026. Installer un paquet tout fait, c’est tentant : npx @quelquun/outils-magiques et hop, l’agent sait tout faire. Sauf que vous n’avez aucune idée de ce qui tourne dans ce processus. Un serveur MCP malveillant peut :
- lire vos fichiers (
fs/read), - exécuter des commandes (
shell/exec), - exfiltrer vos secrets via une simple sortie réseau.
Vous me direz que vous ne vous connectez que à des serveurs de confiance. Le problème, c’est que la confiance ne se vérifie pas à l’oeil nu. Un serveur bénin peut contenir une vulnérabilité, ou une dépendance compromise. Faire tourner ça en dehors d’un sandbox, avec accès à votre homedir, c’est courir le risque que mon article sur le shell des agents décrivait.
Auto-héberger donne trois choses que les serveurs publics ne donnent pas :
- Le contrôle : vous savez exactement quels outils sont exposés et quels privilèges ils ont.
- L’isolation : chaque serveur tourne dans son propre conteneur, sans accès résiduel.
- L’audit : vous journalisez chaque appel, chaque argument, chaque résultat.
L’architecture cible
Je découpe systématiquement l’exposition en couches. Un client MCP ne se connecte jamais directement à un outil dangereux. Il passe par un proxy qui applique les règles.
Le point clé : le proxy connaît la liste des outils autorisés, valide les arguments, et refuse tout le reste. C’est une couche de plus, mais c’est elle qui transforme un agent qui peut tout en un agent qui ne peut que ce qu’il faut.
Étape 1 : un serveur MCP minimal, bien isolé
Je pars du serveur filesystem de l’article précédent et je le mets en conteneur. Rien d’exotique : une seule dépendance, l’utilisateur nobody, un volume monté en lecture seule partout sauf dans un dossier dédié.
FROM python:3.12-slim |
Le serveur lui-même n’expose que ce qui est nécessaire, et refuse tout chemin hors autorisation :
from mcp.server.fastmcp import FastMCP |
La vérification is_relative_to me sort du piège des ../ : un chemin comme /data/../etc/passwd est résolu avant le contrôle, et donc refusé. C’est le genre de garde-fou qu’on n’a pas quand on installe un serveur tout fait.
Pourquoi le dossier outbox séparé pour l’écriture ? Parce qu’un agent qui lit une base de code n’a aucune raison d’écrire dedans. Lui donner l’écriture partout, c’est lui donner la capacité de se modifier lui-même. Un agent qui peut écrire dans son contexte, c’est une faille structurelle : il peut masquer ses traces ou piéger le lecteur suivant.
Étape 2 : le sandbox Docker
Je ne réinvente pas ce que j’ai détaillé dans mon article sur le sandboxage d’agents, je l’applique. Pour du MCP, trois règles suffisent.
Lecture seule par défaut sur tout le système de fichiers, sauf le volume /data :
services: |
Pas de réseau sortant pour les serveurs qui n’en ont pas besoin. Le serveur filesystem n’a AUCUNE raison de parler à internet. network_mode: none ou un réseau interne sans passerelle.
Droits système minimaux : cap_drop: ALL, no-new-privileges, pas d’utilisateur root. Même si un attaquant échappe au serveur MCP, il retombe dans un conteneur vide, sans privilèges, sans réseau.
Étape 3 : ajouter l’authentification
Un serveur MCP en stdio (processus local) n’a pas besoin d’identité : c’est le système qui décide qui peut le lancer. Mais dès qu’on passe en réseau (SSE ou streamable HTTP), il faut authentifier. En 2026, le protocole a mûri : l’authentification OAuth 2.1 est normalisée dans la spec.
Mon approche reste volontairement simple : un jeton unique par client, validé à l’entrée du proxy.
import os |
Le jeton ne circule que dans l’infrastructure, jamais dans le prompt de l’agent. Mes réflexions sur les secrets d’agents s’appliquent à la lettre : le client (agent) ne connaît pas le jeton, c’est l’orchestrateur qui l’injecte au moment de la connexion.
Étape 4 : le proxy de contrôle
Le serveur MCP expose des outils. Tous les outils ne doivent pas être exposés à tous les agents. Je monte un proxy qui filtre tools/list et tools/call selon une politique.
POLICY = { |
Concrètement, le proxy :
- intercepter
tools/listet ne renvoyer que les outils autorisés pour cet agent, - intercepter
tools/callet bloque si l’outil n’est pas dans la politique, - journaliser chaque appel dans une base ou un fichier (qui, quoi, quand, arguments).
Quand le modèle demande un outil interdit, il ne le voit même pas. C’est mieux que de refuser après coup : la moitié du temps, l’agent ne saura pas pourquoi il échoue, mais il n’essaiera pas non plus de ruser pour contourner.
|
Cet audit log est la partie que tout le monde oublie. Quand un agent fait une bêtise, c’est ce journal qui permet de savoir ce qu’il s’est passé, exactement. C’est le prix d’un diagnostic rapide, et il se paie au moment de la conception, pas à 3h du matin.
Étape 5 : les serveurs qui appellent l’extérieur
Tous les serveurs MCP ne sont pas des fichiers locaux. Un serveur qui va chercher la météo, qui fait une recherche web ou qui appelle une API tierce a besoin de sortir sur le réseau. Deux règles pour ceux-là :
- un réseau sortant restrictif, pas un accès internet plein (via un proxy d’entreprise ou une liste blanche DNS),
- jamais les credentials de l’agent : le serveur MCP a ses propres clés API, limitées et révocables, et il s’appuie sur l’architecture de secret que j’ai décrite dans mon article dédié aux tokens.
Un cas particulier vicieux : les outils qui exécutent du code. Un agent qui génère une requête SQL, ou un script shell, transforme un problème de confiance en problème d’exécution arbitraire. Ma règle : ces outils ne reçoivent jamais une string libre. Ils reçoivent des paramètres typés, validés, et l’exécution se fait en périphérie, dans un conteneur jetable avec un timeout.
|
La règle startswith("select") est grossière et je m’en méfie : SELECT pg_sleep(10) reste un SELECT. Le vrai garde-fou, c’est le conn_readonly : une connexion dont les privilèges interdisent toute écriture, créée avec un rôle PostgreSQL dédié. La validation applicative traîne, la vraie sécurité est dans la base.
Le verdict
Un serveur MCP auto-hébergé, c’est un service comme les autres : il obéit aux mêmes règles que n’importe quel processus exposé. Conteneuriser, limiter les privilèges, authentifier, filtrer, journaliser. L’erreur à ne pas faire, c’est de traiter MCP comme un cas particulier alors que c’est juste une API de plus.
L’avantage de tout faire soi-même, c’est que chaque couche est au bon endroit : le proxy connaît la politique, le conteneur ne sait rien du réseau, la base refuse les écritures. Et quand quelque chose tourne mal, l’audit log me dit quoi, par qui, et avec quels arguments.
La suite logique, c’est de multiplier les agents qui partagent ces serveurs : un MCP secure en commun, trois agents spécialisés dessus.