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 :
- permette plusieurs connexions transparentes, consenties et révocables en simultané
- permette au ou à la client·e d’auditer ce qui est fait sur sa machine
- tende à être simple techniquement1
- soit raisonnablement facile à expliquer et comprendre, en particulier au regard des connaissances des employées de l’entreprise
- soit simple à utiliser, par les client·es et par le support
- n’utilise que du logiciel libre
- puisse être auto-hebergé
- soit sécurisé : seul le support peut se connecter aux machines clientes et ce n’est possible que lorsqu’un·e client·e le permet
- soit gérable pour plusieurs dizaines voir centaines de postes
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
-o 'ExitOnForwardFailure'permet à la commande de terminer si le reverse tunnel ne peut pas être créé-fpermet de mettre la commande en arrière plan-Npermet de ne pas exécuter de commande sur le serveur distant (on ne veut que produire le reverse tunnel)-Tpermet de ne pas obtenir de pseudo-TTY (pas besoin puisque pas de commande)-R '1312:localhost:22'veut dire “je veux que le port 1312 du serveur écoute en local sur le serveur et soit branché sur le port 22 du client”. “Ecoute en local” veut dire qu’il n’est possible de se connecter au port 1312 du serveur que si l’on se trouve déjà sur le serveur. On ne peut pas s’y connecter depuis une autre machine. Fairessh root@serveur -p 1312ne fonctionnera pas.
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 à :
- Éviter que deux machines clientes tentent d’utiliser le même port
- 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 :
- Les utilisateurices des machines clientes lancent des commandes
sshà la main - 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 :
- Lancer la première commande
sshpermettant de récupérer son port - Lancer la seconde commande
sshpour demander le tunnel - Notifier l’utilisateurice si le tunnel n’a pas pu être ouvert et tenter, même approximativement, de savoir pourquoi
- Identifier le processus associé au tunnel
- Lancer le serveur SSH
- Se connecter à une session tmux tagguée support
- 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 :
support lsqui consulte l’authorized_keysdereverseet imprime la listesupport connectqui prend en argument un commentaire d’une des clefs publiques disponibles
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
- Installer
openssh-client,openssh-serverettmux - Créer une paire de clef ssh A pour le compte courant
- Rendre accessible la commande
supportau compte courant - Modifier la commande
supportpour y inclure l’adresse IP du serveur - Installer sur le bureau le fichier permettant l’exécution du script
support - Créer un
authorized_keyspour le compte root et y copier la clef publique C.pub - Ajouter dans la configuration du serveur ssh :
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
- Installer
openssh-client - Générer une paire de clef ssh B pour le compte courant
- Optionnels :
- Créer et rendre accessible un lien symbolique vers
ssh-argv0avec le nomsupport - Ajouter le host suivant dans le
.ssh/configdu compte courant. Le nom du host et le nom du lien symbolique doivent correspondre :
- Créer et rendre accessible un lien symbolique vers
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 :
- Installer
openssh-clientetopenssh-server - Créer un compte
support:adduser ... - Créer un compte
reverse:adduser ... - Donner aux deux comptes un shell de login viable sans quoi ils ne pourront pas exécuter de commandes.
- Rendre accessible au compte
reversela commandereverse - Rendre accessible au compte
supportla commandesupport - Modifier la commande
supportpour y insérer le bon chemin vers l’authorized_keysdu comptereverse - Créer un
authorized_keyspour le comptereverselisible par les comptesreverseetsupportet y copier les clefs publiques A.pub des machines clientes avec en face l’optionpermitlisten=et un port unique par clef. Le fichier doit appartenir àreverseet être accessible en lecture et écriture àreverse. - Créer un
authorized_keyspour le comptesupportet y copier les clefs publiques B.pub des machines du support dedans - Créer une paire de clef ssh C pour le compte
support - Ajouter dans la configuration du serveur ssh :
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.
-
j’ai conscience que ça ne veuille pas dire grand chose seul comme ça ↩
-
C pour être raccord avec le nommage des clefs dans la partie installation](/support-ssh/#installation) ↩
-
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. ↩
-
ou ses* adresses publiques mais peu importe ici ↩
-
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 ↩
-
du moins par rapport à des interfaces sophistiquées impliquant GUI et/ou de nombreuses fonctionnalités ↩
-
Il y a eu une tentative avec la ligne
Allocated listen portpré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 depuisjournalctlprobablement à cause desystemdet 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 ↩ -
via une bête commande netcat par exemple. ↩
-
Matthieu Moy, Guillaume Salagnac et moi ↩
-
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 ↩
-
c’est bien le sujet ↩
-
oui c’est un nom un peu malheureux ↩