7 min de lecture

Les commandes SSH forcées peuvent-elles contenir un compte de service IA ?

Les commandes SSH forcées limitent les comptes de service contrôlés par une IA à des actions serveur nommées, tandis que l'approbation humaine reste à la limite d'accès aux identifiants.

Les commandes SSH forcées peuvent-elles contenir un compte de service IA ?

Un agent IA ne devrait jamais recevoir un identifiant SSH qui signifie « faites tout ce que le compte peut faire ». Ce n'est pas une limite d'accès. C'est une invitation à en trouver une, généralement par le biais d'un argument auquel vous ne vous attendiez pas, d'une connexion transférée que vous avez oublié de désactiver ou d'un script de déploiement qui fait trop confiance à son appelant.

Les commandes SSH forcées donnent au serveur distant le dernier mot sur ce qui démarre après l'authentification. Elles conviennent bien aux comptes de déploiement et de diagnostic, car elles remplacent une capacité vague, l'accès à un shell distant, par une opération nommée que vous contrôlez et pouvez inspecter. Elles ne remplacent pas l'approbation humaine de l'utilisation d'un identifiant. Gardez ces deux contrôles séparés : une personne décide si un agent peut utiliser l'identifiant, et le serveur décide de l'action limitée que cet identifiant peut effectuer.

Une commande forcée limite l'exécution, pas l'authentification

Une commande forcée demande à sshd d'exécuter un programme choisi par le serveur, même lorsque le client demande un shell ou fournit une autre commande. Le client doit toujours commencer par s'authentifier. Cette distinction semble évidente jusqu'à ce qu'un compte de service apparaisse dans la configuration d'un agent et que l'on commence à traiter une connexion réussie comme s'il s'agissait d'un déploiement approuvé.

OpenSSH prend en charge ce contrôle à deux endroits. Vous pouvez associer command="/path/to/wrapper" à une clé publique dans authorized_keys, ou définir ForceCommand dans sshd_config pour un utilisateur ou un groupe. Dans les deux cas, sshd enregistre la commande demandée par le client dans la variable d'environnement SSH_ORIGINAL_COMMAND, puis démarre le programme forcé.

Le manuel OpenSSH sshd(8) est très clair sur le premier point : une option command force l'exécution de la commande indiquée après l'authentification. Il précise aussi que la commande d'origine reste disponible pour ce programme forcé. C'est ce deuxième détail qui fait échouer de nombreuses conceptions fragiles. Le wrapper reçoit une chaîne provenant d'un client non fiable. Il doit analyser cette chaîne comme une demande, et non la transmettre à un shell.

Utilisez la forme par clé lorsqu'un compte possède plusieurs identifiants soigneusement séparés. Un identifiant de mise en production peut démarrer le wrapper de déploiement, tandis qu'un identifiant d'exploitation peut démarrer un wrapper de diagnostic en lecture seule. L'intention reste visible dans authorized_keys et vous pouvez supprimer un identifiant sans modifier les autres accès du compte.

Utilisez ForceCommand lorsque le compte lui-même ne doit jamais fournir un shell général, quelle que soit sa méthode d'authentification. Cela inclut un mot de passe que vous avez oublié de désactiver, une future autorité de certification ou un administrateur qui ajoute une autre clé publique sans recopier les options nécessaires. Un bloc Match User deploy rend la règle difficile à manquer lors d'une revue.

N'utilisez aucune de ces formes pour transformer un compte d'administrateur humain en compte d'automatisation. Les personnes finiront par avoir besoin d'un vrai shell pour réparer un problème. Donnez à l'automatisation un compte Unix distinct, un identifiant distinct, un wrapper distinct et des limites de propriété adaptées à la tâche.

Le serveur doit contrôler le point d'entrée du déploiement

Un compte de déploiement doit entrer dans un script unique que vous contrôlez, et non dans un interpréteur de commandes générique. Le script peut accepter un petit vocabulaire de demandes, mais il doit choisir lui-même le chemin du dépôt, le répertoire cible, l'unité de service et l'exécutable.

Cette entrée authorized_keys limite un seul identifiant à un wrapper et refuse les fonctions de connexion qui n'ont pas leur place dans un compte de déploiement :

restrict,command="/usr/local/libexec/release-gate" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent

L'option restrict est utile, car OpenSSH la décrit comme un raccourci qui désactive la redirection de ports, la redirection de l'agent, la redirection X11 et l'allocation d'un pseudo-terminal. Son comportement exact dépend des options OpenSSH prises en charge par le serveur, testez-la donc avec la version que vous utilisez. Si votre parc exige des options explicites pour la revue ou la compatibilité, écrivez-les :

command="/usr/local/libexec/release-gate",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent

Le wrapper ne doit pas accepter une commande de déploiement libre. Donnez aux appelants des verbes fixes et une valeur contrainte. Par exemple, ils peuvent demander une mise en production uniquement au moyen d'une révision immuable :

ssh [email protected] "release 9f2a7c6d1e4b8a03"

Un wrapper sûr peut autoriser cette grammaire, et rien d'autre :

#!/bin/sh
set -eu

request=${SSH_ORIGINAL_COMMAND-}
case "$request" in
  "release "[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]* )
    revision=${request#release }
    case "$revision" in
      *" "*|*[!0-9a-f]*)
        echo "invalid revision" >&2
        exit 64
        ;;
    esac
    exec /usr/local/libexec/run-release "$revision"
    ;;
  *)
    echo "unsupported remote request" >&2
    exit 64
    ;;
esac

Cet exemple nécessite encore une vérification de longueur si votre format de révision en impose une. En production, le wrapper doit accepter un identifiant d'objet immuable complet ou un identifiant de version dont vous avez défini le format. N'acceptez pas un nom de branche comme main si l'appelant peut le déplacer entre l'approbation et le déploiement. Une branche est un pointeur. Une révision immuable permet à l'enregistrement d'approbation, au journal de déploiement et à l'artefact produit de désigner exactement le même élément.

Le script run-release doit utiliser des chemins absolus et définir son propre environnement. Ne faites pas confiance au PATH, au répertoire de travail, aux paramètres régionaux, à GIT_DIR, à GIT_SSH_COMMAND ou à LD_PRELOAD fournis par l'appelant. Un début minimal ressemble à ceci :

#!/bin/sh
set -eu
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
unset CDPATH ENV BASH_ENV GIT_DIR GIT_WORK_TREE GIT_SSH_COMMAND
cd /srv/release-repo

revision=$1
/usr/bin/git cat-file -e "$revision^{commit}"
/usr/local/libexec/build-and-activate "$revision"

Le compte ne doit être propriétaire que des fichiers qu'il doit modifier. S'il doit redémarrer un service, accordez-lui une seule commande sudoers aux arguments fixes, plutôt qu'un accès sans mot de passe à un gestionnaire de paquets ou à un shell généraliste. Un compte de déploiement qui peut modifier son propre wrapper, ses propres authorized_keys ou l'unité qui exécute son code peut généralement retrouver un contrôle étendu. Vérifiez ces chemins, pas seulement la configuration SSH.

SSH_ORIGINAL_COMMAND est une entrée, pas une ligne de commande

L'erreur la plus fréquente avec les commandes forcées est la suivante :

sh -c "$SSH_ORIGINAL_COMMAND"

Cette ligne annule le contrôle que vous venez d'installer. Le client peut demander release goodrev; curl ... | sh, une substitution de commande, une sortie redirigée ou un argument soigneusement protégé qui atteint un outil privilégié. Un wrapper qui utilise eval, sh -c, bash -c ou une expansion non protégée a recréé l'accès à un shell distant sous un autre nom de fichier.

N'essayez pas de construire un analyseur complet de shell. Vous n'en avez pas besoin. Définissez un protocole volontairement réduit et refusez tout ce qui n'y entre pas. Pour un compte de déploiement, une demande peut être composée d'un verbe et d'un identifiant. Pour un compte de diagnostic, elle peut être un mot exact comme health ou version.

Un wrapper de dispatch pour les diagnostics peut éviter toute analyse :

#!/bin/sh
set -eu

case "${SSH_ORIGINAL_COMMAND-}" in
  health)
    exec /usr/local/libexec/report-health
    ;;
  queue-depth)
    exec /usr/local/libexec/report-queue-depth
    ;;
  version)
    exec /usr/local/libexec/report-version
    ;;
  "")
    echo "a diagnostic name is required" >&2
    exit 64
    ;;
  *)
    echo "diagnostic is not allowed" >&2
    exit 64
    ;;
esac

Ces scripts de diagnostic doivent eux aussi contrôler leurs arguments. report-health doit appeler des binaires fixes sur des sockets locaux fixes ou des noms de service connus. Il ne doit pas accepter un hôte et exécuter curl "$host", ni accepter un filtre de journal pour le transmettre à un shell. Une commande en lecture seule peut tout de même divulguer des identifiants de base de données, la topologie interne, des valeurs d'environnement ou des données client.

On entend souvent qu'une commande shell correctement protégée suffit, car l'agent appelant est considéré comme fiable. Cet argument ne tient plus lorsque l'agent suit des instructions hostiles présentes dans un dépôt, confond une valeur avec une instruction ou commet une simple erreur. Le serveur distant ne peut pas distinguer une demande dangereuse issue d'une attaque d'une demande issue d'un appel d'outil trop enthousiaste. Il ne voit que des entrées. Rendez sa décision déterministe.

Si vous avez besoin de données structurées, utilisez un format limité et analysez-le avec un parseur qui rejette les champs supplémentaires. JSON n'est pas automatiquement plus sûr, car un wrapper shell peut encore le traiter incorrectement. Une petite demande comme release <64 lowercase hex characters> est plus facile à valider, documenter, tester et auditer qu'un objet JSON comportant des champs facultatifs.

La redirection peut contourner l'esprit de la restriction

Une commande forcée n'empêche pas automatiquement un client authentifié d'utiliser SSH comme tunnel. Le manuel OpenSSH traite l'exécution des commandes et les redirections comme deux contrôles distincts. Si vous ajoutez seulement command="...", le client peut encore demander à sshd de rediriger un port local vers un service interne, selon le reste de votre configuration serveur.

Cela compte, car un compte restreint peut avoir un accès réseau que l'agent n'a pas. Un agent qui ne peut pas exécuter /usr/bin/ps sur l'hôte distant peut tout de même atteindre un port de base de données grâce à cet hôte si la redirection reste ouverte. Le compte cesse alors d'être une identité de déploiement et devient un point de rebond réseau.

Pour un compte qui n'a besoin d'aucune session interactive, refusez toutes ces fonctions, sauf si vous pouvez expliquer pourquoi le compte en a besoin :

  • redirection TCP
  • redirection de l'agent
  • redirection X11
  • allocation d'un pseudo-terminal
  • variables d'environnement contrôlées par l'utilisateur

Sur les déploiements OpenSSH modernes, restrict gère les quatre premières catégories. Si votre compte a légitimement besoin d'une exception, n'abandonnez pas tout le jeu de restrictions. OpenSSH propose des options comme permitopen="host:port" pour limiter la destination d'une redirection. Traitez cela comme une conception d'accès distincte et testez les destinations autorisées comme celles qui doivent être refusées.

Examinez aussi l'accès réseau sortant du wrapper. Un script de déploiement capable de récupérer des URL arbitraires, de cloner des dépôts arbitraires ou d'envoyer des données arbitraires vers l'extérieur dispose d'un canal étendu, même si les redirections SSH sont désactivées. Des sources d'artefacts fixes et des révisions épinglées réduisent cette exposition. Les règles de pare-feu ou des identifiants propres au service devront peut-être prendre le relais.

L'approbation doit précéder l'ouverture de la connexion

Visualisez chaque appel SSH
Le journal Activity enregistre chaque appel SSH dans un audit chiffré et chaîné par hachage.

Une commande forcée réduit les dommages qu'une utilisation SSH approuvée peut causer. Elle ne répond pas à la question de savoir si le processus d'agent actuel doit utiliser l'identifiant. Cette décision doit être prise à la limite d'accès à l'identifiant, avant que l'agent n'établisse une connexion SSH.

C'est particulièrement important pour les agents autonomes de programmation. Un dépôt peut demander à l'agent d'exécuter une commande de déploiement. La sortie d'un outil peut le lui demander. Une dépendance compromise peut l'y pousser. Si l'identifiant se trouve dans l'environnement ou le système de fichiers de l'agent, celui-ci peut l'utiliser sans qu'une personne voie le moment de l'utilisation.

Gardez les clés privées SSH hors du processus de l'agent et demandez une approbation lorsqu'une nouvelle exécution d'agent demande pour la première fois l'accès. Pour les comptes à conséquences élevées, demandez une approbation à chaque utilisation de l'identifiant. La commande forcée distante impose alors une limite stricte à l'action autorisée par cette approbation.

Sallyport applique cette séparation en conservant les clés SSH dans son coffre chiffré, en autorisant par défaut les nouveaux processus d'agents pour chaque session et en exécutant SSH via son assistant sp-ssh, plutôt que de remettre la clé à l'agent.

Ne confondez pas une demande d'approbation avec l'autorisation du serveur. L'approbation répond à « Ce processus peut-il utiliser cet identifiant maintenant ? » Le serveur répond à « Que peut faire cet identifiant après la connexion ? » Vous avez besoin des deux réponses, car leurs échecs sont différents. L'approbation peut arrêter un processus surprenant. Les commandes forcées peuvent empêcher un processus approuvé de transformer un identifiant de déploiement en shell.

Rendez la description de l'approbation utile. Indiquez l'environnement et l'action dans le libellé de l'identifiant, par exemple production release ou staging diagnostics. Un libellé comme deploy-key-2 oblige le réviseur à se souvenir du contexte pendant une interruption. C'est ainsi que les approbations routinières deviennent des clics automatiques.

Séparez le déploiement du diagnostic avant que la liste autorisée ne s'allonge

Révoquez une exécution d'agent
Le journal Sessions suit les exécutions d'agents et vous permet de révoquer une session instantanément.

Le déploiement et le diagnostic semblent similaires, car tous deux nécessitent SSH, mais leurs flux de données et leurs modes d'échec diffèrent. Placez-les derrière des comptes distincts ou des identifiants à commandes forcées distincts chaque fois que possible.

Un compte de déploiement modifie l'état du système. Il peut récupérer une révision fixe, construire un artefact, remplacer un répertoire de version et redémarrer un service. Sa sortie doit indiquer la révision, la cible, le statut de sortie et un bref message d'échec. Il n'a pas besoin d'un accès arbitraire aux journaux, à l'inspection des processus ou aux bases de données.

Un compte de diagnostic lit l'état du système. Il peut indiquer le résultat d'un point de contrôle, la taille limitée d'une file d'attente, la version d'un service ou la fin d'un journal local filtré avec soin. Il ne doit pas redémarrer des services, faire tourner des fichiers, interroger tous les processus ni lire des chemins arbitraires. Dès qu'un wrapper de diagnostic accepte un nom de fichier, d'unité, d'hôte ou une option de commande fournis par l'utilisateur, réévaluez son modèle d'entrée.

Un compte combiné commence par une liste innocente :

release <revision>
health
logs <service>
restart <service>

Puis quelqu'un a besoin de logs api --since, une autre personne d'un redémarrage d'urgence, et le wrapper commence à transmettre des arguments à journalctl ou systemctl. Bientôt, le code contient des cas particuliers que personne ne sait expliquer. Séparez les comptes avant d'en arriver là. Des identifiants distincts vous permettent d'exiger une approbation plus stricte pour les changements en production tout en conservant un diagnostic moins risqué.

Chaque action doit produire un enregistrement indiquant ce que le wrapper a accepté, et pas seulement la chaîne SSH opaque. Pour une version, consignez la révision immuable et le nom de la cible. Pour un diagnostic, consignez le nom du diagnostic et sa réussite ou son échec. Ne mettez pas de valeurs secrètes dans les arguments de commande ni dans les journaux. Si une action a besoin d'un secret, le script distant doit le récupérer grâce à son propre mécanisme contrôlé au lieu de l'accepter du client SSH.

Testez les refus depuis un client jetable

Un compte restreint n'est réellement restreint qu'après avoir testé les demandes qu'il doit refuser. Effectuez ces vérifications depuis un compte jetable ou un hôte de test avant de faire confiance à la configuration dans un environnement de production. Les exemples supposent que l'identifiant est déjà installé sur le serveur.

ssh [email protected] "release 9f2a7c6d1e4b8a03"
ssh [email protected]
ssh [email protected] "id"
ssh [email protected] "release 9f2a; id"
ssh -N -L 15432:db.internal:5432 [email protected]
ssh -tt [email protected] "health"

La première commande doit atteindre uniquement le wrapper de version si la révision respecte ses règles. Les trois suivantes doivent échouer avec le message de refus du wrapper et un code de sortie différent de zéro. La tentative de redirection de port doit échouer avant de pouvoir établir un écouteur. La demande de terminal doit échouer ou s'exécuter sans terminal, selon la manière dont votre client signale le refus de l'allocation.

Testez ensuite les cas moins évidents. Essayez les espaces en début et en fin, les tabulations, une commande entre guillemets vide, les caractères de nouvelle ligne, un argument très long, les espaces Unicode, les substitutions de commande, les redirections et les arguments en double. Si votre shell ou votre wrapper normalise l'un de ces cas pour l'accepter, resserrez la grammaire.

Vérifiez les permissions et la propriété des fichiers du compte dans le cadre du test. Un attaquant capable de remplacer /usr/local/libexec/release-gate n'a pas besoin de contourner SSH. Un compte capable de modifier sa propre source de déploiement peut aussi modifier le code qui s'exécute avec davantage de privilèges. Inspectez toute la chaîne : authorized_keys, configuration sshd, wrapper, scripts de déploiement, définitions de services, répertoires accessibles en écriture et toute entrée sudoers.

Auditez la demande des deux côtés de la limite

Vérifiez l'historique d'audit hors ligne
Exécutez sp audit verify sur le texte chiffré de l'audit, sans clé du coffre.

Les journaux distants expliquent ce que le serveur a accepté. Les journaux de la limite d'accès aux identifiants expliquent quel processus local a demandé la possibilité de se connecter. Conservez les deux, car aucun ne peut répondre à la question de l'autre.

Côté distant, consignez la réussite de l'authentification, l'opération acceptée par le wrapper forcé, la révision immuable ou le nom du diagnostic, un identifiant de demande et le statut final. Envoyez ces enregistrements vers un emplacement que le compte de service ne peut pas réécrire. Ne consignez pas la valeur brute de SSH_ORIGINAL_COMMAND si les appelants risquent d'y placer des secrets, et n'autorisez de toute façon pas votre protocole à accepter des secrets à cet endroit.

Côté local, conservez l'identité de la session et chaque demande d'action SSH. Sallyport enregistre les exécutions d'agents et les appels individuels dans des journaux distincts, au sein d'un même audit chiffré et chaîné par hachage. sp audit verify peut vérifier cette chaîne hors ligne sans clé du coffre.

Une chaîne de hachage ne transforme pas une mauvaise permission en bonne permission. Elle rend plus facile la détection d'une modification ultérieure de l'historique enregistré. C'est utile après un déploiement échoué, une approbation contestée ou une demande de commande surprenante. Cela impose aussi une discipline bénéfique : définir tôt le vocabulaire des actions pour que les enregistrements restent compréhensibles par une personne.

La première implémentation doit rester simple. Créez un compte Unix dédié, un wrapper forcé, une opération avec une grammaire d'entrée fixe, désactivez les redirections et ajoutez un test prouvant que ssh account@host ne fournit pas de shell. N'ajoutez des capacités que lorsque vous pouvez nommer leurs entrées, leurs sorties, leurs accès aux fichiers, leurs accès réseau et la personne qui doit les approuver.

FAQ

Qu'est-ce qu'une commande SSH forcée ?

Utilisez un compte SSH dont l'entrée authorized_keys contient l'option command="...", ou utilisez ForceCommand dans sshd_config lorsque toute méthode d'authentification de ce compte doit suivre la même règle. Le serveur exécute votre wrapper au lieu de la commande fournie par le client et transmet la demande d'origine via SSH_ORIGINAL_COMMAND.

Les commandes SSH forcées suffisent-elles à sécuriser un compte de service utilisé par une IA ?

Non. Une commande forcée limite ce que le serveur SSH lance après une authentification réussie, mais elle ne détermine pas qui peut utiliser l'identifiant. Associez-la à une limite d'accès aux identifiants qui exige qu'une personne approuve une nouvelle exécution d'agent ou une utilisation sensible, puis limitez suffisamment le compte serveur pour que cette approbation ait une conséquence maîtrisée.

Dois-je utiliser ForceCommand ou l'option command d'authorized_keys ?

En général, non. Une clé dédiée dans authorized_keys établit une correspondance claire entre un identifiant et une règle de commande forcée, tandis qu'un paramètre de compte partagé peut s'appliquer à la fois aux personnes et à l'automatisation. Utilisez ForceCommand lorsque vous voulez délibérément que toute voie d'accès à un compte restreint passe par le même wrapper.

Comment une commande forcée reçoit-elle la commande SSH d'origine ?

OpenSSH stocke la commande demandée dans SSH_ORIGINAL_COMMAND lorsqu'il exécute une commande forcée. Traitez cette valeur comme une entrée hostile : refusez les métacaractères du shell, les options que vous n'avez pas prévues et toute forme autre qu'un petit ensemble de commandes exactes.

L'option command d'authorized_keys désactive-t-elle la redirection de ports SSH ?

Non. L'option command="..." seule ne désactive ni la redirection de ports, ni la redirection de l'agent, ni la redirection X11, ni l'allocation d'un pseudo-terminal. Ajoutez restrict lorsque cette option convient, ou désactivez explicitement chaque fonctionnalité, puis testez le résultat depuis un client.

Comment limiter un compte SSH aux seuls déploiements ?

Un compte de déploiement doit exécuter un script dont vous contrôlez le contenu, avec des chemins fixes, un répertoire de travail déterminé, un environnement restreint et une liste autorisée de cibles de déploiement. N'acceptez pas une branche, un hôte, un chemin ou un fragment de shell arbitraire pour le transmettre à git, rsync, sudo ou à un shell.

Que doit refuser un wrapper de commande forcée ?

Retournez un code différent de zéro pour une demande vide, un shell, une sous-commande inconnue, des arguments mal formés ou une demande contenant des espaces inattendus ou des métacaractères. Journalisez le rejet avec le compte authentifié et les informations de source, mais ne consignez jamais les secrets transmis par des variables d'environnement ou dans le texte de la commande.

Les agents de programmation IA peuvent-ils utiliser des commandes SSH forcées sans interaction ?

Les commandes forcées fonctionnent avec SSH, car c'est le serveur, et non le processus d'IA, qui décide quel programme démarre après l'authentification. Elles ne nécessitent pas de terminal interactif sur l'hôte distant, ce qui les rend adaptées aux exécutions d'agents sans interaction, à condition de conserver l'approbation au niveau de l'accès à l'identifiant.

Comment auditer les actions effectuées via un compte SSH à commande forcée ?

Conservez les journaux du wrapper distant, les journaux de déploiement et les journaux d'authentification SSH, puis mettez-les en relation avec l'enregistrement d'utilisation de l'identifiant. Un journal local d'actions, évident à toute altération, est utile, car il indique quel processus d'agent a demandé l'utilisation de l'identifiant avant que le serveur distant ne reçoive la connexion.

Le déploiement et le diagnostic doivent-ils utiliser des comptes SSH distincts ?

Utilisez un compte distinct lorsque la tâche de diagnostic a un objectif, des commandes autorisées ou des conséquences différents du déploiement. Réunir les deux dans un même wrapper fait généralement grossir la liste autorisée jusqu'à créer accidentellement un shell distant.

Sallyport

Sallyport exécute les appels d'API et les commandes SSH à la place de votre agent IA. Les clés restent dans un coffre-fort local sur votre Mac ; vous approuvez chaque exécution et chaque action est consignée dans un journal scellé.

© 2026 Sallyport · Open source sous Apache-2.0 · Oleg Sotnikov