The sshd jail is enabled in fail2ban almost everywhere, and it feels like SSH is covered. It does catch password guessing. But a noticeable share of what reaches port 22 isn't guessing passwords: scanners open a connection and drop it before authentication, send an HTTP request instead of SSH, or haggle over obsolete algorithms. In its default mode the stock filter doesn't see those lines. Here's what exactly it misses and how to close the gap.
What reaches SSH besides password guessing
This is what the sshd log looks like on an ordinary server with port 22 open (addresses replaced with test ones):
sshd[1234]: Invalid user admin from 203.0.113.10 port 41522
sshd[1234]: Failed password for invalid user admin from 203.0.113.10 port 41522 ssh2
sshd[1301]: Connection closed by 198.51.100.7 port 60122 [preauth]
sshd[1302]: banner exchange: Connection from 198.51.100.8 port 50014: invalid format
sshd[1303]: error: kex_exchange_identification: client sent invalid protocol identifier "GET / HTTP/1.1"
sshd[1304]: Unable to negotiate with 198.51.100.11 port 44821: no matching host key type found. Their offer: ssh-rsa,ssh-dss [preauth]
sshd[1305]: error: kex_exchange_identification: Connection closed by remote host
sshd[1305]: Connection closed by 198.51.100.12 port 52011
The first two lines are classic guessing that any filter catches. The rest is reconnaissance:
- Disconnect before authentication (
[preauth]): the scanner learned the server version and left. That's how databases of vulnerable OpenSSH versions get built. - Garbage instead of SSH: an HTTP request, an empty connection or random bytes. These are mass scanners knocking on every port with the same request.
- Haggling over algorithms: the client offers only
ssh-rsaandssh-dss. That's typical of old brute-force bots whose SSH library hasn't been updated in years.
Note the last two lines: the kex_exchange_identification line contains no address at all; it only appears on the next line from the same process. A filter that looks at one line at a time can't connect them.
Why the stock jail misses them
The sshd filter shipped with fail2ban has several modes: normal, ddos, extra and aggressive, which combines them all. Pre-auth disconnects, protocol garbage and failed algorithm negotiation are caught by the ddos and extra modes. The default, though, is normal, which only looks at login failures.
You can change the mode in /etc/fail2ban/jail.local:
[sshd]
enabled = true
mode = aggressive
Check what changes before enabling it:
# Debian 12 and newer: sshd logs to journald
fail2ban-regex systemd-journal "sshd[mode=normal]"
fail2ban-regex systemd-journal "sshd[mode=aggressive]"
# systems with /var/log/auth.log or /var/log/secure
fail2ban-regex /var/log/auth.log "sshd[mode=aggressive]"
Aggressive mode has a downside. A single login attempt writes several lines to the log: “Invalid user”, “Failed password”, “Connection closed by invalid user”. In aggressive mode several of them can match at once, and one failed attempt counts as two or three. With maxretry = 5 a colleague who mistypes the password twice can end up banned.
How we tested it
The KIPBan SSH pattern catches everything the stock sshd jail catches, plus scanners that drop the connection before authentication or send garbage instead of SSH. Each attempt counts once, however many lines it leaves in the log.
We ran both filters over the logs of four real servers. On every server the KIPBan pattern caught 2–21 more addresses than the stock one, with no false positives. It's worth keeping the scale in perspective: we see SSH password guessing on 13 servers out of 13, almost 19 thousand attempts from 886 addresses a day, and all those addresses get blocked. Scanners add a few dozen addresses per server. But those are exactly the addresses studying your server today that come back tomorrow with an exploit for the version they found.
Is it worth banning SSH scanners at all
If SSH uses keys and passwords are disabled, guessing can't hurt you, and scanners even less so. Banning them still pays off for three reasons:
- Less noise. A log without thousands of bot lines is easier to read when you need to find something important.
- Protection against the next vulnerability. When a new OpenSSH hole comes out, the first to arrive are those who already know your version.
- A shared ban list. An address scanning one of your servers is usually scanning all of them. Ban it centrally and the other servers are closed in advance.
By the way, OpenSSH 9.8 and newer has a built-in PerSourcePenalties mechanism: sshd itself briefly refuses addresses that fail a lot or drop connections. It's useful, but it works only inside sshd on one server and doesn't close the address at the firewall.
How KIPBan does it
- Every Linux server runs the
kipban-sshjail in ban mode by default: 5 attempts in 10 minutes. New servers get it when they connect. - While it runs, the stock sshd jail is off so the same lines aren't counted twice. If you delete
kipban-sshin the dashboard, the stock jail turns back on by itself. - The threshold and window are set in the dashboard: from 1 to 100 hits, a window from one minute to one day.
- A banned address gets an escalating term on the KIPBan ladder and goes to all your servers, including Windows.
For the rest of SSH hardening, from keys to ports, see SSH brute-force protection. The server's other logs, from nginx to mail, are checked by log analysis; what it finds on ordinary servers is in What fail2ban on your server doesn't see.
Questions
What does Connection closed by ... [preauth] mean in the sshd log?
The client opened a connection and closed it before authentication. Most often it's a scanner that learned the SSH server version and left. It didn't try a password, so the stock sshd filter in normal mode doesn't count the line.
How do I enable the aggressive mode of the sshd filter?
Add mode = aggressive to the [sshd] section of jail.local and restart fail2ban. Check the change first with fail2ban-regex and the sshd[mode=aggressive] filter, and keep in mind that one login attempt can be counted several times.
Why is there no IP address in the kex_exchange_identification line?
OpenSSH doesn't include the address in that message. The address appears on the next line from the same process, Connection closed by ... port .... A filter that reads one line at a time has to catch that one.