Récemment, j'ai découvert la puissance des clés pour s'identifier sur des services SSH divers, comprenant les shells simples (serveurs où j'aide à l'administration), les shells git (tels que Github, ou les instances Gitlab ou Gitolite), et les autres shells que je nomme "action shells" tels que celui pour l'AUR de Archlinux. Mais on peut trouver ça un peu magique quand, alors que vous vous connectez au même utilisateur que tout le monde (généralement git) avec votre certificat personnel, le shell distant vous identifie. Par exemple :
Hi cakeisalie5! You've successfully authenticated, but GitHub does not provide shell access.
Mais j'ai fini par m'y habituer, et j'en suis venu à vouloir faire mon propre shell d'action, pour pouvoir, entre autres, mettre à jour un site (en récupérant les modifications et en re-construisant le projet Jekyll) avec une commande locale qui ressemble à ssh adm@domain.org update domain.org. J'ai déjà fait un shell interactif (voir Faites un service shell, comme dans les années 80 !), mais maintenant, la subtilité est que je souhaite identifier l'utilisateur à l'aide de la clé publique avec laquelle il se connecte, et prendre la ligne de commande depuis sa ligne de commande originale (dans mon exemple, update domain.org).
Faites un service shell, comme dans les années 80 !
Publié le 14 décembre 2016 par Thomas Touhey.
Avez-vous déjà voulu faire un service shell, juste parce que c'est cool ? Voici comment j'ai fait le mien !
La question "quel serveur SSH utiliserai-je pour ceci" a déjà une réponse, puisqu'il y a un standard de facto pour ça : OpenSSH. C'est un logiciel libre qui fournit tant des clients (ssh, scp, sftp) que le serveur (sshd) et les utilitaires pour celui-ci tels que sftp-server ou ssh-agent. Si vous utilisez une distribution GNU/Linux, vous utilisez OpenSSH comme client, et éventuellement comme serveur.
Ma première piste pour tenter d'identifier l'utilisateur était les variables d'environnement définies par OpenSSH et transmises au shell, telles que SSH_ORIGINAL_COMMAND (que l'on utilisera !), mais rien dedans n'était relatif à l'utilisateur identifié ou à la clé publique qu'il utilisait. Après quelques heures de recherche, j'ai fini par trouver le cœur de la bataille : le fichier authorized_keys.
Dans une configuration d'OpenSSH basique, quand vous tentez de vous connecter à votre compte en utilisant une clé publique, OpenSSH ouvrira votre fichier ~/.ssh/authorized_keys et cherchera la clé publique avec laquelle vous vous identifier. S'il vous trouve dedans, il vous connecte, et sinon, il vous dira probablement Permission denied (publickey). ou quelque chose du même acabit. En réalité, ce fichier peut faire bien plus que cela : pour chaque clé publique, il peut définir quelques propriétés avec lesquelles l'utilisateur va se connecter, comme différentes options d'OpenSSH... ou la commande qui va être exécutée ! Donc la technique consiste à définir la commande avec un argument différent pour chaque clé dans ce fichier, comme par exemple :
command="/opt/ssh-update/shell first-key" ssh-rsa <clé>
command="/opt/ssh-update/shell secnd-key" ssh-rsa <clé>
Ainsi, dans votre shell, vous aurez juste à lire votre premier argument pour lire quel est l'utilisateur ! N'oubliez pas de mettre ceci dans la configuration de sshd, généralement /etc/ssh/sshd_config, afin de n'activer que l'identification par clé (l'utilisateur serait adm dans mon exemple) :
Match User <utilisateur>
PasswordAuthentication no
Et n'oubliez pas de recharger/redémarrer sshd, en utilisant service sshd reload sur Debian, ou plus généralement systemctl reload sshd avec systemd.
Mais sachez qu'il y a une autre technique qui peut être plus pratique. La première marche sur toutes les distributions GNU/Linux récentes, est utilisée par Gitlab, et ne requiert "que" de ré-engendrer le fichier ~/.ssh/authorized_keys à chaque mise à jour de clé publique. Mais la seconde technique, celle que je m'apprête à vous présenter, dépend d'un mécanisme qui n'est disponible et utilisable qu'à partir d'OpenSSH 6.9, et que des distributions répandues et encore maintenues utilisent une version plus ancienne d'OpenSSH, comme Debian Jessie par exemple (l'oldstable de Debian au moment où j'écris cet article), qui utilise OpenSSH 6.8. Cette seconde technique est utilisée par l'AUR d'Archlinux, mais tout projet qui vise à être utilisable sur toutes les distributions GNU/Linux répandues et maintenues devraient dépendre de la première technique.
Cette technique repose sur AuthorizedKeysCommand. Cette option de configuration est présente depuis OpenSSH 6.2, et vous permet de générer un fichier authorized_keys à chaque fois qu'un utilisateur se connecte, avec un utilitaire de votre choix. Cependant, cette option est trop gourmande à grande échelle, puisque cela signifie qu'il faut regénérer des milliers de lignes à chaque fois qu'un utilisateur se connecte ; ne générer le fichier que lorsque c'est nécessaire est une solution bien plus sage. Cependant, en 6.9, une fonctionnalité très pratique a été implémentée : le fait d'utiliser des tokens dans AuthorizedKeysCommand ! Par exemple, vous pouvez définir la valeur de l'option à /opt/ssh-update/auth "%t" "%k", et chaque fois qu'un utilisateur se connecte, le type de la clé et la clé encodée en base64 est envoyée à votre utilitaire d'identification, qui ne produit alors que les lignes d'authorized_keys appropriés ! Ceci signifie que vous n'avez plus besoin de générer statiquement le fichier ~/.ssh/authorized_keys, puisqu'un équivalent sera engendré à chaque connexion !
Pour configurer ceci, nous n'avons qu'à mettre à jour le bloc précédent :
Match User <utilisateur>
PasswordAuthentication no
AuthorizedKeysCommand /opt/ssh-update/auth "%t" "%k"
AuthorizedKeysCommandUser <utilisateur>
Voici un exemple d'/opt/ssh-update/auth, codé en Python :
#!/usr/bin/env python3
import sys
cake_key = "..."
if sys.argv[1] != "ssh-rsa" or sys.argv[2] != cake_key:
exit(1)
print('command="/opt/ssh-update/shell cake" ssh-rsa %s cake@thing'%cake_key)
Et voici un exemple d'/opt/ssh-update/shell, tout autant codé en Python :
#!/usr/bin/env python3
import os, sys
print("Hello, %s!"%sys.argv[1])
if not 'SSH_ORIGINAL_COMMAND' in os.environ:
print("No command?")
else:
print("Your command was: %s"%os.environ['SSH_ORIGINAL_COMMAND'])
Bien entendu, vous pouvez faire beaucoup de choses avec ceci :
Faire un utilitaire qui génère statiquement ~/.ssh/authorized_keys à partir d'une base de données (si vous utilisez la première technique) ;
Récupérer le pseudonyme de l'utilisateur à partir d'une clé publique en faisant appel à une base de données (si vous utilisez la seconde technique) ;
Implémenter les commandes SSH pour git git-receive-pack et git-upload-pack ;
La seule limite est votre imagination !