Les frontières de sécurité de MCP stdio pour les agents IA locaux
Sécurité MCP stdio pour les agents IA locaux : séparez le contexte des outils des actions authentifiées, avec approbations, contrôles SSH et journaux d'audit inviolables.

Les serveurs MCP locaux sont souvent considérés comme inoffensifs parce qu'ils communiquent avec stdio et fonctionnent sur le même Mac que l'agent. Cette conclusion ne tient plus dès que le serveur peut appeler une API externe, utiliser SSH, lire un fichier d'identifiants ou lancer une commande shell avec les droits du développeur. Le transport local supprime un saut réseau. Il ne réduit pas les droits du processus qui reçoit les requêtes.
La frontière importante est simple : un serveur MCP doit exposer du contexte et des opérations strictement limitées ; une passerelle d'actions de confiance doit détenir les identifiants et effectuer les actions qui sortent de la machine. Mélanger ces rôles donne à un modèle de langage un chemin pratique entre des instructions non fiables et une autorité durable. J'ai vu cette erreur se présenter sous la forme d'un outil bien rangé dans un seul fichier, puis devenir un fourre-tout de jetons, d'appels de sous-processus et d'exceptions que personne ne sait expliquer pendant un incident.
Stdio est un transport, pas une décision de confiance
La sécurité MCP stdio commence par un constat : l'entrée et la sortie standard n'authentifient pas l'intention. Le client MCP lance le serveur et échange des messages JSON-RPC par des tubes. La spécification du Model Context Protocol décrit stdio comme un transport : le serveur lit les messages sur l'entrée standard et écrit les réponses sur la sortie standard. Elle ne prétend pas que ce transport prouve qu'une requête est sûre, approuvée par une personne ou même produite par le modèle attendu.
Un client local peut envoyer directement une requête tools/call. Il peut contourner la boucle habituelle du modèle, répéter les appels à la vitesse de la machine, choisir des arguments déconseillés dans la description de l'outil et conserver chaque résultat reçu. Si une extension d'éditeur compromise lance le client, le serveur n'a aucun moyen magique de savoir qu'il ne s'agit pas de Claude Code ou d'un autre appelant attendu, à moins que l'architecture environnante lui fournisse cette information.
L'arbre des processus habituel offre également moins d'isolation qu'on ne le pense. Si votre agent démarre un serveur MCP sous votre compte, ce serveur hérite généralement de votre identité utilisateur, de son répertoire de travail, de son environnement, de ses permissions sur les fichiers, de son accès réseau et des secrets éventuellement laissés dans les variables d'environnement. Un tube ne diminue pas cette autorité.
Traitez chaque appel d'outil comme une requête non fiable provenant d'un processus qui a convaincu le modèle, ou usurpé son identité, de l'effectuer. Cela paraît strict parce que ça l'est. C'est aussi l'hypothèse qui résiste à l'injection de prompt, aux clients défectueux, aux configurations copiées et au développeur qui teste une requête avec un script JSON-RPC brut.
Le serveur doit s'arrêter avant l'autorité réutilisable
Un serveur MCP doit s'arrêter avant d'avoir besoin de révéler ou de gérer un identifiant réutilisable. Ses bons usages consistent notamment à rechercher dans un dépôt indexé, analyser une sortie de compilation, mettre en forme une charge utile, lire un fichier de projet délibérément exposé et produire une commande à soumettre à approbation. Ces tâches peuvent encore causer des dommages si elles sont mal écrites, mais elles n'exigent pas de secret qui resterait utile après la fin de la session.
Les actions externes ont besoin d'un autre responsable. Une requête API authentifiée, une connexion SSH, une publication de paquet, une requête en production ou une mise à jour d'incident associe une entrée non fiable à une identité qui peut avoir des conséquences. Placez l'identifiant dans un composant qui exécute lui-même la requête, puis renvoyez un résultat limité au serveur MCP ou à l'agent.
Cette distinction est souvent brouillée parce que les deux composants peuvent être des exécutables locaux. Ils ne sont pourtant pas interchangeables :
- Un serveur MCP transforme une requête de l'agent en opération limitée, ou en demande d'opération.
- Une passerelle d'actions détient l'identifiant, décide si ce processus peut l'utiliser, exécute l'appel externe et consigne ce qui s'est passé.
- L'agent reçoit le résultat, pas le moyen de répéter l'action authentifiée en dehors de cette passerelle.
N'envoyez pas API_TOKEN=... comme résultat d'un outil. N'envoyez pas une référence de coffre en prétendant que cela suffit à assurer la sécurité. N'exposez pas une commande qui affiche une clé privée sur la sortie standard en espérant que le modèle détournera le regard. Dès que le client obtient un secret, tous les contrôles ultérieurs deviennent consultatifs.
Une passerelle ne doit pas non plus devenir une API shell universelle. run(command) est un raccourci tentant parce qu'il évite de concevoir des outils. Il confie aussi l'analyse des arguments, l'accès aux fichiers, les destinations réseau et souvent l'accès aux secrets à une seule chaîne opaque. Concevez plutôt des actions précises : get_deployment_status, create_issue, run_readonly_query ou ssh_exec avec un hôte nommé et une famille de commandes limitée. Des actions étroites rendent la validation et la revue possibles.
Les schémas d'outils décrivent les appels, mais ne les confinent pas
Un schéma JSON pour un outil est utile pour valider les entrées, mais ce n'est pas une autorisation. La spécification MCP exige que les outils publient des schémas d'entrée, que les clients peuvent utiliser pour former les appels. Un modèle peut toujours choisir toute valeur valide selon le schéma. Plus grave encore, des implémentations négligentes acceptent une chaîne conforme au schéma, puis l'insèrent dans une commande shell ou une URL où sa signification change.
Prenons un outil destiné à récupérer l'état d'un déploiement :
{
"name": "deployment_status",
"inputSchema": {
"type": "object",
"properties": {
"environment": {"enum": ["staging", "production"]},
"service": {"type": "string", "pattern": "^[a-z0-9-]{1,48}$"}
},
"required": ["environment", "service"],
"additionalProperties": false
}
}
Ce schéma empêche l'ajout d'un champ de premier niveau inattendu et rejette les signes de ponctuation shell évidents dans service. Il n'autorise pas l'appelant à consulter la production, ne prouve pas que service appartient au dépôt actuel et ne limite pas la destination HTTP une fois que le serveur a construit une URL. Un validateur de schéma répond à la question « La forme est-elle correcte ? » L'autorisation répond à la question « Cet appelant peut-il effectuer cette action maintenant avec cette identité ? » Gardez ces questions séparées dans le code comme dans la revue.
Une mauvaise implémentation ressemble souvent à ceci :
subprocess.run(
f"ssh {host} systemctl status {service}",
shell=True,
check=True,
)
Même si host et service ont passé un schéma peu strict, l'analyse shell crée un autre langage avec une autre surface d'attaque. Utilisez des vecteurs d'arguments, refusez les hôtes inconnus avant d'ouvrir une connexion et faites choisir l'identifiant par la passerelle à partir d'un identifiant fixe, plutôt que d'accepter un chemin ou un nom de jeton fourni par l'agent.
Pour HTTP, analysez l'URL avant la connexion, exigez https, comparez le nom d'hôte normalisé à une liste exacte d'hôtes approuvés et désactivez les redirections ou validez-les à nouveau. Une redirection depuis un hôte autorisé vers une adresse interne ou un point de terminaison contrôlé par un attaquant peut transformer une requête apparemment inoffensive en divulgation d'identifiants. Ne vous fiez pas à un contrôle de préfixe comme url.startswith("https://api.example.com") : les informations utilisateur, les ports et les noms d'hôte ressemblants rendent les vérifications textuelles peu fiables.
L'identité du processus doit être visible au moment de l'approbation
Un bouton d'approbation humaine n'est utile que s'il indique qui demande l'accès et quelle autorité cette approbation accorde. « Autoriser l'accès de l'agent » est une formulation faible, car elle cache l'exécutable qui recevra la permission et la durée de cette permission. Elle habitue les utilisateurs à approuver une catégorie d'activité vague.
Une meilleure conception identifie le processus appelant par son autorité de signature, sa relation avec le processus parent, son chemin d'exécutable et sa durée de vie. La personne peut alors approuver une exécution d'un client connu au lieu de bénir définitivement une étiquette. Lorsque le processus se termine, son approbation doit prendre fin. Un nouveau processus nécessite une nouvelle décision.
La signature du code ne prouve pas que chaque prompt ou extension du client est inoffensif. Elle répond à une question plus étroite, mais toujours utile : quel exécutable signé a demandé cette autorité ? Cette distinction compte lorsqu'un programme local malveillant ou modifié tente de reprendre un nom rassurant. Sur macOS, le système d'exploitation fournit les informations de signature que la passerelle peut afficher avant d'autoriser une action.
Sallyport utilise cette identité de processus pour l'autorisation par session, tandis que son coffre verrouillé refuse toute action jusqu'à ce que l'utilisateur l'ouvre au moyen des contrôles matériels de sécurité du Mac. Ce modèle reste volontairement limité : un coffre verrouillé, une approbation pour chaque nouveau processus et, en option, une approbation pour chaque utilisation d'un identifiant donné.
N'essayez pas de résoudre la lassitude liée aux approbations avec une politique complexe en langage naturel. Les utilisateurs ne peuvent pas évaluer de manière fiable un ensemble de règles dense après l'ajout de dizaines d'exceptions. Préférez quelques décisions correspondant à des éléments visibles pour un développeur : les secrets sont-ils disponibles, quel processus peut agir pour cette exécution et quels identifiants nécessitent une confirmation à chaque utilisation.
La portée de l'approbation doit suivre les dommages possibles
Une approbation par session convient aux tâches répétitives et peu risquées, comme la lecture d'un outil de suivi des incidents ou la vérification de l'état d'un service de développement. Elle devient dangereuse lorsque cette même approbation couvre silencieusement des modifications destructrices de base de données, la publication de paquets, des mouvements d'argent, des communications avec des clients ou un accès SSH à la production.
Donnez à chaque identifiant son propre niveau de sensibilité pour les approbations. Un jeton en lecture seule peut fonctionner après l'approbation du processus pour la session. Un jeton d'écriture en production ou une clé SSH doit demander une confirmation à chaque utilisation. La passerelle doit afficher suffisamment de contexte pour qu'une personne puisse évaluer l'action : identité de l'identifiant, hôte ou service cible, méthode ou classe de commande et arguments nettoyés. N'affichez jamais le secret lui-même.
Une approbation doit autoriser une requête concrète, pas une promesse selon laquelle l'agent se comportera correctement plus tard. Si un appel d'outil indique POST /releases, la vue d'approbation ne doit pas le résumer en « utiliser l'API des versions ». La méthode, la destination finale et le nom de l'opération sont les faits qui distinguent une lecture inoffensive d'une écriture irréversible.
L'alternative courante consiste à utiliser une liste d'autorisation large : approuver un domaine, un binaire shell ou un agent pour toute la journée de travail. Cela semble efficace jusqu'à ce qu'une instruction injectée oriente cette même capacité vers un autre dépôt, un autre point de terminaison ou d'autres arguments. Les autorisations larges réduisent les interruptions en reportant la charge de la revue au moment où personne ne peut voir l'appel réel.
Utilisez largement les expirations. Une autorisation de session doit disparaître avec le processus client. Une décision par appel doit expirer après cette opération. Si une passerelle doit plus tard prendre en charge des autorisations plus longues, indiquez clairement leur portée et leur expiration au lieu de laisser une approbation mise en cache se faire passer pour une confiance permanente.
SSH a besoin de sa propre frontière, pas d'une échappatoire shell
C'est avec SSH que les conceptions d'agents locaux perdent souvent toute rigueur. Les développeurs disposent déjà d'un agent SSH, d'alias d'hôtes, de clés transférées et de l'habitude de saisir des commandes arbitraires dans un terminal. La tentation est donc de laisser le serveur MCP appeler ssh avec l'environnement existant de l'utilisateur. L'agent devient alors appelant de chaque identité et de chaque règle d'hôte accessible depuis le shell.
OpenSSH documente une limite sérieuse du transfert de l'agent : un utilisateur distant qui peut accéder au socket de l'agent transféré peut demander des opérations à votre agent local, même s'il ne peut pas extraire les clés privées. Cela suffit pour agir en votre nom pendant la durée du transfert. Un agent autonome ne doit pas emprunter ce chemin sans précaution, car il élargit l'autorité au-delà de l'hôte d'origine.
Utilisez une identité SSH dédiée au travail de l'agent et associez-la à une fiche d'hôte nommée. Sur le serveur, limitez cette identité avec les options adaptées au compte, comme une commande forcée et la désactivation du transfert lorsque le cas d'utilisation le permet. Côté local, sélectionnez l'hôte et l'identité depuis une configuration hors du contrôle de l'agent. L'agent peut demander host: build-staging et une action de commande limitée, mais il ne doit pas soumettre un nom d'hôte arbitraire, un chemin de clé privée ou -o ProxyCommand=....
Voici la forme minimale d'une requête qu'une passerelle d'actions peut valider :
{
"action": "ssh_exec",
"host_id": "build-staging",
"command_id": "read_service_status",
"args": {"service": "worker"}
}
La passerelle associe build-staging à son hôte connu, à sa politique de clé d'hôte, à son compte et à son identifiant dédié. Elle associe read_service_status à un vecteur d'arguments fixe. Elle ne concatène pas cette charge utile dans une chaîne shell. Une requête refusée doit indiquer pourquoi dans un enregistrement d'audit, sans afficher de secrets ni de données potentiellement hostiles dans un terminal.
Si vous avez besoin d'un diagnostic distant arbitraire, faites-en une opération séparée, très encadrée, avec une confirmation par appel et des limites de sortie évidentes. Ne dissimulez pas un accès shell arbitraire sous un nom d'outil rassurant comme check_server.
Les enregistrements d'audit doivent survivre à l'acteur qui les a provoqués
Un journal texte écrit par le même processus que celui qui réalise l'action ne constitue une preuve que jusqu'au moment où ce processus souhaite le modifier. L'activité d'un agent nécessite un enregistrement qui permet de reconstituer l'exécution et chaque appel externe, puis de détecter toute suppression ou modification ultérieure.
Consignez l'identité du processus, l'identifiant de session, l'heure, le type d'action, l'identifiant de l'accès, la cible approuvée, la forme nettoyée de la requête, le résultat de l'approbation, le statut de la réponse et la catégorie d'erreur. Séparez le journal de session du journal des actions. La vue de session répond à « Quelle exécution d'agent disposait de l'autorité ? » La vue des actions répond à « Qu'a-t-elle fait avec cette autorité ? » Ne forcez pas l'enquêteur à déduire l'un à partir d'un flux de lignes unique.
Une chaîne de hachage fournit un contrôle d'intégrité pratique. Pour chaque enregistrement, calculez un condensat à partir du condensat de l'enregistrement précédent et des octets canoniques du nouvel enregistrement chiffré. Stockez le nouveau condensat avec l'enregistrement. Un vérificateur peut ainsi détecter une modification, une suppression au milieu ou une réorganisation de la séquence sans avoir besoin du texte en clair.
Le vérificateur d'audit doit fonctionner indépendamment de l'agent et ne doit pas avoir accès aux identifiants. L'interface de commande peut être aussi simple que ceci :
$ sp audit verify
records: 184
first sequence: 1
last sequence: 184
chain: valid
Cette forme de sortie donne à l'opérateur des informations précises à joindre à un ticket ou à un rapport d'incident. Si la vérification échoue, la commande doit signaler la première séquence où la continuité a été rompue et retourner un code de sortie différent de zéro. « Journal illisible » est trop vague pour mener une enquête.
La preuve d'altération n'est pas la prévention de l'altération. Un utilisateur local disposant de suffisamment de droits peut encore supprimer l'intégralité du journal ou restaurer une version antérieure de son stockage. Gardez cette limite visible. Si les enjeux exigent une preuve contre un retour en arrière local, exportez des points de contrôle signés vers un système contrôlé distinct. Ne prétendez pas qu'une chaîne de hachage locale résout une menace à laquelle elle ne répond pas.
Gardez les secrets hors des variables d'environnement et des résultats d'outils
Les variables d'environnement sont pratiques pour une session shell humaine, mais offrent un confinement médiocre aux agents autonomes. Un processus enfant les hérite par défaut. Les journaux de débogage peuvent les afficher. Une commande qui liste l'environnement peut les renvoyer au modèle. Les rapports d'incident, l'inspection des processus, les paquets de support et les transcriptions de terminal copiées ont tous exposé des secrets de cette manière.
Un fichier d'identifiants dans l'espace de travail est encore pire. Le modèle peut le lire, un outil peut le téléverser, une commande Git peut l'ajouter à l'index et un service d'indexation peut en conserver une copie. Déplacer le fichier dans un répertoire caché réduit la fréquence des accidents, mais ne change pas la frontière de sécurité.
Conservez les identifiants dans un coffre contrôlé par la passerelle d'actions. La passerelle sélectionne l'identifiant à partir d'une association d'actions fixe et ne l'injecte que dans sa propre opération HTTP ou SSH. Elle ne renvoie le corps de la réponse qu'après filtrage, et seulement lorsqu'il est sûr de l'exposer à l'agent. Un jeton bearer ne doit jamais passer par le résultat MCP, même sous forme masquée, car les erreurs de masquage deviennent une partie permanente de la transcription.
Pour HTTP, préférez un contrat de réponse plutôt qu'un simple relais. Une action d'état de déploiement pourrait renvoyer ceci :
{
"environment": "staging",
"service": "worker",
"state": "healthy",
"revision": "a1b2c3d4"
}
Elle ne doit pas renvoyer d'en-têtes susceptibles de contenir des identifiants de session, des détails de routage interne ou un nouveau jeton. Déterminez les champs dont l'agent a besoin avant l'implémentation. Un relais de réponse brut est un autre raccourci dont il devient coûteux de se défaire.
Une petite frontière est plus facile à gérer sous pression
Vous pouvez examiner l'intégration d'un agent local sans langage de politique ni vaste programme de sécurité. Commencez par l'inventaire des actions, puis forcez chaque action à entrer dans l'une de deux catégories : elle lit ou calcule du contexte local sans autorité réutilisable, ou elle atteint un service externe et nécessite une passerelle.
Pour chaque action externe, notez l'identité fixe de la cible, l'identifiant sélectionné par la passerelle, les arguments exacts que l'agent peut influencer, la portée de l'approbation et l'enregistrement d'audit produit. Si une ligne indique « commande arbitraire », « toute URL », « jeton provenant de l'environnement » ou « l'agent choisit l'identifiant », la frontière n'est pas terminée.
Exécutez cet exercice de défaillance avant de donner à l'agent un secret utile :
- Envoyez une requête brute conforme au schéma qui désigne une cible inattendue ou un argument trop volumineux.
- Rejouez une requête approuvée précédemment après la fermeture du processus client.
- Tentez une requête HTTP redirigée et une requête SSH avec des options de transfert.
- Verrouillez le coffre, puis vérifiez que chaque action échoue avant l'ouverture de toute connexion réseau.
- Modifiez un enregistrement d'audit stocké et vérifiez que la commande d'audit détecte la rupture de la chaîne.
Ces tests repèrent les erreurs de conception qu'une démonstration agréable dissimule. Un modèle qui suit parfaitement les instructions n'est pas un test de sécurité.
L'architecture locale la plus claire garde le serveur MCP ordinaire et remplaçable. Laissez-le exposer du contexte utile et demander des opérations conçues avec précision. Placez les secrets, l'approbation tenant compte du processus, l'exécution et un enregistrement auditable derrière la frontière d'actions. Lorsque quelqu'un demande pourquoi un outil ne peut pas simplement recevoir le jeton de production, la réponse doit être visible dans la conception : l'outil n'a jamais eu besoin du jeton pour faire son travail.
FAQ
MCP stdio est-il sécurisé parce qu'il fonctionne localement ?
Non. stdio fournit à un processus local un flux d'octets, pas une frontière de sécurité. Le client lance le serveur, peut envoyer toute requête MCP valide et dispose généralement des mêmes accès utilisateur que le processus du serveur.
Que doit être autorisé à faire un serveur MCP ?
Utilisez un serveur MCP pour le contexte local, les transformations déterministes et les capacités limitées qui n'ont pas besoin d'identifiants privilégiés. Placez les requêtes HTTP authentifiées, les connexions SSH, les paiements, les déploiements et autres effets externes derrière une passerelle d'actions distincte.
Un agent IA doit-il un jour recevoir une clé API via MCP ?
L'agent doit recevoir uniquement le résultat nécessaire à sa tâche, comme un état, certains champs ou la sortie d'une commande. Il ne doit jamais recevoir de jeton réutilisable, de clé privée, d'emplacement d'identifiant ou de fichier de configuration qui lui permettrait d'en obtenir un plus tard.
Les descriptions des outils MCP constituent-elles une politique de sécurité ?
Non. La description d'un outil aide le modèle à choisir un outil, mais elle ne limite pas les arguments malveillants ou erronés. L'exécutable doit valider les arguments et appliquer la véritable frontière d'autorité.
Quand dois-je demander une approbation pour chaque action de l'agent ?
Une approbation au niveau du processus est utile lorsqu'elle identifie l'exécutable qui va agir et expire à la fermeture de ce processus. Elle est trop large pour les identifiants qui peuvent causer des dommages coûteux ou irréversibles. Dans ce cas, chaque utilisation doit faire l'objet d'une décision distincte.
Comment empêcher l'injection de prompt de modifier un appel d'outil ?
Les arguments qui désignent un hôte, une URL, un chemin, une branche, un destinataire ou un compte sont des entrées de sécurité. Validez-les selon des contraintes explicites, résolvez les chemins avant de les vérifier, refusez les redirections qui quittent les hôtes approuvés et consignez la destination finale.
Pourquoi les variables d'environnement sont-elles un mauvais endroit pour les identifiants d'un agent ?
Les variables d'environnement peuvent facilement fuiter par les processus enfants, les diagnostics, l'historique du shell, les rapports d'incident et les sorties de commandes accidentelles. Un courtier local d'identifiants réduit cette exposition en conservant le secret dans son propre processus et en exécutant lui-même l'opération authentifiée.
Que doit contenir un journal d'audit des actions d'un agent IA ?
Un enregistrement utile relie chaque requête au processus appelant, au nom de l'outil, à l'autorité approuvée, aux arguments nettoyés, à l'état du résultat et à l'heure. Un journal append-only inviolable est plus fiable qu'un fichier texte que le même utilisateur ou processus peut modifier.
Le transfert de l'agent SSH est-il sûr pour les agents de programmation ?
Non. Le transfert SSH et le transfert de l'agent peuvent permettre à un hôte distant de demander des signatures par l'intermédiaire de votre agent local. Utilisez une clé dédiée avec des restrictions précises côté serveur, ou faites gérer l'opération SSH par la passerelle et demandez une approbation au moment de l'utilisation.
Ai-je besoin d'une passerelle d'actions pour chaque outil d'IA local ?
Une passerelle locale est particulièrement utile lorsque les agents ont besoin d'une véritable autorité externe sans devoir détenir de secrets réutilisables. Elle n'est pas nécessaire pour un outil de formatage en lecture seule ou de recherche dans un dépôt qui n'a aucun identifiant et ne peut pas atteindre de services externes.