Retour de terrain Publié le · 4 min de lecture

fail2ban nous a bannis de tout le parc, et il avait raison

Un agent SSH chargé de plusieurs clés épuise MaxAuthTries avant d'avoir proposé la bonne. En exécution parallèle, chaque nœud vous bannit simultanément.

Nos serveurs sont durcis selon le référentiel CIS. Deux des contrôles appliqués sont parfaitement banals, et se retrouvent sur à peu près toutes les infrastructures sérieuses :

MaxAuthTries 4          # sshd : nombre d'essais d'authentification par connexion
maxretry  = 5           # fail2ban : connexions échouées avant bannissement
findtime  = 600         # ... comptées sur 10 minutes
bantime   = 3600        # ... bannissement d'une heure

Rien à redire sur ces valeurs. Elles ont pourtant fini par nous verrouiller dehors, sur l’ensemble du parc en même temps, sans qu’aucun mot de passe ne soit faux et sans qu’aucune clé ne soit invalide.

Ce que fait vraiment un agent SSH

Le point de départ est un comportement du client, pas du serveur, et c’est ce qui rend le diagnostic contre-intuitif.

Quand un agent SSH détient plusieurs clés, le client ne sait pas laquelle le serveur acceptera. Il les propose donc une par une, dans l’ordre où l’agent les détient, jusqu’à ce que l’une soit reconnue.

Or le serveur ne voit pas une négociation, il voit une série de tentatives d’authentification. Chacune consomme une unité de MaxAuthTries. Avec la valeur 4 et six clés chargées dans l’agent, les quatre premières clés proposées épuisent le quota, et le serveur coupe la connexion avant que la bonne clé soit présentée.

Le message renvoyé au client est Too many authentication failures. Il est exact et parfaitement trompeur : il n’y a pas d’échec d’authentification au sens où on l’entend. La bonne clé est là, dans l’agent, et n’a simplement jamais été proposée.

Comment cela devient un bannissement de tout le parc

Une connexion coupée compte, du point de vue de fail2ban, comme une connexion échouée. Cinq en dix minutes déclenchent le bannissement de l’adresse source pour une heure.

Le facteur d’échelle est l’exécution parallèle. Un playbook Ansible lancé avec plusieurs processus n’ouvre pas une connexion, il en ouvre autant qu’il vise de machines, simultanément, depuis la même adresse source. Chaque serveur applique alors son propre compteur, indépendamment des autres. Ils sont durcis de façon identique, ils réagissent de façon identique, et ils bannissent tous la même adresse en même temps.

Le résultat est particulier : la panne n’est pas progressive, elle est totale et instantanée. On perd l’accès à l’ensemble du parc d’un coup, y compris au bastion par lequel on aurait pu se rattraper.

Et il faut le dire clairement : fail2ban a fait exactement ce pour quoi il est installé. Une adresse unique produisant des dizaines de tentatives d’authentification échouées sur des dizaines de machines en quelques secondes est une signature d’attaque par force brute distribuée. Le contrôle a fonctionné. Il a simplement identifié le bon motif chez le mauvais acteur.

La réaction à ne pas avoir

La tentation immédiate est d’augmenter MaxAuthTries, ou d’allonger findtime, ou de mettre son adresse en liste blanche dans fail2ban.

Les deux premières affaiblissent réellement la protection : elles augmentent le nombre d’essais qu’un attaquant obtient par connexion, ou la fenêtre dans laquelle il peut se répartir. La troisième désarme le mécanisme pour l’adresse la plus sensible du parc, celle depuis laquelle on administre tout.

Ce sont trois façons de traiter le symptôme, et elles ont un point commun : elles supposent que le serveur a tort. Le serveur n’a pas tort.

Le correctif est côté client

Le défaut est que le client propose des clés non pertinentes. La correction consiste à lui interdire de le faire :

IdentitiesOnly=yes

Cette option indique à SSH de n’utiliser que la clé explicitement désignée, et d’ignorer tout ce que l’agent propose par ailleurs. Une seule tentative d’authentification atteint le serveur, elle est la bonne, et MaxAuthTries n’est jamais approché.

Un détail qui compte en pratique : l’option doit être posée sur chaque saut. Une connexion qui rebondit par un bastion ouvre deux sessions SSH successives, et la seconde repart de zéro avec l’agent complet si l’on ne le contraint pas :

ProxyCommand ssh -i ~/.ssh/cle -o IdentitiesOnly=yes -W %h:%p user@bastion

Corriger seulement la connexion finale laisse le bastion se faire bannir, ce qui produit exactement la même panne avec un composant de moins pour la diagnostiquer.

Ce qu’on en retient

  1. Un agent SSH chargé est un générateur d’échecs d’authentification. Plus il contient de clés, plus vite le quota du serveur est atteint. Le confort du poste de travail devient un problème de production.
  2. Too many authentication failures ne veut pas dire que la clé est mauvaise. Il veut dire que le serveur a cessé d’écouter avant de la voir.
  3. Le parallélisme transforme une gêne en panne totale. Un contrôle par nœud appliqué de façon homogène produit un bannissement homogène, bastion compris.
  4. IdentitiesOnly=yes sur tous les sauts, et non sur le seul saut final.
  5. Un contrôle de sécurité qui vous bloque n’est pas nécessairement mal réglé. Celui-ci a correctement reconnu un motif d’attaque. Le défaut était dans notre façon de nous connecter, pas dans son seuil.

Le cinquième point est celui qui coûte le plus cher à apprendre, parce que le réflexe sous pression, quand on est enfermé dehors, est d’assouplir le contrôle qui vous enferme.

sshfail2bandurcissementansible