ssh-guard : détection d'attaques SSH

Ce que vous allez comprendre

  • Identifier les quatre motifs d'attaque détectés automatiquement et les seuils par défaut.
  • Analyser comment ssh-guard décide de bannir une IP, dans quel ordre les règles s'appliquent et pourquoi.
  • Évaluer les compromis (fail-open, dry-run, allowlist bypass) pour ajuster la posture de sécurité de votre instance.

Public : administrateurs gitrust qui veulent comprendre avant de toucher aux variables SSH_GUARD_*. Pour la recette pas à pas, voir Configurer ssh-guard.


1. Le problème concret

Vous administrez une instance gitrust exposée sur Internet. Les logs russh remontent des dizaines de tentatives d'authentification ratées chaque jour, sans corrélation. Vous savez intuitivement qu'il y a une attaque par dictionnaire en cours, mais :

  • vous ne savez pas combien d'IP distinctes la mènent ;
  • vous ne pouvez pas distinguer un développeur qui a 3 clés mal configurées d'un scanner qui essaie 50 clés en série ;
  • votre fail2ban actuel se base sur des regex sur les messages d'erreur de russh, qui changent à chaque mise à jour ;
  • vous craignez de bannir un partenaire CI qui ouvre 200 connexions SSH par minute pour ses pipelines.

ssh-guard a été conçu exactement pour ce périmètre : observer, classer, corréler, bannir — sans casser les utilisations légitimes intensives.


2. L'analogie

Imaginez le hall d'entrée d'un immeuble. Quatre comportements doivent déclencher un signal pour le gardien :

  1. Quelqu'un sonne 5 fois en 5 minutes au même appartement → c'est une tentative de force brute. (brute-force)
  2. Quelqu'un sonne aux 10 appartements différents en 5 minutes → il cherche qui est chez lui pour cibler ensuite. (énumération d'utilisateurs)
  3. Quelqu'un présente 10 cartes magnétiques différentes en 5 minutes → il essaie un trousseau volé. (scanning de clés)
  4. Quelqu'un ouvre la porte 30 fois en 1 seconde → il sature le système, peu importe ses intentions. (flood TCP)

ssh-guard distingue les quatre, parce qu'ils appellent des réponses différentes. Le flood est un drop instantané (sinon le système se noie). Les trois autres déclenchent un ban temporaire après un nombre d'essais avéré.


3. Les quatre détecteurs

3.1 Vue synthétique

Détecteur Mesure Seuil défaut Fenêtre Action
Brute-force Échecs d'auth depuis une IP 5 5 min Ban 1 h
Énumération d'utilisateurs Usernames distincts essayés depuis une IP 10 5 min Ban 1 h
Scan de clés Fingerprints distincts essayés depuis une IP 10 5 min Ban 1 h
Flood TCP Nouvelles connexions TCP par IP par seconde 10 (burst 20) 1 s glissante Drop immédiat (pas de ban persistant)

Tous les seuils sont ajustables via SSH_GUARD_* — voir Variables d'environnement.

3.2 Brute-force

Compte les événements auth_failed par IP dans la fenêtre glissante. Au seuil, émet brute_force_detected puis pose un ban auto avec TTL SSH_GUARD_AUTO_BAN_DURATION_SECS. Ne compte pas les auth_succeeded (c'est l'objectif : ignorer le bruit légitime).

3.3 Énumération d'utilisateurs

Compte les usernames distincts essayés par IP. Un attaquant qui sonde root, admin, git, deploy, ci, bot, … pour trouver un compte existant déclenche ce détecteur même si chacune de ses tentatives n'est faite qu'une fois.

Cas typiques de faux positif : un script CI qui clone plusieurs dépôts d'orgs différentes avec différents git@host:org/... — mais le user SSH reste git, donc le détecteur ne s'active pas. Un développeur qui se trompe trois fois de username en jouant avec son ~/.ssh/config reste largement sous le seuil.

3.4 Scan de clés

Compte les fingerprints SHA256 distincts présentés par IP. Aligné sur les recommandations CrowdSec : un humain n'a jamais 10 clés SSH distinctes en 5 minutes. Un trousseau volé qui défile en automatique, oui.

Cas typique d'observation : ssh-add -L côté client liste 8 clés, l'agent SSH les présente toutes successivement. Le seuil par défaut (10) absorbe ce cas.

3.5 Flood TCP

Implémenté avec un token bucket GCRA (algorithme de Generic Cell Rate, équivalent leaky bucket sans biais), keyé par IP. Différent des trois autres :

  • Pas de ban persistant : sinon un scan de port créerait des milliers de lignes inutiles dans la DB.
  • Drop au niveau du listener TCP : la connexion ne consomme aucune ressource au-delà du accept().
  • Budgets indépendants par IP : une IP rate-limitée n'affecte pas les autres.

C'est la première ligne de défense, peu coûteuse, qui absorbe les scans avant que les détecteurs comportementaux ne soient sollicités.


4. Le modèle de décision

4.1 Priorité des règles d'ACL

Quand une connexion arrive, ssh-guard consulte dans cet ordre :

flowchart LR
    A[Connexion] --> B{Denylist admin ?}
    B -- Oui --> X1[Drop banned]
    B -- Non --> C{Ban auto actif ?}
    C -- Oui --> X1
    C -- Non --> D{Allowlist admin ?}
    D -- Oui --> Y[Accept,
bypass détecteurs] D -- Non --> E{Flood OK ?} E -- Non --> X2[Drop flood_limit] E -- Oui --> Z[Accept normal]

Conséquences pratiques :

  • deny admin > tout : un opérateur peut bannir une IP même si elle a été allowlistée par erreur ailleurs.
  • auto_ban > allow : un ban posé par un détecteur ne saute pas en allowlistant l'IP après coup. Pour libérer l'IP, il faut explicitement la débannir. C'est volontaire — un allow ne doit pas effacer la trace d'un comportement passé.
  • allow > default : une IP allowlistée bypasse les détecteurs et le flood-limit, mais ses événements auth_succeeded/auth_failed restent loggés pour audit.

4.2 Cycle de vie d'un ban brute-force

sequenceDiagram
    participant C as Client SSH (203.0.113.42)
    participant L as SecureListener
    participant T as AuthTracker
    participant D as BruteForceDetector
    participant B as BanManager
    participant S as Sink JSON
    participant F as Fail2ban

    Note over C,F: Tentatives 1 à 4 (sous le seuil)
    C->>L: TCP accept
    L->>S: connection_accepted
    C->>T: auth fail (russh)
    T->>S: auth_failed
    T->>D: on_event(auth_failed)
    D->>D: count = 4 < seuil 5
    Note over D: Aucun ban

    Note over C,F: Tentative 5 (atteint le seuil)
    C->>T: auth fail (russh)
    T->>S: auth_failed
    T->>D: on_event(auth_failed)
    D->>D: count = 5 >= seuil
    D->>S: brute_force_detected
    D->>B: auto_ban(BruteForce)
    B->>S: ip_banned (expires_at = now + 1h)
    F->>F: Lit ip_banned, applique iptables/UFW

    Note over C,F: Tentative 6 (bannie)
    C->>L: TCP accept
    L->>B: effective_status(203.0.113.42)
    B-->>L: BannedAuto
    L->>S: connection_dropped (banned)
    L-->>C: connexion fermée

4.3 Idempotence

Un détecteur ne rebannit jamais une IP déjà bannie. Le BanManager.auto_ban retourne Ok(None) dans trois cas :

  • dry_run actif (un événement ip_banned est tout de même émis pour fail2ban) ;
  • l'IP est allowlistée ;
  • un ban auto actif couvre déjà l'IP.

Sans cette idempotence, chaque nouvelle tentative ratée après le ban produirait un nouvel ip_banned et noierait le sink.


5. Les leviers admin

5.1 Modes opérationnels

Variable Effet
SSH_GUARD_ENABLED=false Coupe totale. ssh-guard devient un pass-through. À éviter sauf urgence.
SSH_GUARD_DRY_RUN=true Les détecteurs tournent et émettent les événements ip_banned, mais aucun ban n'est persisté. Mode parfait pour valider un nouveau seuil ou laisser fail2ban faire le ban réel.
SSH_GUARD_PROFILE=private Profil réseau interne : tous les détecteurs désactivés (seuil u32::MAX), les événements continuent d'être émis pour audit.

5.2 Allowlist d'un partenaire CI ou d'un VPN

Une IP allowlistée bypasse les quatre détecteurs et le flood-limit. C'est le levier propre pour autoriser :

  • un runner CI qui ouvre des dizaines de connexions SSH par minute ;
  • un bastion VPN qui multiplexe plusieurs développeurs vers une seule IP source ;
  • un job de monitoring qui teste la disponibilité SSH.

Configuré via l'API admin (table ssh_guard_acl, kind = Allow, CIDR du runner). Voir Configurer ssh-guard.

5.3 Denylist d'un AS abusif

Un opérateur peut bannir un CIDR entier (ex. 198.51.100.0/24) en mode permanent. C'est le pendant inverse de l'allowlist, prioritaire sur tout (y compris les bans auto qui n'auraient pas encore été posés).


6. Pourquoi ces compromis

6.1 Fail-open quand PostgreSQL est indisponible

Si la lecture du store échoue (DB down, latence anormale), ssh-guard laisse passer la connexion et log un warn. Raison : éviter de transformer une panne DB en panne SSH généralisée. Conséquence acceptée : pendant la fenêtre de panne, un attaquant peut atteindre le handshake russh (mais doit toujours présenter une clé valide pour s'authentifier).

Pour basculer en fail-closed, il faut modifier le code (voir page développeur). C'est rare dans la vraie vie ; la plupart des admins préfèrent la disponibilité.

6.2 Bans temporaires plutôt que permanents

Le défaut SSH_GUARD_AUTO_BAN_DURATION_SECS=3600 (1 heure) est un compromis :

  • assez long pour ralentir significativement un attaquant patient ;
  • assez court pour qu'un faux positif (un script mal écrit chez un utilisateur légitime) se résolve sans intervention admin.

Mettre 0 rend les bans permanents — à n'utiliser qu'avec un workflow admin pour purger régulièrement la table.

6.3 Fenêtre glissante de 5 minutes

Suffisamment large pour qu'un attaquant ne puisse pas « réinitialiser » le compteur en attendant 30 secondes entre tentatives. Suffisamment courte pour qu'un développeur qui se trompe 4 fois de mot de passe à 8h, puis revient à 14h, ne déclenche pas de ban.

6.4 Allowlist qui bypasse les détecteurs

Une IP allowlistée n'est plus surveillée par les détecteurs. Donc une compromission depuis une IP « de confiance » ne sera pas détectée par ssh-guard. Le compromis est explicite : l'allowlist est un signal de confiance opérationnelle, pas une garantie de sécurité. Si un partenaire CI est compromis, l'attaque sera vue par les logs auth_succeeded (qui sont toujours émis), pas par les détecteurs.


7. Quand ssh-guard ne suffit pas

ssh-guard est une couche de défense. Elle ne dispense pas de :

  • un firewall en amont (UFW, iptables) qui ferme les ports inutiles ;
  • des clés SSH fortes (ed25519, ou RSA 4096+) côté utilisateur ;
  • une politique de rotation des PAT pour l'API HTTP (gitrust ne sert pas que SSH) ;
  • un fail2ban pour l'HTTP (login web, API), couvert par d'autres jails — voir Durcir avec Fail2ban.

Les quatre motifs d'attaque détectés sont les plus fréquents au niveau SSH, mais ils ne couvrent pas : attaques par chronométrie, exfiltration via tunnel, abus de privilèges après auth réussie. Ces vecteurs nécessitent d'autres outils (audit syscalls, EDR, revue de RBAC).


8. Vérifier votre compréhension

  1. Une IP allowlistée présente 50 fingerprints différents en 30 secondes. Combien d'événements key_scanning_detected sont émis ? L'IP est-elle bannie ?
  2. Vous activez SSH_GUARD_DRY_RUN=true puis vous voyez 12 événements ip_banned dans la dernière heure. Combien d'IP sont effectivement bloquées par ssh-guard ? Que faut-il pour qu'elles soient bloquées ?
  3. PostgreSQL est inaccessible pendant 10 minutes. Une attaque brute-force depuis une IP nouvelle démarre à la 5e minute. (a) Le détecteur va-t-il déclencher un ban ? (b) Les événements auth_failed apparaissent-ils dans les logs ?

9. Pour aller plus loin