Une infrastructure pour obtenir un shell sur un ordinateur à distance

proposition dans un contexte d'infogérance de poste de travail Linux

Introduction

Imaginons, à tout hasard, un entreprise souhaitant faire de l’infogérance sur des postes de travail tournant sur du Linux. Pouvoir obtenir un shell sur les postes des client·es est un service utile si ce n’est nécessaire au bon fonctionnement du service. Mises-à-jour, dépannage, nettoyage, installation/configurations de nouveaux logiciels, autant d’opérations qui peuvent être menées depuis un shell.

L’entreprise veut toutefois que ce service respecte certains principes, notamment que le système :

Cet article vise à expliquer les contraintes auxquelles un tel système fait face, à expliquer les solutions envisagées et à motiver leurs choix. C’est avant tout un exercice pédagogique pour vérifier si l’infrastructure est effectivement facile à expliquer et comprendre. Aucune des personnes impliquées dans sa conception n’est spécialiste de ce type d’infrastructure, d’SSH ou de sécurité. Il est donc tout à fait possible qu’en l’état la proposition ne soit pas suffisament sécurisée. Par ailleurs ce système n’a pas été testé en condition réelle et encore moins sur un nombre un peu conséquent de machines. Toutes remarques ou améliorations sont les bienvenus.

Merci à Martin et Victor pour le serveur de test, Victor aussi pour les tests, Guillaume Salagnac et Matthieu Moy pour la conception !

Fonctionnement

S’authentifier avec SSH

Dans sa forme la plus simple s’authentifier et obtenir un shell sur une machine distante (machine cliente) requiert que cette machine fasse fonctionner un serveur SSH sshd et que la machine depuis laquelle on veut se connecter (machine support) puisse y accéder via un client ssh ssh :

    Client C.pub sshd |---------| ssh C.priv Support

----     Connexion réseau
C.pub    Clef publique ssh
C.priv   Clef privée   ssh

Pour que Support puisse effectivement s’authentifier sur Client sans se faire rejeter il faut que la clef publique C.pub2 qui est associée à la clef privée C.priv se trouve sur la machine qui reçoit la connexion. Une métaphore classique est la suivante : C.priv est une clef secrète, détenue par Support, qui permet d’ouvrir la serrure tout à fait publique C.pub. Si Client veut laisser Support entrer sur sa machine iel peut installer la serrure C.pub sur sa porte sshd. On verra par la suite que la clef publique, la “serrure”, doit être présente dans un fichier spécial nommé authorized_keys placé dans le dossier spécial .ssh pour qu’elle soit effective. On pourrait continuer à filer la métaphore en disant que ce fichier est notre “porte” mais bon.

Franchir le NAT

Cette approche a une limite. La condition “puisse y accéder” est rarement satisfaite, en particulier lorsque la machine cliente est un poste de travail. Du fait de la pénurie d’adresse IPv4 et d’enjeux de sécurité l’immense majorité des postes de travail possèdent en réalité des adresses IP privées, ayant l’avantage d’être très peu rares3 mais étant non accessibles directement depuis internet. Un équipement matériel réseau se trouvant entre le réseau privée et le réseau public, souvent nommé “passerelle” du fait de son rôle, a lui une unique adresse IP publique. L’équipement inclut un logiciel permettant d’associer pour chaque connexion internet une adresse IP privée à son4 adresse IP publique de manière multiplier le nombre d’appareil pouvant utiliser l’adresse IP publique. En supposant une machine support voulant communiquer à trois machines clientes derrière une passerelle :

                  Réseau privé | Internet
                               |
Client1 addrpriv1 |\           |
Client2 addrpriv2 |-| Passerelle addrpub1 |--------| addrpub2 Support
Client3 addrpriv3 |/           |
                               |
    addrpriv   Adresse IP privée
    addrpub    Adresse IP publique

Les machines extérieurs au réseau privé ne peuvent contacter les machines ClientN du réseau privé qu’au travers de la passerelle et son adresse IP publique addrpub1. C’est la passerelle qui s’occupera de faire le nécessaire pour savoir à qui le support veut vraiment parler et inversement. Ce mécanisme se nomme “Network Address Translation” (NAT). Les implémentations de NAT sont diverses, non standards et généralement assez restrictives. Elle ne permettent généralement pas à une machine extérieure au réseau privé de contacter directement une machine à l’intérieur sans avoir été d’abord sollicitée. Le NAT est permissif en sortie, dans le sens client -> support mais beaucoup moins dans l’autre sens5. Le support ne peut donc qu’assez rarement spontanément se connecter à un client, la connexion échouera à la passerelle :

                      Réseau privé | Internet
                                   |
Client1 addrpriv1 |     | Passerelle addrpub1 |X------<| addrpub2 Support
                                   |

Mais si le client est à l’initiative et que la machine support est publiquement accessible sur internet elle parviendra à l’atteindre et la machine support pourra répondre :

                      Réseau privé | Internet
                                   |
Client1 addrpriv1 |---->| Passerelle addrpub1 |------->| addrpub2 Support
Client1 addrpriv1 |<----| Passerelle addrpub1 |<-------| addrpub2 Support
                                   |

Malheureusement il est très probable que la machine support soit elle même dans un réseau privé et ne puisse pas être contactée par la machine cliente :

      Réseau privé |     Internet    | Réseau privé
                   |                 |
Client1 |--->| Passerelle |---X| Passerelle |    | Support
                   |                 |

Il existe des techniques sophistiquées pour contourner les NAT sans passer par une machine tierce6 mais puisque cela nous sera de toute façon utile par la suite d’avoir recours à un serveur disponible sur internet nous allons partir sur cette solution :

   Réseau privé |            Internet          | Réseau Privé
                |                              |
Client1 |\      |                              |       /| Support1
Client2 |-| Passerelle |---| Serveur |---| Passerelle |-| Support2
Client3 |/      |                              |       \| Support3

Les machines clientes peuvent contacter le serveur accessible depuis internet peu importe la topologie du réseau du moment qu’il possède un accès à internet. Idem pour les machines support. Le serveur lui ne peut contacter les une ou les autres que s’il a d’abord été sollicité. Le reste de notre problème sera d’orchestrer les connexions de façon à ce que le serveur serve de passe-plat pour les différentes machines ne pouvant pas communiquer entre elles.

Nous voulons obtenir un shell sur les machines clientes. Il faut que la connexion initiée par les machines clientes prépare le nécessaire pour que le serveur puisse la “remonter” à sa guise. SSH inclut une option répondant exactement à ce besoin, le “reverse tunnel”.

Le manuel du client SSH explique :

-R [bind_address:]port:host:hostport

Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.

This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connection is forwarded over the secure channel, and a connection is made from the local machine to either an explicit destination specified by host port hostport, or local_socket, or, if no explicit destination was specified, ssh will act as a SOCKS 4/5 proxy and forward connec‐ tions to the destinations requested by the remote SOCKS client.

En initiant une connexion SSH vers le serveur avec l’option -R le client peut préparer un tunnel dans l’autre sens permettant au serveur de se connecter au serveur SSH du client par la suite, franchissant ainsi la passerelle. Cela revient à “placer” le port 22 de la machine cliente sur le port 1312 du serveur.

Un schéma en omettant pour le moment la partie support :

              Réseau privé | Internet
                           |
1) Client1    |---->| Passerelle |------->| 22   Serveur
2) Client1 22 |<====| Passerelle |<=======| 1312 Serveur
                           |

    ===== tunnel ssh

  1) Le client contacte le serveur SSH du serveur sur le port 22 et demande
     un reverse tunnel
  2) Un reverse tunnel dans le sens `serveur -> client` est établi entre
     le port 1312, écoutant en local et le port 22 du client.

Une commande implémentant ce fonctionnement serait :

$ ssh -o 'ExitOnForwardFailure yes' \
      -f -N -T \
      -R '1312:localhost:22' \
      compte@serveur

Pour se connecter au client depuis le serveur, à condition qu’un serveur SSH tourne sur le client et que la clef publique correspondant à la clef privée du serveur y soit installée, il faut faire :

$ ssh root@localhost -p 1312

Ce qui revient à se connecter au port 22 du client à travers le tunnel précédemment établi. Dans cette commande localhost désigne le serveur sur lequel on se trouve mais root désigne bel et bien le compte root de la machine cliente distante. Il faut imaginer cette commande comme faisant quelque chose d’équivalent à :

$ ssh root@client1 -p 22

puisque toute demande faite sur le port 1312 du serveur est passée telle quelle au port 22 de la machine cliente à travers le tunnel.

Choix du port

Imaginons maintenant que notre système ait à nouveau trois machines clientes. Les opérateurices des machines clientes ont et doivent avoir la main sur leurs machines. Iels peuvent modifier la commande ssh pour changer la partie -R port:localhost:22 et y mettre le port de leur choix. Client1 et Client2 pourraient tous deux avoir envie d’utiliser le port 1312. Ouvrir un reverse tunnel sur un port déjà utilisé n’est pas possible. De plus l’identification de la machine se cachant effectivement derrière un port donné n’est pas assurée :

                  Réseau privé | Internet
                               |
1) Client1    |--r1312-->| Passerelle |--r1312-->| 22   Serveur
2) Client1 22 |<=========| Passerelle |<=========| 1312 Serveur
3) Client2    |--r1312-->| Passerelle |--r1312-->| 22   Serveur
4) Client2    |<---non---| Passerelle |<---non---|      Serveur
                               |

    =====      tunnel ssh
    --rXXXX--> demande de reverse tunnel sur le port XXXX
    <---non--- réponse avec erreur

  1) Client1 demande un reverse tunnel sur le port 1312
  2) Le serveur créé un reverse tunnel entre 1312 local
     et 22 distant
  3) Client2 demande un reverse tunnel sur le port 1312
  4) Le serveur refuse la création d'un reverse tunnel
     sur un port déjà utilisé.

Il y a donc un enjeux à créer une association machine cliente <-> port fiable et immuable pour les clients de façon à :

  1. Éviter que deux machines clientes tentent d’utiliser le même port
  2. Identifier avec certitude la machine cliente derrière un port donné

La première propriété pourrait être obtenue grâce à une syntaxe spécifique de l’option -R :

-R [bind_address:]port:host:hostport

[…]

If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time. When used together with -O forward, the allocated port will be printed to the standard output.

Ainsi avec -R '0:localhost:22' le serveur SSH du serveur choisira lui-même un port adéquat non utilisé. Malheureusement ce fonctionnement ne permet pas de satisfaire seul la propriété 2 puisque le port est choisi au hasard. Côté serveur rien ne permet de rapprocher un port ouvert avec l’identité de la machine cliente, pas même les logs7. La machine cliente peut récupérer le port qui lui a été aléatoirement attribué dans la réponse que le serveur SSH renvoie après création du tunnel :

$ ssh -o 'ExitOnForwardFailure yes' \
      -f -N -T \
      -R '0:localhost:22' \
      compte@serveur
40000

Il est alors imaginable d’envoyer au serveur un message contenant une paire clef_publique n°port comme pour indiquer “telle machine écoute sur tel port”. Soit ce message est envoyé de manière non authentifiée8 et il devient alors compliqué d’authentifier à posteriori le message pour s’assurer que ce n’est pas un·e attaquant·e cherchant à brouiller les pistes, soit le message est envoyé via SSH et donc authentifié. Nous9 avons jugé qu’il était préférable d’avoir recours à la seconde solution, celle d’un message authentifié via SSH. On pourrait imaginer un scénario du type :

              Réseau privé | Internet
                           |
1) Client1    |---r0-->| Passerelle |---r0-->|      Serveur
2) Client1 22 |<=4000==| Passerelle |<=4000==| 4000 Serveur
3) Client1    |--4000->| Passerelle |--4000->|      Serveur Client1=4000
                           |

    <===XXXX====     tunnel ssh + message contenant le port attribué
    ClientN=XXXX   association client<->port stocké dans une base

  1) Le client demande un reverse tunnel avec un port aléatoire (`0`).
  2) Le serveur ouvre un reverse tunnel persistant sur le port 4000
     et envoie l'information au client en réponse
  3) Le client envoie le numéro du port au serveur sur une
     nouvelle connexion temporaire pour qu'il consigne
     quelque part l'association entre Client1 et 4000.

Cette solution a un défaut gênant, similaire à celui identifié au début de ce titre. Puisque les opérateurices des machines clientes ont complète autonomie sur leurs machines on pourrait imaginer qu’iels modifient le script orchestrant tout ça pour mentir sur le port qui leur a été attribué.

              Réseau privé | Internet
                           |
1) Client1    |---r0-->| Passerelle |---r0-->|      Serveur
2) Client1 22 |<=4000==| Passerelle |<=4000==| 4000 Serveur
3) Client1    |--4001->| Passerelle |--4001->|      Serveur Client1=4001
                           |

  1) Le client demande un reverse tunnel avec un port aléatoire (`0`).
  2) Le serveur ouvre un reverse tunnel persistant sur le port
     4000 et envoie l'information au client en réponse
  3) Le serveur associe à tort Client1 au port 4001

Imaginons que la personne utilisant la machine Client1 ait programmé une opération ambitieuse de suppression de donnée pour faire de l’espace sur son disque dur auprès du support à 14h. Cette personne sait que son a sa collègue utilisant Client2 a elle aussi une opération de support programmé à 14h. Admettons que dans cette entreprise tout le monde puisse voir par dessus les épaules des autres et ainsi espérer glaner le port automatiquement attribué à ses collègues10. Voici une attaque possible :

              Réseau privé | Internet
                           |
1) Client1    |---r0-->| Pass. |---r0-->|         Serveur
2) Client1 22 |<=4000==| Pass. |<=4000==| 4000    Serveur
3) Client2    |---r0-->| Pass. |---r0-->|         Serveur
4) Client2 22 |<=4001==| Pass. |<=4001==| 4001    Serveur
5) Client2    |--4001->| Pass. |--4001->|         Serveur Client2=4001
6) Client1    |--4001->| Pass. |--4001->|         Serveur Client2=4001,Client1=4001
7) Client2 22 |<=4001==| Pass. |<=4001==| 4001 <- Serveur <- support Client1
                           |

  1) Client1 demande l'ouverture d'un tunnel.
  2) Client1 reçoit son port mais ne l'envoie pas de suite au serveur.
  3) Client2 demande l'ouverture d'un tunnel.
  4) Client2 reçoit son port.
  5) Client2 envoie son port au serveur.
  6) Rapidement Client1 envoie au serveur le port attribué à Client2, faisant
     donc croire que c'est le sien
  7) Le support demande à se connecter à Client1 pour supprimer des fichiers,
     un script vérifie quel est le port correspondant, trouve le port que Client1
     à dit être le sien mais qui mène en réalité à Client2. Le support arrive sur
     Client2 et supprime les mauvais fichiers.

Pour corriger ce problème on peut utiliser l’option PermitListen d’SSH. Elle peut être inscrite dans la configuration du serveur à l’échelle du serveur entier ou d’un compte. Elle peut aussi être inscrite dans le fichier authorized_keys en face d’une clef publique, permettant ainsi d’associer une machine cliente à un port particulier bien que toutes les machines clientes se connectent toutes au même compte11. La forme la plus simple est la suivante, une clef publique par ligne :

options ssh_ed25519 AAAA... client1
options ssh_ed25519 AAAA... client2
options ssh_ed25519 AAAA... client3

Dans les schémas quand apparaît Client kN.pub | cela veut dire qu’il existe une ligne dans .ssh/authorized_keys avec la clef publique kN.pub sous le format options ssh_ed25519 contenu_de_la_clef commentaire, le commentaire étant souvent une information permettant d’identifier à quelle machine cette clef publique correspond et options une liste facultative d’options à appliquer à toute connexion provenant de la machine correspondant à cette clef publique en particulier.

L’option permitlisten s’écrit de la manière suivante :

permitlisten=”[host:]port”

Limit remote port forwarding with the ssh(1) -R option such that it may only listen on the specified host (optional) and port. IPv6 addresses can be specified by enclosing the address in square brackets. Multiple permitlisten options may be applied separated by commas. Hostnames may include wildcards as de‐ scribed in the PATTERNS section in ssh_config(5). A port speci‐ fication of * matches any port. Note that the setting of GatewayPorts may further restrict listen addresses. Note that ssh(1) will send a hostname of “localhost” if a listen host was not specified when the forwarding was requested, and that this name is treated differently to the explicit localhost addresses “127.0.0.1” and “::1”.

On peut ainsi associer un port en face de chaque clef publique renseignée dans le fichier, c’est à dire à chaque machine cliente :

permitlisten="1312" ssh_ed25519 AAAA... client1
permitlisten="1313" ssh_ed25519 AAAA... client2
permitlisten="1314" ssh_ed25519 AAAA... client3
[...]

Côté client il est alors impossible de faire ouvrir un reverse tunnel sur un autre port que celui en face de sa clef. Client1 devra nécessairement utiliser -R '1312:localhost:22'. Malheureusement l’utilisation du port 0 ne déclenche pas de la part du serveur SSH une recherche dans authorized_keys du port autorisé. Ceci est de la fiction :

              Réseau privé | Internet
                           |
1) Client1    |---r0-->| Passerelle |---r0-->|      Serveur    Client1=1312
2) Client1    |        | Passerelle |        |      Serveur -> Client1=1312
3) Client1 22 |<=======| Passerelle |<=======| 1312 Serveur    Client1=1312

      1) Client1 demande l'ouverture d'un tunnel.
      2) Le serveur cherche dans la configuration si Client1 est limité à un port
         particulier
      3) Si oui le serveur ouvre un reverse tunnel sur ce port

Il faut donc conserver l’ouverture du tunnel en deux temps mais dans le sens chronologiquement inverse. Plutôt que de demander le tunnel et informer le serveur du port on récupère le port du serveur d’abord puis on demande l’ouverture du tunnel ensuite :

                Réseau privé | Internet
                             |
1) Client1    |---?--->| Passerelle |---?--->|      Serveur    Client1=1312
2) Client1    |        | Passerelle |        |      Serveur -> Client1=1312
3) Client1    |<-1312--| Passerelle |<-1312--|      Serveur    Client1=1312
4) Client1    |-r1312->| Passerelle |-r1312->|      Serveur    Client1=1312
5) Client1 22 |<=======| Passerelle |<=======| 1312 Serveur
                             |

    ---?--- Demande du port associé à sa clef publique

      1) Client1 demande quel est son port
      2) Le serveur consulte l'`authorized_keys`
      3) Le serveur répond 1312
      4) Client1 demande l'ouverture du tunnel sur le port 1312
      5) Le serveur ouvre le reverse tunnel sur le port 1312

Cette solution permet de garantir au support de savoir sur quelle machine il se connecte en accédant à un port en particulier et empêche deux machines clientes d’essayer d’ouvrir un tunnel sur le même port. Mission accomplie.

Ouverture du tunnel et sécurité

En reprenant le schéma précédent, que se passe-t-il vraiment lors des deux premières étapes :

1) Client1 |---?--->| Passerelle |---?--->| Serveur    Client1=1312
2) Client1 |        | Passerelle |        | Serveur -> Client1=1312

Pour implémenter ce comportement il faut pouvoir provoquer l’exécution d’un script personnalisé sur le serveur. Pour pouvoir faire cela sans pour autant permettre l’exécution de n’importe quelle commande il est possible d’utiliser l’option ForceCommand dans la configuration du serveur :

ForceCommand

Forces the execution of the command specified by ForceCommand, ignoring any command supplied by the client and ~/.ssh/rc if present. The command is invoked by using the user’s login shell with the -c option. This applies to shell, command, or subsystem execution. It is most useful inside a Match block. The command originally supplied by the client is available in the SSHORIGINALCOMMAND environment variable. Specifying a command of internal-sftp will force the use of an in-process SFTP server that requires no support files when used with ChrootDirectory. The default is none.

Puisque le serveur doit rester accessible pour l’administration il faut circonscrire cette configuration à un compte en particulier. Il faut donc créer un compte spécifique ne permettant que la création de reverse tunnel. On peut le nommer reverse. Plus tôt dans l’article la commande générique pour ouvrir un tunnel mentionnait un compte quelconque compte. Dorénavant c’est spécifiquement le compte reverse par lequel il faut passer :

$ ssh -o 'ExitOnForwardFailure yes' \
      -f -N -T \
      -R '1312:localhost:22' \
      reverse@serveur

Dans la configuration du serveur SSH il faut restreindre ce compte à une seule commande :

Match user reverse
    ForceCommand reverse

Ainsi à chaque connexion sur le compte reverse la commande reverse sera exécutée. Cette commande doit consulter le fichier authorized_keys du compte reverse pour trouver le port correspondant. Reste à savoir qui a initié la connexion. Pour cela il faut activer une configuration du serveur SSH nommée ExposeAuthInfo :

ExposeAuthInfo

Writes a temporary file containing a list of authentication methods and public credentials (e.g. keys) used to authenticate the user. The location of the file is exposed to the user session through the SSHUSERAUTH environment variable. The default is no.

La configuration du serveur SSH devient alors :

Match user reverse
    ForceCommand reverse
    ExposeAuthInfo yes

La commande reverse pourra alors accéder au contenu de la clef publique correspondant à la clef privée utilisée pour s’authentifier et renvoyer le numéro du port correspondant. Il aurait été préférable de ne pas permettre au compte reverse d’exécuter de commande tout court mais à défaut il faut s’assurer que le script reverse ne permette pas de faire autre chose que ce pourquoi il a été écrit.

Se connecter depuis une machine support

Il est dorénavant possible pour une machine cliente d’ouvrir un reverse tunnel sur le serveur, et à une personne sur le serveur d’identifier de manière certaine quel port correspond à quelle machine cliente. Cependant, comme vu précédemment, les personnes du support ne vont pas se rendre au chevet de la machine serveur dans je ne sais quel datacenter pour s’y connecter directement et atteindre les machines clientes. Si l’on reprend un schéma précédent notre topologie ressemble à cela :

   Réseau privé |            Internet          | Réseau Privé
                |                              |
Client1 |\      |                              |       /| Support1
Client2 |-| Passerelle |---| Serveur |---| Passerelle |-| Support2
Client3 |/      |                              |       \| Support3

Deux solutions pour gérer l’authentification. La première, les machines clientes ont les clefs publiques des machines supports. L’authentification se fait d’abord entre la machine support et le serveur qui ensuite “transmet” l’information d’authentification de la machine support à la seconde connexion ssh avec le client. L’authentification finale se fait entre les deux extrémités en passant par le serveur :

       Réseau privé |            Internet           | Réseau Privé
                    |                               |
                    |                     ________(1)_________
                    |                    /          |         \
Client B.pub |---| Pass. |---| Serveur B.pub |---| Pass. |-|  B.priv Supp.
         \____________________________________________________/
                    |            (2)                |
 __(N)__
/       \ "Vérification" des clefs

Cette solution a le désavantage de devoir déposer sur les machines clientes autant de clefs publiques qu’il existe de machines supports et donc de clef privée. Cela complexifie le déploiement et le maintien du système. Il est possible pour plusieurs machines de partager la même clef privée mais le fait qu’elle soit dupliquée sur autant de machine augmente les chances de la perdre. Perdre la clef privée voudrait dire qu’il faut en créer une nouvelle, la déployer sur toutes les machines support puis déployer la nouvelle clef publique associée sur toutes les machines clientes. C’est possible mais peut-être un peu galère.

La seconde solution est d’authentifier les machines support auprès du serveur puis, à l’aide d’une seconde authentification utilisant une clef privée présente sur le serveur uniquement, se connecter aux machines clientes :

        Réseau privé |               Internet              | Réseau Privé
                     |                                     |
                     |                           ________(1)________
                     |                          /          |        \
Client C.pub |---| Pass. |---| C.priv Serveur B.pub |---| Pass. |-| B.priv Supp
         \_____________________/                           |
                   (2)                                     |

Avec cette solution une seule paire de clef permet in fine de réellement se connecter aux machines clientes et son unique exemplaire se trouve sur le serveur. La perte d’une clef privée du support ne change rien pour les machines clientes, qui sont les plus difficiles à reconfigurer puisque par définition pas toujours accessibles12, et n’implique des changements que sur la partie gérée par le support. Cela dit puisque pouvoir se connecter au compte support du serveur permet ensuite d’utiliser C.priv pour se connecter à n’importe quelle machine cliente il faut tout de même être rapide sur le retrait de B.pub du serveur en cas de fuite ou perte de B.priv. Je propose d’utiliser cette seconde solution mais je n’ai pas d’argument définitif en tête.

Les interfaces

Jusqu’à maintenant les scénarios ont été imaginés avec deux hypothèses en tête :

  1. Les utilisateurices des machines clientes lancent des commandes ssh à la main
  2. Les opérateurices du support se connectent directement au serveur puis lancent des commandes ssh à la main

Il est certainement utile de créer des interfaces, même simples, pour emballer ces deux interactions afin de réduire les erreurs possibles, faciliter leurs prises en main et s’approcher des objectifs vus en introduction. Il est possible, à moindre coût[^6], de produire deux scripts permettant de faciliter les principales opérations.

Être client et ouvrir le tunnel

Côté machine cliente il est possible de créer un script dont les objectifs principaux sont de :

  1. Lancer la première commande ssh permettant de récupérer son port
  2. Lancer la seconde commande ssh pour demander le tunnel
  3. Notifier l’utilisateurice si le tunnel n’a pas pu être ouvert et tenter, même approximativement, de savoir pourquoi
  4. Identifier le processus associé au tunnel
  5. Lancer le serveur SSH
  6. Se connecter à une session tmux tagguée support
  7. Programmer la destruction du processus associé à la fermeture du shell depuis lequel la commande est lancée et la fermeture du serveur SSH lorsque la commande terminera

Ce script pourrait être lancé depuis un terminal ou depuis une icône sur un bureau à l’aide d’un fichier respectant la spécification freedesktop :

[Desktop Entry]
Type=Application
Name=Support
Comment=Donner la main pour le support
Exec=support
Icon=/usr/share/support/favicon.png
Terminal=true

Il suffit ensuite à l’utilisateurice de double cliquer sur l’icône présent sur son bureau. Une fenêtre de terminal s’ouvre avec le script lancé automatiquement . Il reste à à suivre les instructions données par le script. A la fin de son exécution, ce qui ne devrait pas prendre plus de quelques secondes avec une bonne connexion, le script se connecte à une session tmux nommée support. Cette astuce permet, combinée avec une ForceCommand, que l’utilisateurice puisse contrôler ce que la personne du support fait voir taper elle mêmes les commandes. Être à deux sur une session tmux revient en quelque sorte à faire du shell à quatre mains mais avec une seul curseur. En ajoutant dans la configuration du serveur ssh de la machine cliente :

Match user root
    ForceCommand tmux a -t support

On s’assure qu’une fois connecté à la machine cliente le support se retrouve aussi dans la bonne session tmux. On rempli ainsi les conditions de transparence et on provoque même des opportunités pédagogiques !

Être du support, choisir un tunnel puis s’y connecter

En ayant recours une seconde fois à la configuration ForceCommand il est envisageable de restreindre le compte support à une commande implémentant quelques opérations. Ce qui suit est ajouté à la configuration du serveur ssh du serveur :

Match user support
    ForceCommand support

La commande support peut intégrer deux sous-commandes essentielles :

Pour un authorized_keys du compte reverse ayant cette tête :

permitlisten="1312" ssh_ed25519 AAAA... client1
permitlisten="1313" ssh_ed25519 AAAA... client2
permitlisten="1314" ssh_ed25519 AAAA... client3

Le résultat de l’exécution serait :

$ ssh support@serveur ls
client1 1312
client2 1313
client3 1314
$ ssh support@serveur connect client1
machin@client1 $

Conséquence de ce fonctionnement : il faut que le fichier soit lisible à la fois par le compte reverse et le compte support. Il est possible d’implémenter un peu d’intelligence pour détecter les ports ouverts ou pas à l’aide de la commande ss13 et imprimer un résultat sous la forme :

$ ssh support@serveur ls
client1 1312 ouvert
client2 1313 fermé
client3 1314 utilisé

Mais l’implémentation est légèrement embêtante, j’ai un doute sur la capacité à réellement savoir si le tunnel est utilisé et au pire des cas une erreur sera de toute façon obtenue en tentant de s’y connecter :

$ ssh support@serveur connect client1
ssh: connect to host localhost port 1313: Connectio refuse
$ ssh support@serveur connect clientquinexistepas
Bad port ''

C’est un cas assez classique de mauvais message d’erreur mais étant donné que l’on peut raisonnablement partir du principe que les personnes utilisant le compte support sont formées et en contact assez proche de la personne ayant mis ça en place c’est une situation où je pense qu’il est acceptable de ne pas écrire du code pour diagnostiquer exactement ce qu’il se passe. A l’inverse la commande lancée par les clients doit elle être explicite et juste dans ses messages d’erreurs. Évidemment plus l’équipe support utilisant le système est grande et/ou éloignée de personnes comprenant bien son fonctionnement plus les messages d’erreurs côté support auront besoin d’être explicites. Encore une histoire de logiciel situé.

Si l’on veut rendre l’interface encore plus simple on peut utiliser la commande ssh-argv0 :

ssh-argv0 — replaces the old ssh command-name as hostname handling

hostname | user@hostname [-l login_name] [command]

ssh-argv0 replaces the old ssh command-name as hostname handling. If you link to this script with a hostname then executing the link is equivalent to having executed ssh with that hostname as an argument. All other arguments are passed to ssh and will be processed normally.

Traduction : si on créé un lien symbolique portant le nom d’un host ssh vers ssh-argv0 alors exécuter le lien revient à appeler ssh avec ce host en argument. On peut ainsi transformer ssh support@serveur en support en écrivant dans son .ssh/config :

host support
hostname ip_du_serveur
port 22
user support

Et en créant un lien au nom du host support pointant vers ssh-argv0 :

$ sudo ln -s "$(command -v ssh-argv0)" /usr/local/bin/support
$ support ls
client1 1312
client2 1313
client3 1314

Implémentation

Ce dépôt git contient les scripts évoqués dans cet article : http://git.bebou.netlib.re/support/files.html

Il est possible que les deux divergent au fur et à mesure de l’évolution de l’infrastructure mais j’essaierai de synchroniser les deux autant que possible.

Installation

Machine cliente

Match user root
    ForceCommand tmux a -t support

Ces étapes pourraient probablement tenir dans un paquet debian facile à installer via apt. Cela pourrait également servir à mettre à jour la clef C.pub.

Machine support

host support
hostname ip_du_serveur
user support
RequestTTY yes

A défaut d’utiliser un host ssh configuré avec RequestTTY yes il faudra se connecter au serveur avec l’option ssh -t : ssh -t support@serveur ls

Serveur

Sur le serveur il faut :

Match user reverse
    ForceCommand reverse
    AuthorizedKeysFile /chemin/vers/authorized_keys_de_reverse
    ExposeAuthInfo yes
    AllowTcpForwarding yes

Match user support
    ForceCommand support

Si des soucis de connexion sont rencontrés, typiquement des erreurs Permission denied (publickey) bien que les clefs publiques soient au bon endroit, il faut vérifier les logs du serveur SSH. Il est probable que les droits sur les dossiers et fichiers menant à l’authorized_keys ne plaisent pas au serveur, en particulier pour une installation en dehors des homes naturels des comptes reverse et support. Si pour des raisons de practicité on veut tout mettre dans le même dossier c’est possible uniquement en mettant la configuration StrictModes à no dans la configuration du serveur ssh. A noter que ceci diminue la sécurité du serveur. Voir le manuel de sshd_config :

StrictModes

Specifies whether sshd(8) should check file modes and ownership of the user’s files and home directory before accepting login. This is normally desirable because novices sometimes accidentally leave their directory or files world-writable. The default is yes. Note that this does not apply to ChrootDirectory, whose permissions and ownership are checked unconditionally.

Si les comptes reverse et support n’ont volontairement pas de home une erreur indiquant qu’il est impossible de chdir dedans sera récupérée à la connexion ssh. Une manière pas idéale de contourner ce problème qui pourrait polluer la sortie que l’on tente de récupérer (pour le port typiquement) est de mettre à la main un dossier existant en home des comptes dans /etc/passwd. La théorie veut qu’à cause des ForceCommand les comptes peuvent de toute façon rien faire d’autre que ce qu’on veut leur faire faire.

Récapitulatif des clefs

Au cas-où la distribution des clefs ne soit pas claire voici un rapide schéma :

                 Client1 |
compte courant : A1.priv |
          root :  C.pub  |                                                       | Support1
-------------------------|                                                       | compte courant : B1.priv
                 Client2 |  | Serveur                                         |  |-------------------------
compte courant : A2.priv |  | compte reverse : A1.pub, A2.pub, A3.pub         |  | Support2
          root :   C.pub |  | compte support : C.priv, B1.pub, B2.pub, B3.pub |  | compte courant : B2.priv
-------------------------|                                                       |-------------------------
                 Client3 |                                                       | Support3
compte courant : A2.priv |                                                       | compte courant : B3.priv
          root :   C.pub |

Usage

Démonstration sous forme de vidéo de 157Ko (de très mauvaise qualité donc) :

Ici viendra un récapitulatif du fonctionnement technique. En attendant l’article est déjà intéressant alors je le publie tel quel.

Discussion

Cette partie viendra peut-être plus tard à l’occasion d’un éventuel article pour une conférence de La Fabrique de Pensée Critique.


  1. j’ai conscience que ça ne veuille pas dire grand chose seul comme ça 

  2. C pour être raccord avec le nommage des clefs dans la partie installation](/support-ssh/#installation) 

  3. ou plus exactement dupliquées sur tous les réseaux privées. addrpriv1 ne diot être unique que sur son réseau. Un autre réseau privé pour aussi avoir addrpriv1 parce qu’ils ne se recoupent pas. 

  4. ou ses* adresses publiques mais peu importe ici 

  5. On parle d’address dependent filtering ou address and port dependent filtering pour le comportement restrictif très largement utilisé et d’endpoint independent filtering pour le comportement permissif, beaucoup plus rare : https://www.rfc-editor.org/rfc/rfc4787.txt 

  6. du moins par rapport à des interfaces sophistiquées impliquant GUI et/ou de nombreuses fonctionnalités 

  7. Il y a eu une tentative avec la ligne Allocated listen port présente dans les logs de debug du serveur SSH mais 1. c’est pas le plus stable, 2. la ligne en question n’est pas présente dans les logs disponibles depuis journalctl probablement à cause de systemd et 3. le message est tout seul sur sa ligne et il est donc impossible de le rapprocher de manière fiable avec un message d’authentification d’une machine cliente. L’idée vient d’ici : https://superuser.com/questions/304142/how-to-determine-the-port-allocated-on-the-server-for-a-dynamically-bound-openss 

  8. via une bête commande netcat par exemple. 

  9. Matthieu Moy, Guillaume Salagnac et moi 

  10. on peut évidemment cacher ce port mais il est préférable d’imaginer qu’il soit public pour se prémunir des attaques mêmes les moins sophistiquées 

  11. ce que l’on voit ici 

  12. c’est bien le sujet 

  13. oui c’est un nom un peu malheureux