# La sécurité de SSH ProxyCommand est-elle plus faible qu’elle n’en a l’air ?

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 :

1. Exécutez `ssh -G alias` et enregistrez les paramètres résolus `hostname`, `user`, `port`, `proxycommand`, `proxyjump` et d’identité.
2. Suivez chaque `Include` et chaque bloc `Host` ou `Match` correspondant qui a fourni ces valeurs.
3. Lisez chaque exécutable local du chemin proxy ou match, y compris les scripts et leurs fichiers de configuration.
4. 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.
5. 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.
