La problématique : se connecter entre ses différents machines, en ssh et sans mot de passe

Lorsqu'on commence à avoir des machines dans tous les coins on aime pouvoir y accèder facilement, et de partout.

Souvent on se retrouve avec un petit réseau local contenant plusieurs machines partageant un seul point d'accès à internet. C'est à partir de cette IP publique que l'on peut se connecter à son réseau interne de l'extérieur.

Le problème c'est que tant qu'IPv6 ne sera pas établi (et que ou l'on pourra disposer de plusieurs adresses publiques sans sourciller) on en est réduit à une seule adresse et on se retrouve avec une machine qui sert de passerelle pour rebondir dans son réseau interne.

Machine externe -> Passerelle -> Machine interne

Une solution basique : un jeu de clés privée / publique

Imaginons que l'on souhaite se connecter depuis la passerelle sur la machine interne. On va générer un jeu de clé :

$ ssh-keygen -t dsa

On peut utiliser le fichier par défaut, par contre il faut mettre une passphrase. Une fois notre paire de clés générée on va copier notre clé publique sur la machine sur laquelle on souhaite se connecter :

$ ssh-copy-id -i ~/.ssh/key.pub login@interne

Cela aura pour effet d'aller copier notre clé publique dans le fichier authorized_keys de la machine interne. Dès lors quand on voudra se connecter sur la machine interne depuis la passerelle c'est la passphrase qui sera demandée.

À ce niveau on peut penser avoir juste déplacé le problème : toujours un mot de passe à taper.

Toutefois, pour ne pas avoir à retaper sa passphrase à chaque connexion on peut utiliser ssh-agent.
Pour cela on rattache ssh-agent à un processus sur la passerelle, par exemple ssh-agent /bin/bash.

Cela aura pour effet de lancer un nouveau shell auquel sera rattaché l'agent.
Ensuite on peut rajouter les clés privées à gérer par notre agent : ssh-add ~/.ssh/id_dsa.

ssh-add va nous demander la passphrase, une fois entrée c'est lui qui interceptera les demandes de passphrases de ssh.

Dès lors je peux me connecter de ma passerelle vers ma machine interne sans mot de passe.

Aller un peu plus loin : utiliser le même agent pour faciliter les rebonds ssh

Soit, mais si je connecte depuis la machine externe vers la passerelle, et que depuis la passerelle je souhaite rouvrir une connexion vers la machine interne ma passphrase sera re-demandée.

Pourquoi ? Parce que notre agent est associé à notre shell (ssh-agent /bin/bash). En dehors de ce shell le problème est le même.

Si on y regarde de plus près, comment cela fonctionne t-il vraiment ?

Au lancement du ssh-agent une socket est créée dans le /tmp, du genre /tmp/ssh-xxxpiddushell/agent.piddushell
En fait le ssh-agent est un processus fils du shell nouvellement créé. En plus de ça notre commande génére plusieurs variables d'environnements qui ne sont accessibles qu'à l'intérieur du nouveau shell.

Regardons ça de plus près :

$ env | grep -i ssh

Deux variables nous intéressent :

SSH_AGENT_PID=22659
SSH_AUTH_SOCK=/tmp/ssh-WgkOE22658/agent.22658

Récupérer l'environnement de notre agent, et l'exporter dans le nouveau shell

On le voit ces informations sont facilement accessibles. Partant de là si on se connecte depuis la machine externe vers la passerelle et que l'on fait un export de ces variables dans le nouveau shell alloué lors de notre connexion ssh on peut parfaitement se reconnecter sur la machine interne sans taper de mot de passe :

$ export SSH_AGENT_PID=22659
$ export SSH_AUTH_SOCK=/tmp/ssh-WgkOE22658/agent.22658
$ ssh interne

L'idée c'est d'alouer automatiquement ces variables, en réalisant l'export ... dans notre .bashrc
En fait il y a deux façons de faire : soit on force le nom de la socket, avec un ssh-agent -a, soit on récupère les informations dynamiquement.

Je penche pour la deuxième qui m'évite de penser à passer cette option lorsque que je lance mon agent (et oui, geek == fainéant). En plus il faudra toujours récupérer l'id du pid.

Pour ça deux commandes à mettre dans le .bashrc :

export SSH_AGENT_PID=$(ps aux | grep -v 'grep' | grep ssh-agent | grep $(env | grep "USER" | sed s/USER=// | head -n 1) | head -n 1 | awk '{print $2}')

if [ "" != "$SSH_AGENT_PID" ]; then
  export SSH_AUTH_SOCK=$(find /tmp -iname "agent.$(ps -p $SSH_AGENT_PID -o ppid=)")
fi

Explications :


  • ps aux liste les processus
  • grep -v fait une recherche inverse c'est à dire qu'il exclut les résultats demandés. En l'occurence je demande à grep de s'exclure lui même, sinon la commande apparait dans la liste des résultats
  • grep ssh-agent fait ce qu'on lui demande, il recherche les lignes qui matchent.
  • grep $(env | grep "USER" | sed s/USER=// | head -n 1) fait un grep sur le nom de l'utilisateur. En effet il peut y avoir plusieurs agents sur la machine, appartenant à des utilisateurs différents.
  • head -n 1 renvoie la première ligne uniquement
  • awk '{print $2}' permet de splitter la ligne selon les espaces ou tabulations. Ici on renvoie le deuxième bloc qui contient le pid. On pourrait aussi utiliser cut.
if [ "" != "$SSH_AGENT_PID" ]; then
        export SSH_AUTH_SOCK=$(find /tmp -iname "agent.$(ps -p $SSH_AGENT_PID -o ppid=)")
fi

Explications :

Cette commande recherche la socket correspond à ce ssh-agent s'il existe (présence du pid).
La socket est nommée dans un format spécial : agent.pidduparentdel'agent ; il nous faut donc récupérer le pid parent du pid de l'agent, celui du shell donc.
C'est ce que réalise cette commande : ps -p $SSH_AGENT_PID -o ppid=

Ensuite c'est un find tout bête qui nous renvoie le full path de la socket.

Une fois ces commandes dans votre .bashrc c'est fini, lors de la connexion de externe vers passerelle, un shell est alloué, le bashrc sourcé et les variables d'environnements disponibles, on peut donc se connecter à la machine interne sans soucis.

Bien sûr pour faciliter le tout on peut utiliser un jeu de clés entre la machine externe et la passerelle.

Une pincée de sécurité en plus : empêcher les connexions ssh par mot de passe

Une fois tout ça en place pourquoi ne pas désactiver l'authentification par password ? Dans le fichier /etc/ssh/sshd_config :

PasswordAuthentication no

et au passage :

PermitRootLogin no

Ainsi on est sûr que seules les machines dont la clé publique se trouve sur la machine hôte pourront se connecter.
Toutefois il vaut mieux avoir au moins 3 machines pour utiliser ce système, ou conserver ses clés en lieu sûr, sinon au premier plantage votre machine distante devient inaccessible.
Dommage quand on n'a pas d'accès physique à celle-ci ...