Points essentiels

  • La spécification MCP exige OAuth 2.1 pour le transport SSE en production — sans authentification, n'importe quel client peut invoquer tous vos tools
  • Le sandboxing des processus serveur MCP via Docker, namespaces Linux ou seccomp limite le blast radius d'un serveur compromis
  • Chaque tool call doit être logué avec timestamp, paramètres complets et résultat — requis pour la conformité NIS2 et DORA
  • Les serveurs MCP tiers (Smithery) constituent de la supply chain logicielle : les auditer avant installation et les pincer sur une version signée

Le Model Context Protocol donne aux LLMs la capacité d'agir sur vos systèmes — lire des fichiers, écrire en base de données, envoyer des emails, appeler des APIs. Cette puissance s'accompagne d'une surface d'attaque nouvelle que les frameworks de sécurité traditionnels ne couvrent pas : tool poisoning, escalade de privilèges via les tools, exfiltration de données, et compromission supply chain via les registres de serveurs MCP. Ce guide décrit comment sécuriser un déploiement MCP de bout en bout : authentification OAuth 2.1, sandboxing des processus, validation des tools, logging, rate limiting et conformité NIS2/DORA.

Périmètre : Ce guide couvre les serveurs MCP en production (transport SSE sur serveur distant). Pour les serveurs stdio locaux (Claude Desktop, Cursor), le modèle de menace est différent — la sécurité repose sur les droits OS de l'utilisateur local. Consultez également notre article sur les risques supply chain des registres MCP pour les serveurs tiers.

Threat model : surfaces d'attaque MCP en production

Avant de configurer les contre-mesures, établir le threat model spécifique à MCP est indispensable. Les surfaces d'attaque diffèrent des APIs REST classiques :

Threat model MCP en production — surfaces d'attaque et impacts
Surface Vecteur Impact Probabilité 2026
Transport SSE sans authAccès direct à l'endpoint /sseInvocation non autorisée de tous les toolsÉlevée (CVE-2026-59822)
Descriptions de toolsTool Poisoning via supply chainExfiltration données, actions non autoriséesMoyenne (en croissance)
Processus serveurRCE via tool mal implémentéCompromission du système hôteFaible à moyenne
Tool outputsExfiltration encodée dans les réponsesFuite de données vers l'attaquantFaible (sophistiqué)
Registres MCP (Smithery)Serveur malveillant ou mis à jourCompromission supply chainMoyenne (incidents 2026)

Authentification OAuth 2.1 — configuration complète

La spécification MCP 2025-11-05 exige OAuth 2.1 pour le transport SSE. Sans authentification, l'endpoint /sse est accessible par n'importe quel client MCP — c'est la cause principale des CVE documentées en 2026 (LiteLLM CVE-2026-59822, nginx-ui CVE-2026-33032).

# Configuration OAuth 2.1 avec FastMCP + token JWT
from mcp.server.fastmcp import FastMCP
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timezone

JWT_SECRET = os.environ["JWT_SECRET"]  # 256 bits minimum
JWT_ALGORITHM = "HS256"
ALLOWED_SCOPES = {"mcp:tools:read", "mcp:tools:execute"}

security = HTTPBearer()

def verifier_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    try:
        payload = jwt.decode(
            credentials.credentials,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM],
            options={"verify_exp": True}
        )
        scopes = set(payload.get("scope", "").split())
        if not scopes.intersection(ALLOWED_SCOPES):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Scopes insuffisants"
            )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expiré")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Token invalide")

# FastMCP avec middleware d'authentification
mcp = FastMCP("api-securisee",
    host="0.0.0.0",
    port=8080,
    dependencies=[Depends(verifier_token)]
)

Pour un déploiement d'entreprise, utilisez un Identity Provider (IdP) existant (Keycloak, Okta, Azure AD) en mode OAuth 2.1 Authorization Code Flow avec PKCE :

# .well-known/oauth-authorization-server (exposition des metadata OAuth 2.1)
{
  "issuer": "https://mcp.interne.example.com",
  "authorization_endpoint": "https://idp.interne.example.com/oauth/authorize",
  "token_endpoint": "https://idp.interne.example.com/oauth/token",
  "scopes_supported": ["mcp:tools:read", "mcp:tools:execute"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"]
}

Sandboxing des processus serveur MCP

Le serveur MCP s'exécute avec les permissions du compte système qui le lance. Un tool mal implémenté (injection de commande, path traversal) peut compromettre l'hôte complet. Le sandboxing limite le blast radius.

Isolation Docker

# Dockerfile sécurisé pour serveur MCP SSE
FROM python:3.12-slim

# Utilisateur non-root
RUN useradd -r -u 1001 -s /sbin/nologin mcpuser
WORKDIR /app

# Dépendances d'abord (cache layer)
COPY pyproject.toml .
RUN pip install --no-cache-dir -e ".[cli]" && \
    pip cache purge

# Code applicatif
COPY --chown=mcpuser:mcpuser . .
USER mcpuser

# Lecture seule sauf /tmp
RUN chmod -R 755 /app
VOLUME ["/tmp"]

# Pas de capabilities Linux inutiles
EXPOSE 8080
ENTRYPOINT ["python", "-m", "mon_serveur_mcp"]
# docker-compose.yml avec restrictions sécurité
services:
  mcp-server:
    build: .
    ports:
      - "127.0.0.1:8080:8080"  # Bind local uniquement
    read_only: true
    tmpfs:
      - /tmp:mode=1777,size=100m
    security_opt:
      - no-new-privileges:true
      - seccomp:seccomp-profile.json
    cap_drop:
      - ALL
    environment:
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
    secrets:
      - jwt_secret

Namespace Linux avec systemd

# /etc/systemd/system/mcp-server.service
[Unit]
Description=MCP Server API Interne
After=network.target

[Service]
Type=simple
User=mcpuser
Group=mcpuser
ExecStart=/usr/bin/python3 -m mon_serveur_mcp
Restart=on-failure
RestartSec=5s

# Isolation système
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/mcp
NoNewPrivileges=yes
CapabilityBoundingSet=
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
MemoryMax=512M
CPUQuota=50%

[Install]
WantedBy=multi-user.target

Validation et limitation des tools

Chaque tool doit implémenter la validation de ses propres paramètres, indépendamment de la validation côté client. Le principe de défense en profondeur s'applique aussi à MCP.

from pydantic import BaseModel, Field, validator
from typing import Annotated
import re

class RequeteSQL(BaseModel):
    requete: Annotated[str, Field(min_length=7, max_length=1000)]
    timeout_ms: Annotated[int, Field(ge=100, le=30000)] = 5000

    @validator("requete")
    def valider_requete_lecture_seule(cls, v):
        # Uniquement SELECT - défense contre injection
        if not re.match(r"^\s*SELECT\s", v, re.IGNORECASE):
            raise ValueError("Seules les requêtes SELECT sont autorisées")
        # Bloquer les sous-requêtes destructrices
        mots_interdits = ["DROP", "DELETE", "TRUNCATE", "INSERT", "UPDATE", "EXEC"]
        v_upper = v.upper()
        for mot in mots_interdits:
            if mot in v_upper:
                raise ValueError(f"Instruction non autorisée: {mot}")
        return v

@mcp.tool()
async def requete_db(requete: RequeteSQL, contexte=Depends(verifier_token)) -> list[dict]:
    # Loguer l'appel avant exécution
    logger.info("tool_call", extra={
        "tool": "requete_db",
        "user": contexte.get("sub"),
        "requete_hash": hashlib.sha256(requete.requete.encode()).hexdigest()[:16],
        "timestamp": datetime.now(timezone.utc).isoformat()
    })
    # Exécuter avec timeout
    return await asyncio.wait_for(
        executer_requete_db(requete.requete),
        timeout=requete.timeout_ms / 1000
    )

Logging et audit trail

Chaque tool call doit être logué de manière structurée. Pour NIS2 et DORA, la traçabilité des actions des agents IA sur les systèmes d'information est une exigence explicite.

import logging, json
from datetime import datetime, timezone

# Logger structuré JSON
class MCPAuditLogger:
    def __init__(self):
        self.logger = logging.getLogger("mcp.audit")

    def log_tool_call(self, tool_name: str, params: dict, result: any,
                      user_id: str, duration_ms: float, success: bool):
        entry = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "event": "tool_call",
            "tool": tool_name,
            "user_id": user_id,
            "params_hash": hashlib.sha256(
                json.dumps(params, sort_keys=True).encode()
            ).hexdigest()[:16],
            "duration_ms": round(duration_ms, 2),
            "success": success,
            "result_size_bytes": len(json.dumps(result)) if result else 0
        }
        self.logger.info(json.dumps(entry, ensure_ascii=False))

audit = MCPAuditLogger()

Configurez la rotation des logs et l'envoi vers votre SIEM (Elastic, Splunk, Wazuh) pour la détection d'anomalies (volume inhabituel de tool calls, patterns d'exfiltration).

Rate limiting et circuit breakers

# Rate limiting par token OAuth
from collections import defaultdict
import time

class RateLimiter:
    def __init__(self, max_calls: int, window_seconds: int):
        self.max_calls = max_calls
        self.window = window_seconds
        self.calls = defaultdict(list)

    def check(self, user_id: str) -> bool:
        now = time.time()
        window_start = now - self.window
        # Nettoyer les appels hors fenêtre
        self.calls[user_id] = [t for t in self.calls[user_id] if t > window_start]
        if len(self.calls[user_id]) >= self.max_calls:
            return False
        self.calls[user_id].append(now)
        return True

# 60 tool calls par minute par utilisateur
limiter = RateLimiter(max_calls=60, window_seconds=60)

def check_rate_limit(contexte=Depends(verifier_token)):
    user_id = contexte.get("sub", "anonymous")
    if not limiter.check(user_id):
        raise HTTPException(
            status_code=429,
            detail="Trop de requêtes. Limite: 60 appels/minute.",
            headers={"Retry-After": "60"}
        )

Revue des serveurs MCP tiers

Les serveurs MCP installés depuis Smithery ou d'autres registres constituent de la supply chain logicielle. Avant d'approuver un serveur tiers en production :

  • Auditez le code source complet (cherchez les appels réseau dans les définitions de tools, les conditions temporelles, les encodages base64)
  • Vérifiez la signature Smithery du package
  • Pincez la version dans votre config ("@mon-serveur-mcp@1.2.3" pas "@mon-serveur-mcp@latest")
  • Analysez les descriptions de tools pour les caractères Unicode suspects (script de détection dans notre article Tool Poisoning MCP)
  • Testez dans un environnement isolé sans accès aux données sensibles

Implications NIS2 et DORA

Pour les entités soumises à NIS2 et DORA, les déploiements MCP en production relèvent de la gestion des risques liés aux tiers (Art. 21 NIS2, Art. 28-30 DORA) quand des serveurs tiers sont utilisés, et de l'exigence de traçabilité des systèmes critiques :

  • Journalisation : tous les tool calls doivent être conservés au minimum 12 mois (NIS2) ou 5 ans (DORA pour les entités financières)
  • Gestion des tiers : les serveurs MCP tiers doivent faire l'objet d'une évaluation de sécurité avant déploiement en production
  • Continuité : définissez les RTO/RPO des services dépendant des tools MCP — une interruption du serveur MCP impacte directement les agents IA en production
  • Tests : incluez les serveurs MCP dans les exercices de réponse à incident et les tests de pénétration annuels

Checklist de déploiement MCP production

  • [ ] Authentification OAuth 2.1 activée sur tous les endpoints SSE
  • [ ] Processus serveur isolé (Docker avec utilisateur non-root, capabilities réduites)
  • [ ] Validation Pydantic sur tous les paramètres de tous les tools
  • [ ] Logging structuré JSON de tous les tool calls avec user_id, params_hash, durée
  • [ ] Rate limiting par utilisateur (60 calls/minute ou adapté à votre usage)
  • [ ] Timeout configuré sur toutes les opérations réseau (max 30s)
  • [ ] Bind local (127.0.0.1) sauf si exposition externe intentionnelle
  • [ ] Analyse Unicode des descriptions de tous les tools tiers installés
  • [ ] Versions des serveurs MCP tiers pinnées sur des releases signées
  • [ ] Rotation des secrets JWT/OAuth tous les 90 jours maximum
  • [ ] Alertes SIEM sur volumes anormaux de tool calls
  • [ ] Test de pénétration MCP inclus dans le périmètre annuel

Questions fréquentes

OAuth 2.1 est-il obligatoire pour MCP en production ?

La spécification MCP 2025-11-05 l'exige pour le transport SSE exposé sur le réseau. Pour les serveurs stdio locaux (Claude Desktop, Cursor sur une machine personnelle), l'authentification repose sur les droits OS — aucun OAuth requis. Dès que le serveur est exposé sur le réseau (même réseau interne), OAuth 2.1 ou a minima une API key longue et rotative est non négociable. CVE-2026-59822 (LiteLLM) a démontré les conséquences d'un endpoint MCP sans authentification : accès non authentifié à tous les tools exposés.

Comment auditer les tool calls pour détecter des exfiltrations ?

Mettez en place des règles de détection SIEM sur : 1) Volume anormal de calls à un tool spécifique (baseline ± 3 sigma). 2) Résultats de tools contenant des patterns suspects (base64, hex strings longs, données encodées). 3) Calls depuis des IPs inhabituelles ou à des heures atypiques. 4) Chaînes de calls inhabituelles (séquence tool A → tool B → tool C jamais vue). Les logs structurés JSON décrits dans cet article alimentent directement ces règles dans Elastic/Splunk.

Un serveur MCP tiers compromis peut-il affecter mon SI ?

Oui, selon les permissions accordées. Si le serveur MCP compromis a accès en écriture à votre base de données ou peut envoyer des emails en votre nom, la compromission peut être critique. Appliquez le principe de least privilege : un serveur MCP de recherche ne devrait avoir que les droits de lecture. Sandboxez chaque serveur dans son propre conteneur avec uniquement les variables d'environnement nécessaires. Pour les serveurs tiers en particulier, limitez leurs droits aux seules ressources qu'ils ont documenté nécessiter.

Comment inclure MCP dans un test de pénétration ?

Incluez dans le scope : 1) Test de bypass d'authentification sur l'endpoint SSE. 2) Fuzzing des paramètres de tools pour détecter les injections (SQL, command, path traversal). 3) Test d'escalade de privilèges via les tools (un tool de lecture peut-il être détourné en écriture ?). 4) Test de rate limiting bypass (header X-Forwarded-For pour usurper des IPs). 5) Analyse des descriptions de tools pour le tool poisoning. 6) Test de cross-server contamination si plusieurs serveurs MCP sont connectés simultanément.

MCP est-il compatible avec les architectures Zero Trust ?

Oui, avec une implémentation correcte. Les principes Zero Trust s'appliquent : 1) Never trust, always verify — OAuth 2.1 sur chaque request, même depuis le réseau interne. 2) Least privilege — chaque token OAuth a uniquement les scopes nécessaires. 3) Assume breach — sandboxing des processus, logging de tous les calls. 4) Micro-segmentation — chaque serveur MCP dans son propre réseau/namespace avec des règles de firewall spécifiques. 5) Continuous verification — rotation des tokens, révocation rapide en cas de compromission.

Pour aller plus loin : Tool Poisoning et Rug Pull Attack MCP, risques supply chain des registres MCP, la surface d'attaque MCP que personne n'audite, sécuriser les agents MCP 2026.