NullPointerException

Blog d’un groupe crypto-terroriste individuel auto-radicalisé sur l’Internet digital

SSH-Ident : gérez proprement vos agents SSH

Du (mauvais) usage de l’agent SSH et du transfert d’agent

Agent SSH et transfert d’agent

Si comme moi vous êtes admin sys, vous utilisez forcément SSH au quotidien pour gérer votre parc (sauf s’il est sous Windows, auquel cas recevez toutes mes condoléances…). Et si vous êtes un vrai admin sys, vous utilisez aussi forcément une clef SSH pour vous connecter plutôt qu’un mot de passe. Sauf qu’il est plus que pénible d’avoir à saisir sa phrase de passe avant chaque connexion à une machine, et donc il est plus que probable que vous utilisiez aussi un agent SSH pour charger vos clefs une bonne fois pour toutes à la première connexion.

Pour les personnes utilisant juste SSH pour le fun ou 2-3 trucs persos, cette solution est relativement sécurisée et ne posera pas les soucis exposés ci-après. Par contre, sur des parcs plus complexes, il y a fort à parier que vous utilisiez une autre fonctionnalité de l’agent SSH : le transfert d’agent (agent forwarding dans le texte). En effet, on a bien souvent certaines machines qui ne sont pas directement accessibles sur Internet (les machines virtuelles généralement), mais nécessitent une machine de rebond (bounce) pour y parvenir. Dans les grands parcs, il n’est même pas rare d’avoir une unique machine exposée dans la zone démilitarisée (DMZ), toutes les autres étant bien à l’abri derrière les pare-feux. Avec l’agent forward, notre agent SSH va se propager au travers du réseau via les machines auxquelles on se connecte, permettant ainsi de se connecter aux machines finales sans mot de passe et sans avoir à copier notre clef SSH sur chaque machine intermédiaire. La plupart du temps, l’admin sys étant un fainéant notoire, il a tendance à activer l’agent-forwarding par défaut pour ne pas avoir à se préoccuper du chemin à suivre pour joindre une machine et ainsi pouvoir accéder à n’importe quelle machine du parc depuis n’importe quelle autre.

Une grosse faille de sécurité si on cumule les deux

Les deux fonctionnalités (ssh-agent + agent-forward) cumulées peuvent avoir un impact très important sur la sécurité de vos clefs SSH. Votre agent SSH local charge toutes vos clefs SSH au démarrage, et se chargera de les proposer à votre client SSH quand il en aura besoin. Pour la communication avec le client SSH, l’agent met à disposition un socket UNIX de la forme /tmp/ssh-<truc random>/agent.<pid de l’agent> et le publie dans la variable d’environnement SSH_AUTH_SOCK. Lorsqu’on fait du transfert d’agent, ce socket va se propager sur chaque machine intermédiaire pour assurer la communication avec le client SSH, toujours via la variable SSH_AUTH_SOCK.

Et c’est là que quelque chose de terrible peut se produire… Le transfert d’agent crée aussi un socket UNIX sur la machine intermédiaire, socket qui est un simple fichier quelque part sur le disque. Et donc tout utilisateur capable de lire ce fichier peut communiquer avec votre agent SSH transféré ! Toute personne connectée sous le même utilisateur SSH que vous peut le faire. L’utilisateur root peut le faire. L’administrateur de l’hôte physique hébergeant votre machine virtuelle peut le faire (cas des VPS, mutualisés, etc). Pour exploiter ce socket, il suffit à un attaquant de trouver le socket (un simple ls /tmp/ssh-* lui les listera tous) et de définir manuellement la variable d’environnement SSH_AUTH_SOCK pour utiliser votre agent SSH…

Là où ça pique vraiment, c’est que votre agent SSH de départ connaît toutes vos clefs. Pas seulement celle que vous avez réellement utilisée pour votre chaîne de connexion. Toutes. Si vous avez par exemple une clef A pour vos machines persos et une clef B pour vos machines pros, que les deux sont chargées dans l’agent que vous avez exporté, votre collègue connecté en même temps peut alors se connecter à toutes vos machines perso ! On pousse encore un peu la parano ? Vous utilisez GitHub très certainement, et avec une clef SSH pour éviter d’avoir à saisir votre mot de passe à chaque push/fetch. Vous avez un agent SSH et vous avez activé le transfert d’agent par défaut ? Bingo, à chacune de vos connexions, GitHub peut se connecter à l’ensemble de vos machines, y déployer sa propre clef SSH (dans /root/.ssh/authorized_keys2, il est aussi pris en compte par SSH et peu d’admin penseront à vérifier ce fichier :P) et ainsi monter un botnet géant constitué de l’intégralité du parc informatique de tous ses utilisateurs.

Une petite démo de l’utilisation possible :

alice@home  $ eval $(ssh-agent)
alice@home  $ ssh-add ~/.ssh/id_ed25519_perso
alice@home  $ ssh-add ~/.ssh/id_ed25519_pro

alice@home  $ ssh alice@perso
alice@perso $ logout 

alice@home  $ ssh alice@pro -A
alice@pro   $ echo $SSH_AUTH_SOCK
SSH_AUTH_SOCK=/tmp/ssh-HVo7YpeUUH/agent.4245
alice@pro   $ (do a long stuff)

(Meanwhile)
malory@home $ ssh root@pro
root@pro    # echo $SSH_AUTH_SOCK

root@pro    # ssh alice@perso
Permission denied (publickey).

root@pro    # find /tmp -path /tmp/ssh-*/agent.*
/tmp/ssh-HVo7YpeUUH/agent.4245
root@pro    # ls -al /tmp/ssh-HVo7YpeUUH/agent.4245
srwxr-xr-x 1 alice alice 0 Jul 22 20:08 /tmp/ssh-HVo7YpeUUH/agent.4245
root@pro    # export SSH_AUTH_SOCK=/tmp/ssh-HVo7YpeUUH/agent.4245

root@pro    # ssh alice@perso
alice@perso $ echo Powned | write alice

Du (bon) usage de l’agent SSH et du transfert d’agent

Maintenant qu’on a vu comment on pouvait exploiter l’agent SSH des autres, voyons comment on peut se prémunir de ce genre de problème.

Ne pas utiliser de transfert d’agent SSH

La solution la plus simple est de se passer tout simplement du transfert d’agent.

On peut conserver les possibilités de rebond via l’option ProxyCommand et -W de SSH. Par exemple pour joindre bar via foo, vous pouvez mettre dans votre fichier ~/.ssh/config :

Host bar
	ProxyCommand ssh -W %h:%p foo

L’idée est d’utiliser les possibilités de transfert de connexion TCP de SSH pour se connecter d’abord à foo, puis de raccrocher un tunnel TCP depuis bar vers votre machine locale via foo et de se connecter localement en SSH à ce tunnel, donc en utilisant l’agent local.

En pratique, sur des infrastructures un peu complexe, cette possibilité est très vite limitante puisque ça devient très vite l’enfer si on doit passer par plus d’une machine intermédiaire et qu’en plus on ne peut plus joindre les machines finales à partir de n’importe quelle autre machine (il faut obligatoirement revenir à la machine locale).

Limiter le transfert d’agent à la seule clef utilisée

L’option IdentitiesOnly existe dans SSH pour limiter les clefs possibles pour un agent SSH. Si vous voulez donc l’activer par défaut, il faut ajouter à votre ~/.ssh/config :

Host *
	IdentitiesOnly yes

Vos chaînes de connexion doivent du coup utiliser la même clef SSH à chaque rebond, ce qui peut parfois être problématique (par exemple vos frontaux sécurisés n’acceptent que les clefs ED22519 quand vos vieilles machines finales en sont restées à RSA-4096).

Cloisonner les clefs SSH via des agents multiples

L’un des gros problèmes de la « faille » de l’agent SSH est surtout que l’agent expose l’ensemble de vos clefs SSH, et non pas uniquement celles concernées par la connexion établie. À la limite, si GitHub ne pouvait avoir accès qu’à ses propres machines, vous aux vôtres et vos collègues à celles de votre parc professionnel, il n’y aurait plus vraiment de problème.

L’idéal serait donc d’avoir un agent SSH par groupe de connexion (pro, perso, dev, git…), chacun chargé uniquement des clefs qui vont bien. Ça aurait en plus l’énorme avantage d’alléger le nombre de clefs par agent, et donc d’accélérer les connexions, l’agent testant les clefs disponibles les unes après les autres jusqu’à parvenir à se connecter (ce qui peut en plus causer des problèmes si vous avez trop de clefs qui échouent ou un fail2ban en face).

De manière générale, le cloisement d’identité n’est jamais une mauvaise idée en termes de sécurité, et permet par exemple d’éviter ceci.

SSH ne gère nativement pas cette séparation, il va donc falloir ruser un peu.

SSH-Ident a été développé initialement par Carlo Contavalli et j’ai corrigé 2 ou 3 trucs dessus (support de SSH_ASKPASS, suppression des invocations bash…). Il remplace SSH et se base sur les options passées à SSH pour déterminer à quelle identité raccrocher la connexion. Chaque identité se trouve isoler dans un sous-répertoire de ~/.ssh/identities/, avec son fichier de configuration, ses clefs SSH propres, son agent SSH…

Pour l’installer et l’utiliser, suivez la doc.

Personnellement, j’utilise 4 identités :

  • personal, pour mes machines personnelles (sous mon seul contrôle),
  • pro, pour mes machines professionnelles,
  • dev, pour toutes mes machines de développement (donc des clefs privées qui peuvent facilement finir dans la nature sur un commit, un copié-collé ou une release malencontreux),
  • public, pour les serveurs tiers dont je n’ai pas le contrôle (GitHub).

rsync et autres outils liés à SSH ne posent pas de soucis particuliers avec SSH-Ident. Je rencontre uniquement des problèmes avec scp, qui cherche à invoquer ssh via un chemin en dur et non via le mécanisme du PATH, ainsi que sur la machine distante ce qui pose des problèmes si SSH-Ident est aussi déployé là-bas. Investigations en cours !

Bonus : Concierge

Avec SSH-Ident, vous allez perdre la complétion automatique sur votre invocation SSH, puisqu’elle ne tient compte que du fichier ~/.ssh/config, qui n’est plus utilisé ici. Et au passage, c’est déjà assez chiant à rédiger un fichier de config SSH, mais là en plus, on se retrouve avec plusieurs…

Vous pouvez donc utiliser Concierge, un moteur de template de fichiers SSH, qui permet d’en écrire des plus concis et compréhensible que ceux par défaut (par exemple la gestion horrible du paramétrage par défaut qui nécessite des Host * en fin de fichier…).

Et pour restaurer la complétion automatique, générez automatiquement votre ~/.ssh/config avec tous les hôtes trouvés dans vos fichiers d’identité.

Pour automatiser le tout, faites un ~/.ssh/Makefile que vous invoquerez avec un petit make -C ~/.ssh !

.DEFAULT_GOAL := config
MAKEFLAGS += --no-builtin-rules

TEMPLATES := $(wildcard $(HOME)/.ssh/identities/*/config.tpl)
CONFIGS := $(subst .tpl,,$(TEMPLATES))

%: %.tpl
	"$(WORKON_HOME)/concierge/bin/concierge-check" -u mako -s "$<" -o "$@"

$(HOME)/.ssh/config: $(CONFIGS)
	grep -h "^Host" $^ | grep -v "*" > $@
config: $(HOME)/.ssh/config

clean:
	rm -f $(HOME)/.ssh/config $(CONFIGS)

Comments !