Sécurité & détectiondébutant5 min de lecture

Détecter et bloquer le brute force SSH : config sshd, fail2ban, et ce que fail2ban ne voit pas

Comment voir en deux minutes qui martèle votre port SSH, quels cinq réglages sshd rendent 99 % des attaques vaines, et comment configurer fail2ban correctement sur Ubuntu 24.04 (le piège du backend systemd). Ensuite : les trois schémas d'attaque auxquels fail2ban est aveugle.

Sommaire
  1. Étape 1 : voir qui est à la porte maintenant
  2. Étape 2 : cinq réglages sshd qui changent la donne
  3. Étape 3 : limitation de débit au pare-feu
  4. Étape 4 : fail2ban — avec le bon backend
  5. Étape 5 : alerter sur ce qui compte
  6. Pièges
  7. Ce qu'il vous manque encore
  8. Comment monsys fait
  9. FAQ

Mettez un serveur avec une IP publique en ligne et en moins d'une heure quelqu'un essaie root avec 123456. Ce n'est pas une attaque contre vous, c'est le bruit de fond d'internet. Cela ne devient un problème que si vous autorisez la connexion par mot de passe, si l'un de vos utilisateurs a un mot de passe faible, ou si vous ne remarquez pas que le schéma passe de « tout le monde essaie root » à « quelqu'un essaie vos noms d'utilisateur ». Cet article met en place la détection et le blocage avec ce qui est déjà sur le serveur.

Étape 1 : voir qui est à la porte maintenant

Sur Ubuntu 24.04 et Debian 12, sshd journalise dans le journal systemd. /var/log/auth.log n'existe que si rsyslog est installé — sur les images cloud, souvent pas.

# Connexions échouées aujourd'hui, nombre
journalctl -u ssh --since today --no-pager | grep -c 'Failed password'
# Debian appelle aussi l'unité ssh.service ; sur certains systèmes sshd.service
journalctl -u sshd --since today --no-pager | grep -c 'Failed password'

# Top 10 des IP sources
journalctl -u ssh --since today --no-pager | grep 'Failed password' \
  | grep -oE 'from [0-9a-f.:]+' | sort | uniq -c | sort -rn | head

# Quels noms d'utilisateur essaient-ils ? Distinguer existants (Failed password for USER)
# des inexistants (invalid user USER)
journalctl -u ssh --since today --no-pager | grep -oE 'invalid user [^ ]+' | sort | uniq -c | sort -rn | head
journalctl -u ssh --since today --no-pager | grep -E 'Failed password for [^i]' | grep -oE 'for [^ ]+' | sort | uniq -c | sort -rn | head

# Et : qui EST entré, d'où ?
journalctl -u ssh --since "7 days ago" --no-pager | grep -E 'Accepted (publickey|password)' \
  | awk '{print $1, $2, $3, $9, $11}' | sort -k4

La dernière commande est la plus importante et la moins lancée. Mille tentatives échouées, c'est du bruit ; un Accepted publickey for deploy from 185.x.x.x depuis une IP que vous ne connaissez pas, c'est un incident.

Étape 2 : cinq réglages sshd qui changent la donne

Tout dans /etc/ssh/sshd_config.d/10-hardening.conf (Ubuntu et Debian lisent ce répertoire ; laissez le fichier principal tranquille) :

sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
# 1. Pas de mots de passe. Le brute force sur des clés est vain.
PasswordAuthentication no
KbdInteractiveAuthentication no

# 2. root ne se connecte jamais directement. sudo laisse une trace, root non.
PermitRootLogin no

# 3. Seuls ces comptes peuvent entrer via SSH. Ce qui n'est pas ici n'existe pas pour sshd.
AllowUsers deploy jeroen

# 4. Moins de tentatives, fenêtre plus courte.
MaxAuthTries 3
LoginGraceTime 20

# 5. Pas de canaux inutilisés.
X11Forwarding no
AllowAgentForwarding no
EOF

# Vérification de syntaxe AVANT de recharger — une erreur ici vous enferme dehors.
sudo sshd -t && sudo systemctl reload ssh

Gardez votre session actuelle ouverte et testez dans un deuxième terminal que vous entrez encore. Ne fermez la première qu'ensuite.

Ce qui n'y est délibérément pas : un autre port. Port 2222 réduit le bruit dans vos logs, pas l'attaque. Les scanners trouvent le port en quelques secondes, et vous perdez la possibilité de distinguer le trafic SSH sur le port 22 dans les pare-feux et le netflow. Faites-le uniquement pour le calme dans vos logs, et n'appelez pas ça de la sécurité.

Étape 3 : limitation de débit au pare-feu

Avant même fail2ban : laissez le noyau faire le gros du travail. Avec ufw :

sudo ufw limit 22/tcp comment 'ssh rate-limit'
# = max 6 nouvelles connexions par 30 s par IP source, puis drop

Ou directement dans nftables si vous n'utilisez pas ufw :

sudo nft add rule inet filter input tcp dport 22 ct state new limit rate over 6/minute burst 6 packets drop

Cela ne coûte rien, ne nécessite aucun démon et arrête complètement le brute force bête et rapide.

Étape 4 : fail2ban — avec le bon backend

fail2ban lit les logs, compte les échecs par IP et ajoute temporairement une règle de pare-feu. Le piège sur Ubuntu 24.04 et Debian 12 : la configuration par défaut cherche /var/log/auth.log, et ce fichier n'existe pas si rsyslog manque. Le jail démarre alors mais ne bannit jamais. Forcez le backend systemd.

sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
backend  = systemd
bantime  = 1h
findtime = 10m
maxretry = 5
# Escalade : qui revient après un ban reste écarté plus longtemps
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 1w
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8

[sshd]
enabled = true
mode    = aggressive   # compte aussi "invalid user" et les déconnexions pré-auth
EOF
sudo systemctl enable --now fail2ban

# Ça marche ?
sudo fail2ban-client status sshd
sudo fail2ban-client status sshd | grep 'Currently banned'

Si « Currently failed » est encore à 0 après un jour alors que journalctl -u ssh regorge d'échecs, le backend est mauvais. fail2ban-client get sshd logpath doit être vide (systemd) ou pointer vers un fichier existant.

Bannir et débannir manuellement :

sudo fail2ban-client set sshd banip 203.0.113.7
sudo fail2ban-client set sshd unbanip 203.0.113.7

Étape 5 : alerter sur ce qui compte

Les connexions échouées ne doivent pas vous réveiller ; fail2ban et ufw s'en chargent. Ces trois choses, si, et pour cela il faut un petit script :

sudo tee /usr/local/sbin/ssh-watch.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/security"
HOST=$(hostname -s)
KNOWN=/etc/ssh-watch.known   # une IP ou un préfixe de confiance par ligne
ALERTS=()
touch "$KNOWN"

# 1. Connexion réussie depuis une IP inconnue (15 dernières min)
while read -r user ip; do
  grep -q "^${ip%.*}" "$KNOWN" || ALERTS+=("login $user from UNKNOWN $ip")
done < <(journalctl -u ssh --since "15 min ago" --no-pager | grep -E 'Accepted (publickey|password)' | awk '{print $9, $11}')

# 2. Tentatives sur de VRAIS noms d'utilisateur (pas 'invalid user') — quelqu'un connaît vos comptes
real=$(journalctl -u ssh --since "15 min ago" --no-pager | grep -E 'Failed password for [^i]' | wc -l)
[ "$real" -gt 20 ] && ALERTS+=("$real failed attempts on EXISTING users in 15 min")

# 3. Brute force lent et distribué : > 50 IP sources uniques en une heure, chacune < 5 tentatives
uniq_ips=$(journalctl -u ssh --since "1 hour ago" --no-pager | grep -oE 'from [0-9a-f.:]+' | sort | uniq -c | awk '$1<5' | wc -l)
[ "$uniq_ips" -gt 50 ] && ALERTS+=("distributed: $uniq_ips low-rate source IPs in 1 h")

if [ ${#ALERTS[@]} -gt 0 ]; then
  printf '%s\n' "${ALERTS[@]}" | curl -s -H "Title: ssh@$HOST" -H "Priority: urgent" --data-binary @- "$NTFY" >/dev/null
fi
EOF
sudo chmod 0755 /usr/local/sbin/ssh-watch.sh
echo '*/15 * * * * root /usr/local/sbin/ssh-watch.sh' | sudo tee /etc/cron.d/ssh-watch

Remplissez /etc/ssh-watch.known avec les trois premiers octets de vos plages bureau et VPN. Oui, c'est grossier. C'est aussi opérationnel en une heure et ça attrape l'alerte que vous voulez vraiment : quelqu'un est entré depuis un endroit que vous ne connaissez pas.

Pièges

  • S'enfermer dehors. Toujours sshd -t avant reload, toujours une deuxième session ouverte, et sur les VPS cloud : sachez où est la console dans le panneau avant de commencer.
  • fail2ban bannit votre propre bureau. Un collègue qui choisit trois fois le mauvais fichier de clé déclenche maxretry. Mettez vos propres plages dans ignoreip.
  • Oublier l'IPv6. ufw limit 22/tcp couvre v4 et v6 ; les règles nftables écrites à la main souvent pas. Vérifiez avec ss -tn state established '( sport = :22 )' s'il y a des connexions v6.
  • AllowUsers et l'automatisation. Un compte de sauvegarde ou de déploiement absent de la liste échoue en silence. Ajoutez-le, ou utilisez AllowGroups ssh-users et gérez le groupe.
  • Rotation du journal. journalctl --since "7 days ago" ne fonctionne que si le journal est conservé aussi longtemps. Mettez SystemMaxUse= et MaxRetentionSec=30day dans journald.conf ; sinon, lors d'un incident, vous ne voyez rien de plus vieux qu'hier.

Ce qu'il vous manque encore

fail2ban et le script voient un hôte et un log. Trois schémas passent au travers :

  1. La géographie. Une connexion réussie depuis une IP absente de known est une alerte — mais « cet utilisateur ne s'est jamais connecté depuis ce pays » en est une bien plus nette. Il faut pour cela du GeoIP, de préférence sans licence MaxMind (les fichiers RIR de RIPE, ARIN, APNIC, AFRINIC et LACNIC sont gratuits).
  2. La corrélation de flotte. La même IP qui fait une tentative sur cinq de vos serveurs n'est bannie nulle part et ne se remarque nulle part. C'est exactement ainsi que fonctionnent les attaques distribuées.
  3. Ce qui se passe après la connexion. Une connexion SSH réussie suivie d'une lecture de /root/.ssh/id_rsa.backup dans les deux minutes, ce n'est pas un sysadmin, c'est un intrus. Voir le guide sur les fichiers honeypot.

Comment monsys fait

L'agent monsys fait la détection sur l'hôte lui-même — aucune ligne de log ne sort, seulement le signal : ssh_bruteforce (par IP source et par utilisateur, avec fenêtre), invalid_user_enumeration, new_country_login (GeoIP via données RIR, sans licence) et geo_blocked_country. Le hub corrèle sur tous les hôtes d'une tenant, et la détection de mouvement latéral relie un déclenchement de honeypot à une connexion SSH interne dans les cinq minutes. Bloquer reste une décision humaine, via un Emergency Action Token signé — monsys n'intervient jamais de lui-même.

FAQ

Est-il sûr de désactiver la connexion par mot de passe ?

Oui, à condition d'avoir d'abord mis votre clé publique dans ~/.ssh/authorized_keys et testé que la connexion par clé fonctionne. Faites-le dans une deuxième session avant de fermer la première. Avec PasswordAuthentication no, le brute force de mots de passe est par définition sans espoir.

Pourquoi fail2ban ne bannit-il rien sur Ubuntu 24.04 ?

Généralement parce que /var/log/auth.log n'existe pas (pas de rsyslog) et que fail2ban lit ce fichier par défaut. Mettez backend = systemd dans jail.local et redémarrez fail2ban. Vérifiez avec fail2ban-client status sshd.

Changer le port SSH aide-t-il ?

Cela réduit le nombre de lignes de log, pas le risque. Les scanners de ports trouvent un port alternatif en quelques secondes. Combinez-le tout au plus avec la connexion par clé uniquement et la limitation de débit ; en soi, ce n'est pas une mesure de sécurité.

Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.

Fait à la main ? Laissez monsys s'en charger en continu.

Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.