# Fichiers d'initialisation SSH : pourquoi les commandes distantes trompent les agents

Une commande SSH n'est pas automatiquement le programme que vous avez saisi. Avant que la machine distante exécute `git`, `python` ou un script de déploiement, le démon SSH sélectionne un compte, lance le shell de ce compte et lui transmet une chaîne de commande. Les fichiers d'initialisation et les règles du serveur peuvent modifier l'environnement, remplacer la commande ou rendre une exécution interactive alors qu'une autre reste non interactive.

Cela devient plus important lorsqu'un agent annonce le résultat. Un humain qui voit une bannière inattendue, un alias ou une invite colorée peut s'arrêter et vérifier. Un agent peut lire le code de sortie 0 et une ligne de sortie familière comme la preuve que le programme attendu s'est exécuté. J'ai vu cette hypothèse transformer une simple vérification d'état en appel à un script wrapper, et une vérification de déploiement en résultat produit par le mauvais exécutable.

La solution n'est pas de supprimer tous les fichiers de profil. Les utilisateurs ont besoin de shells interactifs pratiques. Il faut plutôt identifier le chemin d'exécution, séparer le confort humain de l'automatisation et définir explicitement le contrat de la commande distante.

## Une commande distante passe d'abord par le shell du compte

OpenSSH n'exécute normalement pas les mots placés après `ssh host` comme un `execve` direct du binaire cible. Le manuel de `sshd` indique qu'après l'authentification, sshd exécute la commande demandée via le shell de l'utilisateur, avec l'option `-c` de ce shell. Le chemin du shell de connexion vient de la base de comptes, pas de la machine locale qui a ouvert la connexion SSH.

Ce détail explique beaucoup de rapports déroutants. Supposons qu'un agent envoie :

```text
ssh deploy@buildbox git -C /srv/app rev-parse HEAD
```

Le compte distant peut utiliser Bash, zsh, fish, un shell restreint ou un wrapper propre au site. Le shell sélectionné reçoit une commande équivalente à `git -C /srv/app rev-parse HEAD`. Il peut d'abord s'initialiser. Il peut aussi recevoir une commande de remplacement de la part de sshd avant d'aller plus loin.

Cela est distinct du shell utilisé localement par l'agent. Un agent peut s'exécuter depuis un processus propre sur un Mac alors que le compte distant `deploy` possède dix ans de personnalisations shell personnelles. Une invite locale propre ne rend pas l'autre extrémité propre.

Ne confondez pas non plus l'analyse des commandes par le shell avec le transport SSH. SSH chiffre la connexion et authentifie le compte. Il ne garantit pas que `python` désigne le binaire attendu, que le PATH est resté inchangé ou qu'un profil n'a pas écrit du texte sur la sortie standard.

La première question pratique n'est donc pas « SSH s'est-il connecté ? » C'est « Quel programme a interprété la commande distante, sous quel compte et avec quelles règles d'initialisation ? »

## Le mode du shell détermine quels fichiers interviennent

Le comportement d'initialisation dépend de la famille du shell et de son mode de lancement. Les expressions employées au quotidien, « un shell SSH » ou « un shell Bash », ne sont pas assez précises pour prédire le comportement.

Pour Bash, le manuel de référence GNU Bash distingue les lancements de connexion, interactifs et non interactifs. Un shell Bash de connexion lit `/etc/profile`, puis le premier fichier personnel lisible parmi `~/.bash_profile`, `~/.bash_login` et `~/.profile`. Un shell interactif qui n'est pas un shell de connexion lit `~/.bashrc`.

Une commande distante normale s'exécute généralement dans un shell non interactif. Cela ne signifie pas qu'elle ne charge rien. Bash lit le fichier indiqué par `BASH_ENV`, si cette variable est définie, avant d'exécuter une commande non interactive. Le manuel de Bash décrit également un traitement particulier lorsque Bash détecte que sshd l'a lancé avec une entrée standard reliée à une connexion réseau : il peut alors lire `~/.bashrc`. Ne fondez pas votre automatisation sur la règle simpliste selon laquelle `.bashrc` ne concerne que l'utilisation interactive.

zsh possède une carte différente. `/etc/zshenv` et `~/.zshenv` s'appliquent à chaque lancement de zsh, ce qui rend un `.zshenv` bruyant ou chargé d'effets de bord particulièrement dangereux. zsh lit `.zprofile` pour les shells de connexion et `.zshrc` pour les shells interactifs. Une personne qui place des modifications du PATH et des sorties d'état dans `.zshenv` modifie aussi les commandes distantes, les scripts et souvent le lancement des applications graphiques.

POSIX `sh` ne fournit pas d'issue universelle. Les implémentations diffèrent. La spécification du shell POSIX décrit le fichier `ENV` pour les shells interactifs, mais le `/bin/sh` d'un système peut être dash, Bash en mode de compatibilité ou une autre implémentation avec ses propres détails. Testez l'hôte réel au lieu de supposer qu'un nom de fichier signifie la même chose partout.

Cette distinction a une conséquence opérationnelle concrète. Une commande qui réussit dans une session interactive de dépannage peut échouer pour un agent parce que la session interactive lit un profil que la session de commande ignore. L'inverse se produit aussi : un hook d'automatisation peut affecter les sessions de commande alors que le terminal d'un humain ne le voit pas.

Gardez les réglages interactifs dans des fichiers lus uniquement par les shells interactifs. Les invites, la configuration de la complétion, les titres de terminal, les couleurs par défaut et les messages d'accueil vont à cet endroit. Placez les exigences d'environnement de l'automatisation dans un petit fichier documenté qu'un lanceur contrôlé charge volontairement. Ne laissez pas un agent découvrir son environnement d'exécution en héritant des fichiers personnels d'un développeur.

## Les modifications du PATH changent l'identité, pas seulement le confort

Le PATH est souvent considéré comme un simple réglage pratique. Pour une exécution sans surveillance, il choisit l'identité du programme. Si un profil ajoute `/opt/team/bin` au début du PATH, `curl`, `git`, `ssh` ou `python` peut désigner un wrapper plutôt que le programme système.

Ce wrapper peut être intentionnel. Les équipes s'en servent pour choisir des identifiants cloud, imposer des contrôles de dépôt, ajouter de la télémétrie ou sélectionner des environnements de langage. L'erreur consiste à laisser un agent déduire qu'une commande sans chemin désigne l'exécutable du fournisseur. Elle signifie seulement « le premier exécutable correspondant dans le PATH actuellement utilisé par ce shell ».

Les alias et les fonctions créent une ambiguïté similaire, même si leur comportement dépend du mode du shell. Bash ne développe normalement pas les alias dans un shell non interactif, sauf si `expand_aliases` est activé. Les alias sont donc moins fréquents dans les exécutions SSH classiques, sans être impossibles. Un fichier chargé peut activer leur expansion. Les fonctions n'ont pas besoin de cette expansion. Un profil peut définir une fonction appelée `git`, exporter l'environnement et faire en sorte que chaque commande suivante dans ce shell appelle cette fonction.

Commencez par interroger le shell, mais considérez sa réponse comme un élément de preuve sur l'environnement examiné, pas comme une preuve universelle :

```sh
ssh -T deploy@buildbox '
printf "shell argv0: %s\n" "$0"
printf "shell flags: %s\n" "$-"
printf "PATH: %s\n" "$PATH"
command -V git
command -V python
command -V curl
'
```

Un résultat courant pourrait ressembler à ceci :

```text
shell argv0: -bash
shell flags: hBc
PATH: /opt/team/bin:/usr/local/bin:/usr/bin:/bin
git is /opt/team/bin/git
python is /usr/local/bin/python
curl is /usr/bin/curl
```

La commande intégrée `command -V` peut signaler un alias, une fonction, une commande intégrée ou un chemin. Dans les shells qui le prennent en charge, `type -a git` peut afficher plusieurs chemins correspondants. Dans Bash, `declare -f git` affiche le corps d'une fonction si `git` désigne une fonction. Ces vérifications révèlent un problème courant : un profil définit une fonction `git()` qui exécute une vraie commande Git puis envoie discrètement un événement d'état. La sortie ressemble toujours à celle de Git. L'effet secondaire se produit ailleurs.

Le hachage des commandes ajoute une autre difficulté. Certains shells mémorisent l'emplacement où ils ont trouvé une commande. Si un profil modifie le PATH après la résolution d'un nom, le shell peut continuer à utiliser l'emplacement mémorisé jusqu'à l'exécution de `hash -r` ou de son équivalent. Cela concerne surtout les shells interactifs de longue durée, mais des agents qui gardent un processus shell ouvert peuvent aussi y être confrontés.

Utilisez des chemins absolus pour les commandes dont l'identité influence une décision de sécurité ou un résultat de déploiement. N'écrivez pas `/usr/bin/git` simplement parce que ce chemin existe sur votre ordinateur portable. Vérifiez le chemin attendu sur le système d'exploitation cible, consignez-le et testez-le sous le compte qui exécutera le travail. Si le déploiement utilise volontairement un gestionnaire de versions ou un wrapper, nommez explicitement ce wrapper et intégrez son contrat à la tâche.

## Un pseudo-terminal modifie davantage que la mise en forme

`ssh host command` n'alloue normalement pas de pseudo-terminal. `ssh -t host command` en force un. Cette seule option peut activer d'autres branches d'initialisation du shell, modifier la mise en mémoire tampon, amener les programmes à émettre des codes couleur et provoquer des demandes de saisie qu'une exécution sans TTY ne ferait jamais.

De nombreux fichiers de configuration shell commencent par un test comme celui-ci :

```sh
case $- in
  *i*) ;;
  *) return ;;
esac
```

Ce test quitte le reste du fichier lorsque le shell ne se considère pas comme interactif. Un pseudo-terminal peut rendre le shell interactif dans des circonstances où une connexion de commande ordinaire ne l'était pas. La commande reçoit alors des alias, une configuration de complétion, des assistants d'invite, des modifications du PATH et des variables propres au terminal qui étaient absents de l'exécution de l'agent.

Les programmes réagissent eux aussi directement au terminal. Une commande peut afficher une progression, paginer sa sortie, choisir un autre format de diagnostic ou lire une confirmation. Un analyseur qui attend un seul objet JSON peut échouer parce qu'un profil affiche un message avant le démarrage du programme, ou parce que le programme détecte un TTY et émet des caractères de contrôle.

Testez les deux modes lorsque vous recherchez une différence :

```sh
ssh -T deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
ssh -tt deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
```

La première commande demande à SSH de ne pas allouer de terminal. La seconde en force un, même lorsque l'entrée standard locale n'est pas un terminal. Comparez la sortie, puis la résolution des commandes et le PATH dans chaque mode. Si les résultats diffèrent, ne corrigez pas d'abord l'analyseur. Cherchez la branche d'initialisation qui a créé cette différence.

L'automatisation devrait utiliser `-T` par défaut. N'allouez un terminal que pour une commande qui en a réellement besoin, par exemple une tâche de récupération interactive contrôlée. Si un agent a besoin d'un terminal pour exécuter une simple vérification d'état, son environnement est déjà moins prévisible.

## Les règles du serveur peuvent remplacer la commande demandée

Les fichiers d'initialisation ne sont qu'une couche. Le serveur peut remplacer ou restreindre une commande avant que le shell du compte ne la voie. Si vous inspectez `.bashrc` puis vous arrêtez là, vous pouvez manquer la règle qui a réellement modifié le résultat.

Le manuel de `sshd_config` décrit `ForceCommand`. Un administrateur peut l'appliquer globalement ou dans un bloc `Match` pour un utilisateur, un groupe, une adresse ou une autre condition. Avec `ForceCommand internal-sftp`, par exemple, le serveur ignore les commandes shell ordinaires et lance le service SFTP interne. Avec un wrapper personnalisé, le wrapper reçoit le contexte de la commande demandée et décide quoi faire.

Une entrée de clé publique SSH peut également contenir une restriction `command="..."` dans le fichier `authorized_keys` du compte. C'est courant pour les comptes de sauvegarde, l'accès aux dépôts, le transfert de fichiers et l'automatisation limitée. Cela peut être une bonne pratique de sécurité. Le débogage devient difficile lorsqu'un agent reçoit un identifiant restreint alors qu'on attend de lui l'exécution de commandes arbitraires.

Les contrôles d'environnement doivent être examinés au même endroit. `AcceptEnv` indique à sshd quelles variables fournies par le client il accepte. `SetEnv` peut définir des variables côté serveur. `PermitUserEnvironment`, lorsqu'il est activé, peut permettre au fichier d'environnement SSH d'un compte ou aux options d'une clé autorisée de définir des valeurs. Ces réglages peuvent influencer le PATH, la locale, le comportement d'un environnement de langage et des hooks tels que `BASH_ENV`.

Utilisez `ssh -G` côté client, tout en connaissant ses limites :

```sh
ssh -G buildbox | grep -E '^(hostname|user|port|requesttty|remotecommand|sendenv|setenv) '
```

OpenSSH affiche la configuration client après l'application des blocs `Host` locaux et des valeurs par défaut. La sortie peut révéler un `RemoteCommand` inattendu, un réglage forcé du terminal, une règle de transfert d'environnement ou une autre machine cible. Elle ne peut pas révéler `ForceCommand` côté serveur, le shell du compte distant ni les restrictions stockées dans `authorized_keys`.

Demandez au responsable du serveur la partie pertinente de `sshd_config` et les restrictions du compte lorsque celui-ci est destiné à l'automatisation. Si vous n'administrez pas l'hôte, demandez une interface d'exécution documentée au lieu d'essayer de reconstituer le comportement d'un shell personnel par essais successifs. Un compte doté d'un wrapper forcé opaque n'est pas un point d'accès SSH général, même s'il accepte l'authentification.

## Inspectez le chemin d'exécution sans faire confiance à un seul test

Une commande de diagnostic s'exécute dans l'environnement même qui fait l'objet des soupçons. Cela ne rend pas le diagnostic impossible. Cela signifie qu'il faut recueillir plusieurs faits indépendants et préciser ce que chacun démontre.

Commencez par inspecter la configuration du client local avec `ssh -G`. Déterminez ensuite le shell configuré pour le compte distant dans la base de comptes de l'hôte. Sur de nombreux hôtes Linux, `getent passwd deploy` renvoie une entrée séparée par des deux-points dont le dernier champ est le chemin du shell. Sur macOS, un administrateur peut consulter le compte avec les outils Directory Service. Dans la mesure du possible, faites-le depuis un canal d'administration fiable plutôt que via le shell potentiellement personnalisé du compte.

Récupérez et examinez ensuite les fichiers d'initialisation du shell réel : fichiers globaux, fichiers personnels et fichiers qu'ils chargent. Recherchez notamment :

- les affectations `PATH=`, l'initialisation des gestionnaires de versions et les commandes `hash` ;
- les `alias`, les définitions de fonctions et les options shell comme `expand_aliases` ;
- les commandes de sortie telles que `echo`, `printf` et les assistants de contrôle du terminal ;
- `BASH_ENV`, `ENV`, les variables de locale et les hooks d'environnement propres aux langages ;
- les branches conditionnelles qui testent `-t`, `$-`, `$SSH_TTY` ou `$TERM`.

Ne cherchez pas seulement le mot `alias`. Une ligne comme `. ~/.local/share/tool/init.sh` peut charger le fichier qui définit la fonction qui vous intéresse. Suivez la chaîne des fichiers chargés jusqu'à son terme. Les profils grossissent souvent en une accumulation d'inclusions conditionnelles, et c'est là que le comportement de l'automatisation devient difficile à contrôler.

Effectuez ensuite une courte empreinte avec une commande sans danger. Consignez les indicateurs du shell, le répertoire courant, le PATH, les noms de variables pertinents et la résolution exacte des outils utilisés par la tâche. Évitez de déverser toutes les variables d'environnement dans les journaux. Des jetons et des identifiants cloud y résident souvent. Un compte rendu de diagnostic doit prouver les conditions d'exécution sans devenir un nouveau coffre à secrets.

Enfin, testez la commande de production exacte dans le mode exact utilisé par l'agent : même compte, aucun terminal sauf nécessité, même transport d'entrée et même répertoire de travail. Un résultat obtenu via la connexion interactive d'un administrateur ne peut pas la remplacer. Il répond à une autre question.

Conservez les éléments de preuve avec la définition du déploiement ou de l'automatisation. L'artefact utile n'est pas une capture d'écran d'un terminal fonctionnel. C'est un court document qui indique le shell du compte, les fichiers d'initialisation pouvant influencer l'exécution, les chemins d'exécutables requis, l'environnement attendu, la politique de terminal et les restrictions de commande côté serveur.

## Rendez le shell d'automatisation volontairement ennuyeux

Une action distante fiable doit entrer dans un shell connu avec un environnement réduit, puis exécuter des binaires nommés. Cela ne supprime pas le shell initial utilisé par sshd, mais limite la quantité d'état hérité qui atteint le programme chargé du travail.

Pour un petit script fixe, envoyez le script par l'entrée standard et lancez un shell connu avec un environnement vide. La chaîne de commande distante reste fixe, ce qui évite également de construire une fragile succession d'échappements entre guillemets locaux et distants.

```sh
ssh -T deploy@buildbox '/usr/bin/env -i PATH=/usr/bin:/bin /bin/sh -s' \u003c\u003c'REMOTE'
set -eu
PATH=/usr/bin:/bin
export PATH

/usr/bin/git -C /srv/app rev-parse HEAD
REMOTE
```

Cette méthode possède plusieurs avantages. `-T` évite un terminal. `/usr/bin/env -i` efface les variables héritées. `/bin/sh -s` lit le script littéral depuis l'entrée standard, et le heredoc entre guillemets empêche le shell local de développer `$PATH` ou un autre texte avant la transmission. `set -eu` arrête l'exécution en cas de paramètre non défini ou d'échec d'une commande simple, selon la sémantique habituelle du shell.

Elle a aussi des limites. `/usr/bin/env`, `/bin/sh` et `/usr/bin/git` sont des exemples, pas des chemins à recopier aveuglément sur tous les hôtes. Confirmez les chemins pour chaque catégorie de cible. Vider l'environnement peut supprimer des variables dont un programme a légitimement besoin, notamment le répertoire personnel, la locale, la configuration du proxy ou l'emplacement d'un environnement d'exécution. Ne réintroduisez que les variables nommées nécessaires au programme et documentez la raison de chacune.

N'utilisez pas `env -i` pour masquer une configuration de compte défectueuse. Si un fichier d'initialisation modifie si fortement le shell initial qu'il empêche le lanceur fixe de démarrer, le compte ne convient pas au travail sans surveillance. Utilisez un compte dédié, avec un shell de connexion contrôlé et un minimum de fichiers d'initialisation.

Évitez d'insérer du texte arbitraire fourni par un agent dans une chaîne distante `sh -c`. Une erreur de guillemets peut transformer des données en syntaxe shell avant que le script prévu ne les reçoive. Transmettez les données par un canal limité, utilisez un script distant fixe avec des arguments validés ou adoptez un protocole conçu pour les requêtes structurées. Le shell est puissant parce qu'il analyse le texte comme du code. Un agent ne doit pas pouvoir brouiller cette frontière par accident.

## Séparez les comptes interactifs des comptes d'agents

La conception la plus propre donne aux humains et à l'automatisation des identités distantes différentes. Un compte de développeur peut conserver son invite, sa complétion, ses gestionnaires de langages et ses outils personnels. Un compte d'automatisation doit avoir un shell déclaré, un petit répertoire personnel et des fichiers d'initialisation qui ne font rien pour les sessions de commande ou qui exécutent une préparation étroitement contrôlée.

Cette séparation n'est pas une formalité. Elle permet de répondre à des questions simples mais nécessaires : quel binaire Git est utilisé pour le déploiement ? Quel est le répertoire de départ ? Le compte accepte-t-il un terminal ? Quels noms de variables peuvent influencer une mise en production ? Qui peut modifier le wrapper qui exécute les commandes ?

Un compte dédié facilite également la compréhension des restrictions de commande. Vous pouvez utiliser un wrapper forcé pour les quelques actions prévues par ce compte et refuser tout le reste. Cela ne transforme pas le wrapper en langage de politiques. Gardez-le assez simple pour être inspecté : validez les arguments, définissez l'environnement documenté, appelez un programme avec un chemin absolu et écrivez une trace d'audit.

Ne résolvez pas ce problème en plaçant une grande instruction `case` avec des dizaines d'exceptions dans `.bashrc`. Cette approche plaît parce que chaque utilisateur possède déjà un fichier de profil et qu'aucune modification du déploiement n'est nécessaire. Elle échoue parce que la sémantique des profils varie selon le mode du shell, que l'ordre de chargement devient obscur et qu'une modification interactive anodine peut changer le comportement sans surveillance. Placez la préparation de l'automatisation dans le lanceur ou le script dédié qui porte le contrat d'automatisation.

Sallyport peut garder les identifiants SSH hors du processus d'un agent et exécuter les actions SSH via son utilitaire intégré `sp-ssh`, mais il ne peut pas rendre déterministe un compte distant non contrôlé. Intégrez l'examen du compte distant à la conception de l'action, puis utilisez l'enregistrement d'activité pour comparer ce que l'agent a demandé à ce que le côté distant a renvoyé.

## Ne faites confiance aux résultats qu'après avoir défini le contrat

Le résultat d'une commande d'agent est fiable lorsque vous pouvez dire ce qui s'est exécuté sans vous appuyer sur les habitudes actuelles du shell d'une personne. Cette affirmation demande plus qu'un nom d'hôte et un code de sortie. Elle doit identifier le compte SSH, l'identité de l'hôte cible, le réglage du terminal, l'éventuelle restriction de commande côté serveur, le shell sélectionné, la construction de l'environnement, la source du script et le chemin de l'exécutable.

Il existe ici une limite difficile à contourner. La sortie d'une commande distante ne peut pas prouver à elle seule que le shell distant n'a pas modifié la commande avant de l'afficher. Si le shell du compte, la restriction de la clé autorisée ou la commande forcée échappe à votre contrôle, il vous faut des éléments de preuve administratifs ou un autre compte. Répéter le même test dix fois ne fait que répéter la même hypothèse.

Commencez par les commandes que les agents exécutent déjà et qui peuvent modifier du code, de l'infrastructure ou des données de production. Pour chacune, exécutez-la avec et sans terminal, vérifiez la résolution de son exécutable et remplacez la préparation héritée par un lanceur explicite lorsque le résultat est important. La première surprise vient généralement du PATH. La seconde vient souvent d'un fichier d'initialisation dont quelqu'un avait oublié qu'il était encore chargé.

Après avoir découvert l'une de ces surprises, n'ajoutez pas une nouvelle condition au profil en considérant le problème réglé. Déplacez le comportement vers un compte contrôlé ou un script fixe, consignez le contrat de commande et rendez la prochaine exécution de l'agent suffisamment ennuyeuse pour que sa sortie signifie exactement ce qu'elle dit.
