Outils pour utilisateurs

Outils du site


Surveillance de sshd

Dans les journaux

Nous allons voir que le filtre proposé par défaut pour sshd est le suivant:

journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd.

Voyons ce qu'il en sort sur un hôte exposé sur le Net. Nous sommes le 23/06/2025 à 16:00. Combien y a-t-il eu d' «authentication failure» sur sshd depuis le 23/06/2025 00:00:

journalctl _SYSTEMD_UNIT=ssh.service --since "2025-06-23 00:00:00" | grep "authentication failure"

Jun 23 00:04:33 vps560672 sshd[302919]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=180.253.165.33
Jun 23 01:04:39 vps560672 sshd[303223]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=114.96.90.14
Jun 23 01:28:23 vps560672 sshd[303369]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=8.217.43.77
Jun 23 02:09:46 vps560672 sshd[303611]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=45.119.86.14
Jun 23 02:12:48 vps560672 sshd[303647]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=51.75.207.206
Jun 23 02:36:14 vps560672 sshd[303739]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=125.164.7.239
Jun 23 02:39:55 vps560672 sshd[303814]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=193.43.72.90
Jun 23 02:58:43 vps560672 sshd[303873]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=180.184.78.162  user=root
Jun 23 02:59:22 vps560672 sshd[303876]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=170.254.229.191
Jun 23 03:11:45 vps560672 sshd[304104]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=27.150.188.148  user=root
Jun 23 03:19:52 vps560672 sshd[304118]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=27.150.188.148  user=root
Jun 23 03:24:36 vps560672 sshd[304159]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=47.236.113.106
Jun 23 05:08:19 vps560672 sshd[304639]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=212.127.78.22  user=root
Jun 23 05:18:25 vps560672 sshd[304728]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=212.127.78.22
Jun 23 05:54:58 vps560672 sshd[304923]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=14.103.118.74  user=root
Jun 23 06:08:32 vps560672 sshd[304984]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=47.76.51.152  user=root
Jun 23 06:12:06 vps560672 sshd[305071]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=14.103.127.230
Jun 23 06:14:46 vps560672 sshd[305090]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=14.103.118.74
Jun 23 06:18:38 vps560672 sshd[305160]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=47.76.51.152
Jun 23 07:08:52 vps560672 sshd[305654]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=47.236.228.168
Jun 23 08:33:36 vps560672 sshd[306127]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=8.217.15.62
Jun 23 14:11:39 vps560672 sshd[307935]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=122.13.25.186
Jun 23 14:28:47 vps560672 sshd[308294]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=14.103.107.21
Jun 23 14:29:46 vps560672 sshd[308313]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=108.196.212.151
Jun 23 14:31:56 vps560672 sshd[308339]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=43.252.229.158
Jun 23 14:33:03 vps560672 sshd[308358]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=194.9.56.139
Jun 23 15:06:32 vps560672 sshd[308550]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=36.50.177.119  user=root
Jun 23 15:08:25 vps560672 sshd[308553]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=58.27.134.34
Jun 23 15:16:04 vps560672 sshd[308657]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=180.184.82.249  user=root
Jun 23 15:17:18 vps560672 sshd[308663]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=36.50.177.119
Jun 23 15:29:23 vps560672 sshd[308710]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=180.184.82.249
31 tentatives en 16 heures. La configuration de fail2ban est telle que des adresses repérées deux fois en moins de 600 secondes sont bannies 1 mois. Il n'est donc pas paranoïaque de protéger sévèrement sshd.

Une configuration possible

En relisant les divers fichiers de configuration, nous voyons que:

Dans jail.conf:

  1. le port = ssh (22 par défaut)
  2. il y a aussi une définition du «backend» = sshd_backend, dont nous savons sa valeur est initialisée dans le fichier paths-debian.conf: sshd_backend = systemd. Il y a donc une redite pas forcément nécessaire.
  3. bantime = 10m
  4. findtime = 10m
  5. maxretry = 5
  6. maxmatches = %(maxretry)s
  7. action = %(action_)s

Dans jail.d Debian installe un unique fichier dans ce répertoire defaults-debian.conf qui contient:

[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]

[sshd]
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
enabled = true
Debian protège donc ssh par défaut. Nous allons modifier un peu, mais avant, voyons le principe:

  1. la source des informations (backend), c'est le journal géré par systemd
  2. ce qu'il faut observer dans le journal (journalmatch) c'est :\ SYSTEMD_UNIT=ssh.service + _COMM=sshd que nous avons eu l'occasion de tester plus haut, sur les logs d'un serveur en production.
  3. le filtre est actif (enabled = true)

Toutes les autres directives sont données dans les divers paragraphes [DEFAULT] déjà rencontrés lors de la lecture séquentielle de la configuration.

Nous allons casser le fichier defaults-debian.conf, renommé en 000-defaults.conf et qui ne contiendra plus que :

[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
maxretry = 3
findtime = 10minutes
bantime = 15minutes
ignoreip = 192.168.60.47, 2a01:e0a:875:b1d0:ac2a:a7ff:fedd:e607
Pour les tests, nous restons gentils. Nous accordons par défaut 3 essais infructueux en l'espace de 10 minutes, avec un bannissement de seulement 1/4 d'heure. Notons que ceci peut être suffisant dans la mesure où bien souvent, un attaquant bloqué n'insiste pas, du moins pendant un assez long temps. En revanche, il peut espacer ses tentatives de bien plus de 10 minutes et passerait alors à travers.

Protection des adresses IPv4 et IPv6 de la station de l'administrateur. On ne sait jamais…

Nous créons un fichier 010-sshd.conf qui va se focaliser sur la protection de ssh:

[sshd]
backend = systemd
enabled = true
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
maxretry = 2
findtime = 1minute
bantime = 5minutes
C'est juste une reprise de ce qu'il y avait dans le fichier fourni par défaut, si ce n'est que pour les tests, le nombre d'essais est limité à 2 et que les durées de recherche et de bannissement sont très réduites.

000-defaults.conf

Nous créons ce fichier qui contiendrait pour le moment:

[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
maxretry = 3
findtime = 10minutes
bantime = 15minutes
ignoreip = <liste d'adresses à ne pas bannir> # La ou les adresses publiques d'au moins le poste de l'administrateur est une plutôt bonne idée...

Le nom commençant par «00» assure qu'il sera lu le premier1) dans la liste des fichiers présents dans ce répertoire.

010-sshd.conf

Dans ce fichier, uniquement ce qui concerne le filtre sshd. Pour l'instant, nous mettons des durées très courtes pour l'expérimentation:

[sshd]
enabled = true # important!
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
maxretry = 2
findtime = 1minute
bantime = 5minutes

Nous pouvons désormais supprimer le fichier defaults-debian.conf.

Tests

Pour rappel, le filtre installé au démarrage du serveur de test est le suivant:

table inet filter {
	chain input_ipv4 {
		icmp type echo-request limit rate 5/second burst 5 packets accept
	}

	chain input_ipv6 {
		icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
		icmpv6 type echo-request limit rate 5/second burst 5 packets accept
	}

	chain input {
		type filter hook input priority filter; policy drop;
		iifname "lo" accept
		ct state vmap { invalid : drop, established : accept, related : accept }
		meta protocol vmap { ip : jump input_ipv4, ip6 : jump input_ipv6 }
		tcp dport { 22, 80, 443 } accept
	}
}

Un attaquant, lui aussi virtuel, dont la configuration IP est la suivante:

    inet 192.168.60.76/24 brd 192.168.60.255 scope global dynamic
    inet6 2a01:e0a:875:b1d0:f80d:77c3:5e30:243a/64 scope global dynamic

Va essayer (maladroitement) de forcer ssh sur le serveur de test.

Nous observons pendant ce temps les logs au fil de l'eau, tel que les voit fail2ban:

journalctl _SYSTEMD_UNIT=ssh.service + _COMM=sshd -f

juin 30 18:19:13 trixie-nft sshd-session[1040]: Invalid user admin from 2a01:e0a:875:b1d0:5054:ff:fefe:c099 port 55938
juin 30 18:19:18 trixie-nft sshd-session[1040]: pam_unix(sshd:auth): check pass; user unknown
juin 30 18:19:18 trixie-nft sshd-session[1040]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=2a01:e0a:875:b1d0:5054:ff:fefe:c099
juin 30 18:19:20 trixie-nft sshd-session[1040]: Failed password for invalid user admin from 2a01:e0a:875:b1d0:5054:ff:fefe:c099 port 55938 ssh2
juin 30 18:21:01 trixie-nft sshd-session[1065]: Invalid user admin from 192.168.60.76 port 45056
juin 30 18:21:09 trixie-nft sshd-session[1065]: pam_unix(sshd:auth): check pass; user unknown
juin 30 18:21:09 trixie-nft sshd-session[1065]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.60.76
juin 30 18:21:11 trixie-nft sshd-session[1065]: Failed password for invalid user admin from 192.168.60.76 port 45056 ssh2
juin 30 18:21:13 trixie-nft sshd[750]: Timeout before authentication for connection from 2a01:e0a:875:b1d0:5054:ff:fefe:c099 to 2a01:e0a:875:b1d0:5054:ff:fe79:7d6f, pid = 1040
juin 30 18:23:01 trixie-nft sshd[750]: Timeout before authentication for connection from 192.168.60.76 to 192.168.60.5, pid = 1065
L'attaquant, ce bourrin, s'est fait planter avec son adresse IPv6, mail il a insisté avec son adresse IPv4, ce qui a produit le même résultat.

Sur le serveur, la configuration «nftables» a été dynamiquement mise à jour par fail2ban. Nous pouvons observer le résultat:

nft list ruleset

table inet filter {
	chain input_ipv4 {
		icmp type echo-request limit rate 5/second burst 5 packets accept
	}

	chain input_ipv6 {
		icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
		icmpv6 type echo-request limit rate 5/second burst 5 packets accept
	}

	chain input {
		type filter hook input priority filter; policy drop;
		iifname "lo" accept
		ct state vmap { invalid : drop, established : accept, related : accept }
		meta protocol vmap { ip : jump input_ipv4, ip6 : jump input_ipv6 }
		tcp dport { 22, 80, 443 } accept
	}
}
table inet f2b-table {
	set addr6-set-sshd {
		type ipv6_addr
		elements = { 2a01:e0a:875:b1d0:5054:ff:fefe:c099 }
	}

	set addr-set-sshd {
		type ipv4_addr
		elements = { 192.168.60.76 }
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip6 saddr @addr6-set-sshd reject with icmpv6 port-unreachable
		tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
	}
}

Fail2ban a créé une nouvelle table : «table inet f2b-table» Dans cette table, il a créé:

  • deux «sets»:
    • set addr6-set-sshd destiné à collectionner les adresses IPv6 à bloquer;
    • set addr-set-sshd la même chose, mais pour les adresses IPv4.
  • une chaîne «chain f2b-chain» qui par défaut accepte tout sauf:
    • les adresses IPv6 collectées qui n'ont plus accès au port 22,
    • la même chose, mais pour les adresses IPv4.

Du bon vieux temps d'IPtables, fail2ban aurait créé une chaîne «f2b-ssh» dans laquelle il aurait ajouté une règle de blocage complète pour chaque adresse repérée, dans le style:

-A f2b-ssh -s 80.240.252.168/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 157.254.54.192/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 8.222.168.120/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 65.108.211.181/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 154.209.4.36/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 157.10.252.119/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-ssh -s 14.103.114.20/32 -j REJECT --reject-with icmp-port-unreachable
....

Et le même procédure pour les adresses IPv6. Si les «sets» existaient déjà avec iptables, fail2ban ne les utilisait pas (l'exemple est pris sur une Debian 11 mise à jour en Debian 12).

La technique exposée avec fail2ban et nftables sur la trixie donne un résultat bien plus concis.

Et du temps nécessaire à la rédaction de ces lignes, les 5 minutes de bannissement sont passées, et désormais:

nft list table inet f2b-table

table inet f2b-table {
	set addr6-set-sshd {
		type ipv6_addr
	}

	set addr-set-sshd {
		type ipv4_addr
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip6 saddr @addr6-set-sshd reject with icmpv6 port-unreachable
		tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
	}
}
Si la structure ajoutée par fail2ban est toujours intacte, les sets ont quant-à-eux été purgés.

Aménagement du filtre

Certains filtres, dont sshd.conf, proposent des modes de fonctionnement. Ces modes agissent sur le choix des expressions régulières utilisées pour le filtrage.

Des explications, plus ou moins sibyllines, peuvent se trouver dans le fichier de filtre lui-même. Pour sshd.conf, nous trouvons:

# Parameter "mode": normal (default), ddos, extra or aggressive (combines all)
# Usage example (for jail.local):
#   [sshd]
#   mode = extra
#   # or another jail (rewrite filter parameters of jail):
#   [sshd-aggressive]
#   filter = sshd[mode=aggressive]
#
mode = normal
Hélas, la maîtrise de ces modes suppose non seulement celle des expressions régulières, mais également celle des règles de création des filtres. Si le mode «ddos» peut laisser supposer qu'il s'intéresse essentiellement aux attaques par déni de service distribuées, le mode «extra» est plus obscur.

Le mode «normal» sélectionné par défaut est généralement satisfaisant.

Analyse plus profonde

fail2ban a-t-il de la mémoire ?

Nous allons considérablement rallonger la durée de bannissement, pour vérifier ce qu'il se passerait pour les adresses bannies en cas de redémarrage de l'hôte:

findtime = 10minutes
bantime = 1hour

Le fait de changer la configuration oblige à la faire relire par fail2ban. La commande «fail2ban-client» permet de le faire de façon élégante: pre class="code"> fail2ban reload sshd OK («OK» étant le signe que la commande a bien été prise en compte).

Le même pirate va re-tenter sa chance, va se faire de nouveau coincer. Nous retrouvons son adresse dans le «set» idoine:

table inet f2b-table {
	set addr6-set-sshd {
		type ipv6_addr
		elements = { 2a01:e0a:875:b1d0:f80d:77c3:5e30:243a }
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip6 saddr @addr6-set-sshd reject with icmpv6 port-unreachable
	}
}

Reboot de la machine et contrôle de l'état du pare-feu. Nous retrouvons bien le bannissement:

nft list table inet f2b-table

table inet f2b-table {
	set addr6-set-sshd {
		type ipv6_addr
		elements = { 2a01:e0a:875:b1d0:f80d:77c3:5e30:243a }
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip6 saddr @addr6-set-sshd reject with icmpv6 port-unreachable
	}
}

fail2ban a bonne mémoire.

Dans la foulée, nous pouvons également vérifier qu'une commande comme:

systemctl restart fail2ban

n'efface pas pour autant sa mémoire.

L'outil «fail2ban-client»

Cet outil sert essentiellement à contrôler le fonctionnement du serveur fail2ban. Le manuel fournit (en anglais) tous les détails de son utilisation. Voici quelques-uns de ses usages:

  • fail2ban-client ping permet de savoir si le serveur est vivant. S'il l'est, il répond simplement: «pong»,
  • fail2ban-client status retourne l'état du serveur. exemple:
    Status
    |- Number of jail:	1
    `- Jail list:	ssh
  • fail2ban-client start/stop/reload <JAIL> permet de démarrer, arrêter, recharger la configuration d'une prison,
  • fail2ban-client get <JAIL> banned retourne la liste des adresses IP bannies dans la prison indiquée. Exemple:
    ['1.202.223.2', '101.126.14.37', '101.126.151.131', '101.126.81.188', '101.126.88.203', '101.126.89.164', '101.126.91.34', '101.36.108.134', '101.36.122.23', '101.36.231.231', '102.140.97.134', '102.210.80.6', '102.215.218.114', '103.106.194.74', '103.112.54.86', '103.113.105.228', '103.116.177.252', '103.117.56.152', '103.120.227.88', '103.123.227.244', '103.137.194.125', '103.139.192.247']

    Notons que le résultat est sous la forme d'un tableau python (fail2ban est écrit en python).

  • fail2ban-client unban <IP> … <IP> Permet de sortir une liste d'adresses IP de prison, quelle que soit sa prison.
  • etc.
1)
l'ordre «alphabétique» en informatique place les chiffres avant les lettres
Surveillance de sshd: Dernière modification le: 05/07/2025 à 14:25 par prof