La sécurité de SSH ProxyCommand est-elle plus faible qu’elle n’en a l’air ?
La sécurité de SSH ProxyCommand exige d’examiner l’exécution du shell local, les expansions, les inclusions et le routage via hôtes de rebond avant qu’un agent n’atteigne un hôte distant.

SSH ProxyCommand correspond à une exécution de code locale qui a lieu avant la commande distante que vous vouliez lancer. Cela paraît évident une fois dit, pourtant les revues le traitent souvent comme un détail de transport : l’agent demande ssh host uptime, la personne qui vérifie contrôle l’hôte et la commande distante, puis le client lance discrètement une commande shell locale que personne n’a intégrée à la décision d’approbation.
Cet écart compte davantage lorsqu’un agent autonome de programmation peut invoquer SSH. La destination distante peut être strictement limitée alors que la configuration SSH locale contient des alias, des inclusions, des règles conditionnelles, un assistant proxy et une chaîne d’hôtes de rebond accumulés au fil des années. Une revue sûre commence par l’action effectuée par SSH côté client, puis remonte vers le réseau.
ProxyCommand s’exécute avant que la session distante existe
ProxyCommand indique au client SSH de lancer une commande locale et d’utiliser son entrée et sa sortie standard comme transport vers le serveur SSH. La commande ne s’exécute pas sur la destination. Elle s’exécute sur la machine qui a lancé ssh, avec le compte local, avant que SSH puisse s’authentifier auprès du serveur visé.
Une entrée typique semble inoffensive :
Host build-private
HostName build.internal.example
ProxyCommand /usr/local/bin/connect-private %h %p
Lorsqu’une personne exécute ssh build-private, SSH développe %h et %p, puis lance /usr/local/bin/connect-private build.internal.example 22. L’assistant peut ouvrir un socket, appeler un autre client, lire un fichier de jeton, charger une bibliothèque, consulter des variables d’environnement ou exécuter un script shell. À partir de là, le client SSH ne voit qu’un flux d’octets. Votre demande d’approbation peut décrire une connexion SSH alors que la première action importante a été le lancement d’un programme local.
Le manuel OpenSSH ssh_config est clair sur cette frontière : ProxyCommand indique la commande utilisée pour se connecter au serveur et précise qu’elle est exécutée avec le shell de l’utilisateur. Cette dernière précision change la revue. Une ligne de commande n’est pas un tableau d’arguments. L’analyse du shell, les expansions, les redirections, les substitutions de commandes et l’environnement de démarrage du shell font partie de l’action.
Ne minimisez pas ce risque sous prétexte que l’assistant se trouve dans un dépôt de fichiers de configuration personnels. Une configuration revue il y a six mois peut appeler un binaire trouvé via PATH, charger un fichier dans un répertoire accessible en écriture ou dépendre d’un alias qui se résout désormais autrement. La cible de la revue est le chemin d’exécution résolu sur la machine qui l’exécutera.
Un alias d’hôte peut sélectionner bien plus qu’un hôte
La configuration SSH est un système de correspondance, pas un simple dictionnaire. Un alias court peut déclencher des paramètres issus de la configuration système, de la configuration utilisateur, de fragments inclus, d’hôtes génériques, de la canonicalisation et de blocs conditionnels. La commande finale peut venir d’un fichier que personne n’associe à l’alias.
Commencez par recenser les configurations que SSH lit. Sur un client OpenSSH courant, cela comprend /etc/ssh/ssh_config, les fichiers atteints via Include et ~/.ssh/config. Le paquet de la distribution et les options de ligne de commande peuvent modifier cette liste, examinez donc aussi l’invocation. Un appel qui fournit -F /some/file modifie toute la surface à examiner.
Utilisez ssh -G pour afficher la configuration résolue d’une destination précise. Pour un alias de test contrôlé, cela produit un artefact utile :
ssh -G build-private | grep -E '^(hostname|user|port|proxycommand|proxyjump|identityfile) '
La sortie a cette forme :
hostname build.internal.example
user deploy
port 22
proxycommand /usr/local/bin/connect-private %h %p
identityfile ~/.ssh/id_deploy
ssh -G expose ce que SSH a sélectionné et révèle un nombre étonnant d’erreurs : une entrée générale Host *, une ancienne inclusion ou un alias qui correspond à un compte différent de celui attendu. Il ne prouve pas que l’assistant est sûr. Il ne montre pas toujours non plus, d’une façon compréhensible pour une personne qui révise, toutes les conditions qui ont mené à ce résultat. Remontez donc chaque directive pertinente jusqu’à son fichier source.
Considérez l’entrée d’hôte comme des données seulement si la configuration la conserve comme telle. Si un agent peut fournir des alias arbitraires, il peut alors choisir n’importe quelle strophe Host correspondante que le compte local peut lire. S’il peut fournir des options -o arbitraires ou un chemin de configuration, il peut souvent contourner entièrement la revue des alias. Un enveloppeur qui accepte une commande SSH en texte libre n’est pas une frontière utile.
Une meilleure interface accepte un identifiant de destination issu d’une petite liste autorisée et le mappe vers un compte, un nom d’hôte, un port et une méthode de connexion fixes. L’identifiant peut être production-readonly ; les arguments SSH sous-jacents ne doivent pas venir d’une chaîne générée par un agent.
L’expansion du shell transforme la configuration en frontière d’entrée
Comme SSH transmet ProxyCommand à un shell, les guillemets dans la configuration n’offrent pas la même protection que des arguments de processus structurés. La syntaxe shell survit jusqu’à ce que le shell la traite. Cela inclut les espaces, les caractères génériques, les références à des variables, les redirections et les substitutions de commandes lorsqu’ils apparaissent dans le texte de la commande.
Le modèle risqué est celui d’un assistant qui construit sa propre commande shell à partir des valeurs qu’il reçoit. Examinez cette configuration :
Host *
ProxyCommand sh -c 'relay --target %h --port %p --token \"$RELAY_TOKEN\"'
Elle soulève plusieurs questions distinctes. Chaque nom d’hôte résolu possible respecte-t-il uniquement la syntaxe attendue pour un nom d’hôte ? relay interprète-t-il --target comme une seule valeur ? Un environnement contrôlé par l’utilisateur peut-il définir RELAY_TOKEN ou modifier le chemin de recherche ? Le shell externe reçoit-il une chaîne de commande contenant des caractères qui changent de sens avant le démarrage de relay ? Dire que le nom d’hôte vient de SSH ne répond à aucune de ces questions.
Les jetons en pourcentage ne forment pas un système de modèles sûr. OpenSSH développe des jetons comme %h, %p, %r et %n dans de nombreuses options. %n correspond à l’argument d’hôte d’origine, tandis que %h est le nom d’hôte que SSH utilisera après le traitement de la configuration. Cette différence est facile à manquer. Une commande proxy qui utilise %n peut recevoir un alias convivial ou un texte demandé arbitraire là où son auteur attendait un nom DNS canonique. Une commande qui utilise %h peut toujours recevoir une valeur modifiée par HostName ou par la canonicalisation.
La correction est généralement moins ingénieuse que la configuration d’origine. Utilisez un petit assistant dédié avec un chemin d’exécutable fixe, donnez-lui une syntaxe limitée et faites-lui rejeter tout ce qui sort de cette syntaxe. Demandez à l’assistant d’appeler des API de connexion ou un lanceur de processus par tableau d’arguments plutôt que de construire une autre ligne shell. Si un script shell est inévitable, validez les entrées avant interpolation, protégez chaque expansion pour ce shell et gardez l’ensemble accepté assez réduit pour qu’un auditeur puisse le tester.
Ne remplacez pas l’analyse par eval. Les équipes y recourent parce qu’il donne de la souplesse à un enveloppeur piloté par configuration. Une seule paire de guillemets oubliée devient alors un défaut d’exécution locale. La souplesse doit se trouver dans un format de configuration vérifié, pas dans un shell qui analyse à nouveau du texte.
ProxyJump élimine un shell, pas le problème de confiance
ProxyJump, souvent écrit -J ou ProxyJump, demande à SSH d’atteindre la destination via un ou plusieurs hôtes de rebond SSH. Pour le routage habituel via un bastion, préférez-le à un ProxyCommand écrit à la main qui lance simplement une autre commande ssh -W. Il décrit la topologie prévue et évite de placer cette topologie dans une commande shell personnalisée.
Par exemple :
Host build-private
HostName build.internal.example
User deploy
ProxyJump bastion-admin
Host bastion-admin
HostName bastion.example
User relay
Cette configuration est plus facile à examiner qu’une chaîne remplie de guillemets imbriqués. Elle exige toujours une revue attentive. Le client s’authentifie auprès de l’hôte de rebond, l’hôte de rebond participe à l’itinéraire et son nom, son compte, sa clé d’hôte, ses exigences MFA, ses règles de transfert et sa connectivité réseau influencent l’action. Un hôte de rebond compromis ou mal configuré peut exposer les schémas de trafic et rediriger une tentative de connexion. La vérification des clés d’hôte reste obligatoire à chaque saut SSH.
OpenSSH autorise les deux paramètres, mais ProxyCommand est prioritaire sur ProxyJump lorsque les deux s’appliquent. C’est une mauvaise surprise de configuration. Une personne qui révise peut voir un ProxyJump propre dans un bloc propre à l’hôte alors qu’une règle générale correspondante, plus tôt ou plus tard, fournit une commande proxy. Vérifiez le résultat effectif avec ssh -G au lieu de le déduire d’une seule strophe.
Les sauts multiples demandent aussi une raison explicite. Ne considérez pas une chaîne comme une sécurité supplémentaire par défaut. Chaque hôte ajouté implique une nouvelle utilisation d’identifiants, une décision sur la clé d’hôte et un emplacement d’audit. N’utilisez une chaîne que lorsque le chemin réseau l’exige et consignez le compte attendu à chaque saut.
Include et Match exec peuvent exécuter une logique locale lors de la sélection
Include rend la configuration SSH composable, ce qui est utile jusqu’à ce qu’un dépôt, un outil de gestion de configuration ou un installeur dépose un fragment dans un répertoire correspondant largement. OpenSSH développe les motifs génériques dans Include : un fichier inattendu peut donc modifier les paramètres d’un hôte sans toucher à la configuration principale. Vérifiez la propriété et les droits d’écriture des répertoires inclus, pas seulement leur contenu actuel.
Match ajoute des conditions. Match host, user, localnetwork et des conditions associées modifient les options qui s’appliquent. Match exec mérite un avertissement plus visible : SSH exécute la commande locale indiquée et utilise son code de sortie pour décider si le bloc correspond. La lecture de la configuration peut donc invoquer un exécutable local.
Cet exemple rend la frontière visible :
Match exec \"/usr/local/bin/on-corporate-network\"
ProxyJump corp-bastion
La condition peut être raisonnable si l’assistant est un programme fixe appartenant à root et donnant un résultat simple. Elle devient fragile s’il appelle un shell, lit un fichier de projet accessible en écriture, contacte un service réseau ou dépend de PATH. Ne testez pas des règles Match exec inconnues sur un poste de travail rempli d’identifiants actifs sous prétexte que vous prévoyez d’exécuter ssh -G. Examinez d’abord la commande et utilisez si nécessaire un compte ou une machine isolés.
Le manuel OpenSSH documente aussi l’expansion de jetons dans les options de correspondance et liées aux commandes. Une condition Match exec peut donc dépendre du texte d’hôte ou d’utilisateur demandé. Gardez les valeurs sélectionnées par l’agent hors de ces conditions. Si la condition a besoin de données contextuelles, dérivez-les d’un fichier d’état local fiable aux droits stricts, pas d’une chaîne ayant franchi la frontière de l’agent.
L’incident commence généralement par un assistant pratique
Une défaillance réaliste n’a pas besoin d’une configuration visiblement malveillante. Imaginez qu’un développeur ajoute ceci il y a des années pour accéder à un réseau de test privé :
Host test-*
ProxyCommand connect-testnet %h %p
connect-testnet a autrefois été installé par un script d’amorçage dans un répertoire personnel de binaires. Le script appelle un client VPN d’entreprise, puis lance une commande de relais trouvée dans PATH. Plus tard, un agent de compilation s’exécute sous le compte du développeur. Il peut demander ssh test-cache. L’action attendue est une commande en lecture seule sur une machine de test.
D’abord, SSH fait correspondre test-*. Ensuite, le shell local démarre connect-testnet. Puis le script démarre le client VPN et l’exécutable qui l’emporte actuellement lors de la recherche dans PATH. Ce n’est qu’après ces étapes locales que SSH commence son protocole avec l’hôte nommé. Un audit qui n’enregistre que la commande distante perd le comportement qui a modifié l’état de la machine locale et sélectionné l’itinéraire réseau.
Le défaut ne vient pas du fait que les scripts shell seraient interdits. Il vient du traitement d’une chaîne locale pratique comme si elle faisait partie du service distant. La capacité de l’agent à choisir test-cache a sélectionné du code local. Un agent malveillant pourrait chercher des alias correspondant à des motifs plus larges, mais un agent ordinaire peut déclencher le même problème en devinant un nom qui existe par hasard.
Corrigez le chemin réel. Remplacez le caractère générique par des destinations nommées lorsque c’est possible. Placez l’assistant de relais à un chemin absolu. Retirez les recherches dans PATH à l’intérieur. Faites en sorte que l’assistant n’accepte que les valeurs d’hôte et de port attendues. Sortez la gestion de la connexion VPN d’une commande proxy par requête si elle n’a pas besoin de s’exécuter pour chaque action SSH. Testez ensuite la configuration finale avec le compte et l’environnement que l’agent utilisera.
Approuvez la méthode de connexion, pas seulement la destination
Un flux d’approbation doit afficher assez d’informations pour qu’une personne reconnaisse l’action. ssh deploy@build-private ne donne pas assez de contexte lorsque l’alias lance un assistant proxy ou passe par un bastion. La personne qui révise a besoin de la destination résolue, du compte, du port, de la méthode proxy et du chemin de rebond. Sinon, elle approuve une étiquette en espérant que la configuration locale signifie toujours la même chose que la semaine dernière.
Séparez trois décisions que les équipes confondent souvent. Premièrement, cet agent peut-il traiter une requête SSH quelconque ? Deuxièmement, peut-il utiliser cette identité SSH précise ? Troisièmement, cette destination peut-elle employer cette méthode de connexion locale particulière ? Un oui aux deux premières questions ne répond pas automatiquement à la troisième. L’assistant proxy peut atteindre une zone de confiance différente ou provoquer des effets locaux que la connexion directe ne produirait pas.
Sallyport garde les identifiants SSH hors de l’agent et utilise son assistant sp-ssh intégré pour les actions SSH, mais cela ne transforme pas une configuration SSH locale en surface de politique sûre. Limitez l’interface de destination de l’agent et examinez toute configuration client ou tout processus assistant qui intervient avant le début de l’action avec identifiants.
Pour les chemins sensibles, exigez une approbation à chaque utilisation de l’identité SSH et incluez la destination ainsi que l’itinéraire dans la description d’approbation. Cela détecte un alias modifié, un bastion inattendu ou une tentative d’utilisation hors du chemin de maintenance habituel. Cela ne rendra pas utile une carte d’approbation vague. Les informations doivent y figurer.
Testez le chemin résolu avec des identifiants jetables
Une revue de configuration nécessite un test d’exécution, mais réalisez-le avec des identifiants qui ne peuvent pas endommager la production. Utilisez un hôte de test, un compte temporaire et un environnement contrôlé. Capturez l’arbre des processus côté client et les destinations réseau si votre système d’exploitation le permet. L’objectif est de confirmer quel exécutable se lance, quels arguments il reçoit et s’il crée des connexions au-delà de l’itinéraire attendu.
Une courte séquence de revue suffit pour la plupart des alias d’hôtes :
- Exécutez
ssh -G aliaset enregistrez les paramètres résolushostname,user,port,proxycommand,proxyjumpet d’identité. - Suivez chaque
Includeet chaque blocHostouMatchcorrespondant qui a fourni ces valeurs. - Lisez chaque exécutable local du chemin proxy ou match, y compris les scripts et leurs fichiers de configuration.
- Exécutez la connexion avec des identifiants jetables et vérifiez les véritables hôtes de rebond, les clés d’hôte et les processus locaux.
- Révoquez l’identifiant de test après le test et consignez l’itinéraire attendu à côté de l’alias.
Ne vous fiez pas uniquement à une connexion réussie. Le succès prouve que des octets ont atteint un serveur SSH. Il ne prouve pas que le bon programme les a transportés, que l’hôte prévu les a reçus ou qu’aucun effet local ne s’est produit auparavant.
Un peu de friction est justifiée ici. Si personne ne peut expliquer l’exécutable derrière un ProxyCommand, personne ne devrait autoriser un agent à invoquer l’alias. Remplacez-le, supprimez-le ou gardez cet itinéraire hors de portée de l’agent jusqu’à ce que quelqu’un en assume la responsabilité.
Une petite surface SSH vaut mieux qu’une configuration client ingénieuse
La configuration SSH la plus sûre face à un agent comporte peu de destinations nommées, des identités fixes, des hôtes de rebond explicites et aucun indicateur de configuration arbitraire. Elle n’a pas besoin d’un langage de politique généraliste. Elle a besoin d’une interface limitée et d’une configuration qui reste banale lors de l’examen.
Revoyez-la quand l’un de ces éléments change : une mise à jour du client SSH, un nouveau script d’amorçage, un déploiement de gestion de configuration, l’ajout d’un répertoire d’inclusion, un nouvel environnement d’exécution d’agent ou un nouvel hôte de rebond. Ces changements arrivent souvent dans des dépôts distincts, ce qui explique précisément pourquoi le chemin de connexion se dégrade sans que personne ne s’en aperçoive.
Gardez la règle finale simple : avant qu’une commande distante ne s’exécute, vous devez pouvoir nommer chaque exécutable local lancé par SSH, chaque hôte contacté, chaque identité utilisée et la personne qui a approuvé cet itinéraire. Si vous ne pouvez pas le faire pour un alias, il n’est pas prêt pour une utilisation autonome.
FAQ
ProxyCommand s’exécute-t-il sur la machine locale ?
Non. SSH démarre ProxyCommand sur le client avant d’avoir un transport vers la destination. Considérez la commande et chaque programme qu’elle appelle comme une exécution locale avec l’identité de la personne ou de l’agent qui lance SSH.
Comment voir le ProxyCommand que SSH utilisera ?
Utilisez ssh -G alias pour examiner le paramètre résolu d’un hôte nommé, puis lisez vous-même chaque fichier de configuration correspondant. Effectuez cette vérification depuis un compte de test contrôlé si la configuration utilise Match exec, car cette condition peut exécuter une commande locale pendant que SSH lit la configuration.
ProxyJump est-il plus sûr que ProxyCommand ?
En général, oui. ProxyJump exprime un saut SSH sans transmettre de commande shell à votre shell local, ce qui réduit les risques liés aux guillemets et aux expansions. Il crée tout de même une frontière de confiance au niveau de l’hôte de rebond et demande la même vérification des clés d’hôte et des comptes.
Un alias d’hôte SSH peut-il provoquer une injection de commandes ?
Oui, lorsque le shell traite du texte contrôlé par un attaquant comme une partie de la ligne de commande ProxyCommand. Les alias d’hôtes, noms d’hôtes canoniques, noms d’utilisateur, numéros de port, variables d’environnement et configurations incluses peuvent influencer la commande finale si vous ne les limitez pas délibérément.
Quel risque de sécurité un hôte de rebond ajoute-t-il à SSH ?
Un hôte de rebond peut observer et relayer le flux d’octets. Ses propres identifiants SSH, sa configuration et ses règles de transfert entrent aussi en jeu. Il ne doit pas devenir discrètement un hôte d’administration général par simple commodité.
Pourquoi Match exec est-il risqué dans ssh_config ?
Match exec permet à SSH d’exécuter une commande locale pour décider si la configuration qui suit s’applique. C’est utile pour des conditions ciblées et fiables, mais la lecture de configuration devient alors un chemin d’exécution de code local et mérite le même examen qu’un script assistant.
Dois-je utiliser des chemins absolus dans ProxyCommand ?
Les chemins absolus empêchent qu’un changement de PATH sélectionne un autre assistant, mais ils ne rendent pas cet assistant sûr. Vérifiez aussi ses autorisations, ses arguments, ses fichiers de configuration et les identités auxquelles il se connecte.
Puis-je laisser un agent IA utiliser SSH en toute sécurité ?
Ne laissez pas l’agent fournir des arguments SSH, des alias d’hôtes ou des chemins de configuration arbitraires. Donnez-lui un petit ensemble de destinations vérifiées et gardez les mécanismes de connexion locaux hors de sa surface d’entrée.
Un coffre d’identifiants SSH rend-il ProxyCommand sûr ?
L’autorisation de l’agent et l’isolation des secrets déterminent qui peut demander une action et qui voit les identifiants. Elles n’examinent pas une configuration SSH locale à la recherche d’expansions shell, de Include ou d’assistants exécutables. L’examen de la configuration reste donc nécessaire.
Quelle est la première chose à auditer dans une configuration client SSH ?
Commencez par recenser les alias d’hôtes auxquels un agent peut accéder et exécutez ssh -G pour chacun dans un environnement contrôlé. Supprimez les entrées ProxyCommand personnalisées que vous ne pouvez pas expliquer, puis remplacez celles qui restent par des assistants vérifiés à chemin absolu ou par ProxyJump lorsque c’est adapté.