Guides
Protect SSH with fail2ban
A complete sshd jail you can paste, plus the four things that break it in practice: the wrong port, the wrong backend, the wrong mode and an old filter that can't read OpenSSH 9.8+ logs.
To protect SSH with fail2ban, enable the sshd jail in /etc/fail2ban/jail.local with enabled = true, set port to the port sshd really listens on (for example port = 2222), use backend = systemd on distributions without /var/log/auth.log, and run sudo fail2ban-client reload. Check it with sudo fail2ban-client status sshd. The defaults ban an IP for 10 minutes after 5 failures in 10 minutes.
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1hsudo fail2ban-client reload
sudo fail2ban-client status sshdThe rest of this page explains each line, covers custom ports, key-only servers and IPv6, and shows how to prove the jail actually catches attacks.
The stock sshd jail#
fail2ban ships an [sshd] jail in /etc/fail2ban/jail.conf. Upstream 1.1.1 defines it as:
[sshd]
#mode = normal
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s%(sshd_log)s and %(sshd_backend)s come from the paths-*.conf file for your distribution: /var/log/auth.log on Debian and Ubuntu, /var/log/secure on Fedora and RHEL, and the systemd journal wherever the packager switched sshd_backend to systemd. Don't edit jail.conf — package upgrades overwrite it. Put your changes in jail.local or a file in jail.d/, which are read later and win (config load order explained).
Most packages already enable the jail. Check what yours does before adding your own:
cat /etc/fail2ban/jail.d/defaults-debian.conf # Debian, Ubuntu
ls /etc/fail2ban/jail.d/ # Fedora, EPEL: 00-firewalld.conf, 00-systemd.conf| Distribution | fail2ban | What the package sets for SSH |
|---|---|---|
| Debian 12 | 1.0.2 | [sshd] enabled = true only, but there's no /var/log/auth.log (no rsyslog) |
| Debian 13 | 1.1.0 | nftables, backend = systemd, journalmatch for ssh.service, enabled |
| Ubuntu 24.04 | 1.0.2 | nftables, backend = systemd for all jails, [sshd] enabled = true |
| Fedora, RHEL 9/10 (EPEL) | 1.1.0 | firewalld actions; backend = systemd with the fail2ban-systemd package |
Choose the right backend: log file or systemd journal#
fail2ban has to read sshd's log messages from somewhere. If it looks in the wrong place, the jail starts but never sees a failure — the most common reason for "fail2ban ssh not working".
- Log file (
backend = auto): reads/var/log/auth.logor/var/log/secure. Needs rsyslog writing that file. - Journal (
backend = systemd): reads the systemd journal directly. Needs the Python systemd bindings (python3-systemdon Debian/Ubuntu,python3-systemdon Fedora/EPEL).
On Debian 12 there is no rsyslog by default, so /var/log/auth.log doesn't exist and fail2ban 1.0.2 refuses to start the jail with Have not found any log file for sshd jail. Fix it with the journal:
sudo apt install python3-systemd[sshd]
enabled = true
backend = systemdWith the systemd backend, fail2ban-client status sshd shows the journal filter instead of a file list:
Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-sessionThat's the upstream journalmatch. Debian and Ubuntu name the unit ssh.service, but the _COMM=sshd and _COMM=sshd-session terms still match. fail2ban 1.1.1 also switches a jail to the journal automatically when no log file is found and the filter defines a journalmatch. Details: backends and the systemd journal and no log file found for jail.
fail2ban SSH on a custom port#
If sshd listens on a non-standard port, tell the jail. port = ssh means port 22, so fail2ban would block port 22 while the attacker keeps hammering 2222:
[sshd]
enabled = true
port = 2222For multiple SSH ports (for example 22 internally and 2222 from outside), list them with commas:
port = ssh,2222The port only decides what the ban blocks; detection doesn't depend on it, because sshd logs the same messages on any port. To block banned addresses on every port instead, use the all-ports action for this jail: banaction = %(banaction_allports)s. Reload after any change:
sudo fail2ban-client reload sshdsshd filter modes: normal, ddos, extra, aggressive#
The sshd filter has four modes, selected with mode in the jail:
[sshd]
enabled = true
mode = aggressive| Mode | Adds on top of the normal rules | Example line it catches |
|---|---|---|
normal (default) |
— wrong passwords, invalid users, AllowUsers/DenyUsers refusals | Failed password for invalid user admin from 203.0.113.42 |
ddos |
Connections that never authenticate, protocol garbage, pre-auth disconnects | Timeout before authentication for 203.0.113.42 port 51234 |
extra |
Clients with no usable auth method or no common algorithms | Unable to negotiate with 203.0.113.42 port 51234: no matching host key type found. |
aggressive |
ddos + extra combined |
all of the above |
We ran these lines through fail2ban-regex with each mode: the Timeout before authentication line only matches in ddos/aggressive, and the Unable to negotiate line only in extra/aggressive.
Key-only SSH needs aggressive mode#
If you've set PasswordAuthentication no, bots can't produce Failed password lines any more. Their attempts end up as pre-auth disconnects instead:
Oct 25 18:36:10 web1 sshd-session[1234]: Connection closed by authenticating user root 203.0.113.42 port 51234 [preauth]
Oct 25 18:36:11 web1 sshd-session[1234]: Received disconnect from 203.0.113.42 port 51234:14: No supported authentication methods available [preauth]Normal mode doesn't count either of these. The first matches in ddos and aggressive, the second in extra and aggressive. So on a key-only server, mode = aggressive is what makes fail2ban useful. Combined with key authentication it also keeps your logs readable and stops the endless scanning noise. The filter also has a publickey parameter: filter = sshd[mode=aggressive, publickey=invalid] additionally counts rejected keys offered for non-existent users.
sshd-session: OpenSSH 9.8 and newer#
Since OpenSSH 9.8 most authentication messages come from a process called sshd-session, not sshd. fail2ban's sshd filter only accepts that name from version 1.1.1 (_daemon = sshd(?:-session)?). Debian 13 patched its 1.1.0 package the same way. An older filter running against a newer OpenSSH misses every line — the jail looks healthy and bans nothing.
Check your filter:
grep '^_daemon' /etc/fail2ban/filter.d/sshd.confIf it prints _daemon = sshd, add an override instead of editing the stock file:
[DEFAULT]
_daemon = sshd(?:-session)?We confirmed with fail2ban-regex that a sshd-session[1234]: Failed password … line is missed with _daemon = sshd and matched once this override is in place. With the systemd backend, also make sure your journalmatch includes _COMM=sshd-session (the upstream 1.1.1 filter does).
Invalid users and preauth messages#
The default normal mode already counts the lines bots generate most:
Oct 25 18:36:10 web1 sshd[1234]: Invalid user admin from 203.0.113.42 port 51234
Oct 25 18:36:12 web1 sshd[1234]: Failed password for invalid user admin from 203.0.113.42 port 51234 ssh2[preauth] lines (disconnects before authentication finished) are the grey zone: legitimate clients produce some too, which is why they only count in the stricter modes. If your maxretry is low and you use aggressive, add your own networks to ignoreip so a flaky office connection doesn't lock you out.
IPv6#
fail2ban has handled IPv6 since 0.10, and the sshd filter matches IPv6 addresses like any other. The nftables action keeps a separate set for IPv6 bans, so nothing extra is needed:
Oct 25 18:36:10 web1 sshd[1234]: Failed password for invalid user admin from 2001:db8::25 port 51234 ssh2Keep ::1 in ignoreip next to 127.0.0.1/8. An allowipv6 warning at startup is harmless.
A complete jail.local for SSH#
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.maxtime = 1w
[sshd]
enabled = true
port = 2222
mode = aggressive
backend = systemd
maxretry = 3
[recidive]
enabled = truebantime.increment doubles the ban for each repeat offense (up to a week here). The recidive jail watches fail2ban's own log and bans addresses on all ports for a week (bantime = 1w, findtime = 1d) when they get banned again and again. The jail.local generator builds this file from a form.
Test the jail#
1. Test the filter against real log lines. Pass the mode you configured:
# log file backend
sudo fail2ban-regex /var/log/auth.log 'sshd[mode=aggressive]'
# journal backend
sudo fail2ban-regex systemd-journal 'sshd[mode=aggressive]'The summary at the end shows how many lines matched. If you have recent failed logins but 0 matched, you're reading the wrong source or have the sshd-session problem above. The fail2ban-regex guide explains the output.
2. Check the running jail:
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 6
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 203.0.113.423. Trigger a real ban from a machine you can afford to lock out (a phone on mobile data works): try a few wrong passwords, or ssh nosuchuser@your-server repeatedly, then watch:
sudo tail -f /var/log/fail2ban.log2026-10-25 18:36:10,238 fail2ban.filter [1997]: INFO [sshd] Found 203.0.113.42 - 2026-10-25 18:36:10
2026-10-25 18:36:11,027 fail2ban.actions [1997]: NOTICE [sshd] Ban 203.0.113.42Unban yourself afterwards with sudo fail2ban-client set sshd unbanip <IP> (more unban options).
Why fail2ban SSH is not working#
| Symptom | Likely cause | Fix |
|---|---|---|
Have not found any log file for sshd jail |
No /var/log/auth.log (Debian 12, minimal images) |
backend = systemd + python3-systemd |
Total failed: 0 despite attacks |
Old filter vs OpenSSH 9.8+ sshd-session, or wrong backend |
sshd.local override above; check fail2ban-regex |
| Failures counted, nobody banned | Key-only server in normal mode |
mode = aggressive |
| IP banned but still connecting | Ban on port 22, sshd on 2222 | Set port to the real port |
| Your own IP keeps getting banned | Saved wrong key/password in a client, monitoring | Add it to ignoreip |
The general checklist is in fail2ban not banning.
sshd, dropbear and selinux-ssh filter reference#
The dropbear filter is for the small SSH server used on routers and embedded systems; the OpenWrt setup, including the log file it needs, is in fail2ban on OpenWrt.
The built-in filters this guide uses, exactly as fail2ban 1.1.1 ships them — with the log lines they match and a link to test them. Looking for something else? See xrdp, LDAP, DNS, VPN and the other network-service filters, or the full filter library.
sshd OpenSSH server
The sshd filter bans failed SSH logins: wrong passwords, invalid users, and in ddos/aggressive mode pre-auth disconnects and scans. File: /etc/fail2ban/filter.d/sshd.conf; jail [sshd]; log on Debian/Ubuntu: /var/log/auth.log; modes: aggressive, ddos, extra, normal.
Log lines it bans (from fail2ban's test suite):
Jun 21 16:47:48 digital-mlhhyiqscv sshd[13709]: error: PAM: Authentication failure for myhlj1374 from 192.030.0.6May 29 20:56:52 imago sshd[28732]: error: PAM: Authentication failure for stefanor from example.comMay 29 20:56:56 imago sshd[28732]: error: PAM: Authentication failure for test-ipv6 from 2606:2800:220:1:248:1893:25c8:1946
Show the failregex fail2ban compiles (22 lines)
^<F-MLFID>(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?sshd(?:-session)?(?:\(\S+\))?[\]\)]?:?|[\[\(]?sshd(?:-session)?(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?</F-MLFID>(?:(?:error|fatal): (?:PAM: )?)?<F-CONTENT>.+</F-CONTENT>$^[aA]uthentication (?:failure|error|failed) for <F-USER>.*?</F-USER> (?:from )?<HOST>( via \S+)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User not known to the underlying authentication module for <F-USER>.*?</F-USER> (?:from )?<HOST>(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^Failed publickey for invalid user <F-USER>(?P<cond_user>\S+)|(?:(?! from ).)*?</F-USER> from <HOST>(?: (?:port \d+|on \S+)){0,2}(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
^Failed (?:<F-NOFAIL>publickey</F-NOFAIL>|\S+) for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>(?: (?:port \d+|on \S+)){0,2}(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
^<F-USER>ROOT</F-USER> LOGIN REFUSED FROM <HOST>
^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> (?:from )?<HOST>(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> (?:from )?<HOST> not allowed because not listed in AllowUsers(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> (?:from )?<HOST> not allowed because listed in DenyUsers(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> (?:from )?<HOST> not allowed because not in any group(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^refused connect from \S+ \(<HOST>\)
^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>(?: (?:port \d+|on \S+)){0,2}:\s*3: .*: Auth fail(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> (?:from )?<HOST> not allowed because a group is listed in DenyGroups(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> (?:from )?<HOST> not allowed because none of user's groups are listed in AllowGroups(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^<F-NOFAIL>pam_[a-z]+\(sshd:auth\):\s+authentication failure;</F-NOFAIL>(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=<F-ALT_USER>\S*</F-ALT_USER>\s+rhost=<HOST>(?:\s+user=<F-USER>\S*</F-USER>)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^maximum authentication attempts exceeded for (?:invalid user )?<F-USER>.*?</F-USER> (?:from )?<HOST>(?: (?:port \d+|on \S+)){0,2}(?: ssh\d*)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^User <F-USER>\S+|.*?</F-USER> not allowed because account is locked(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+</F-USER> <HOST>(?: (?:port \d+|on \S+)){0,2}:\s*Change of username or service not allowed:\s*.*\[preauth\]\s*$
^Disconnecting: Too many authentication failures(?: for <F-USER>\S+|.*?</F-USER>)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>(?: (?:port \d+|on \S+)){0,2}:\s*11:
^<F-NOFAIL><F-MLFFORGET>(?:Connection (?:closed|reset)|Disconnect(?:ed|ing))</F-MLFFORGET></F-NOFAIL>(?: (?:by|from))?(?: (?:invalid|authenticating) user <F-USER>\S+|.*?</F-USER>)?(?: from)? <ADDR>(?: (?:port \d+|on \S+)){0,2}(?:: (?!Too many authentication failures)[^\[]+)?(?: \[preauth\])?\s*$
^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)
^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>Test sshd in the browser or on the server: sudo fail2ban-regex /var/log/auth.log sshd · source
dropbear Dropbear SSH
The dropbear filter bans failed password logins and bad users on the Dropbear SSH server. File: /etc/fail2ban/filter.d/dropbear.conf; jail [dropbear]; log on Debian/Ubuntu: /var/log/auth.log.
Log lines it bans (from fail2ban's test suite):
Mar 24 15:25:51 buffalo1 dropbear[4092]: bad password attempt for 'root' from 198.51.100.87:5543Feb 11 15:23:17 dropbear[1252]: login attempt for nonexistent user from ::ffff:198.51.100.215:60495Mar 24 15:25:51 buffalo1 dropbear[4092]: bad password attempt for 'root' from 198.51.100.87:5543
Show the failregex fail2ban compiles (4 lines)
^(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?dropbear(?:\(\S+\))?[\]\)]?:?|[\[\(]?dropbear(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?(?:\[\d+\] \w{2,3} [\d:\s]+)?<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$^[Ll]ogin attempt for nonexistent user (?:'<F-USER>.*</F-USER>' )?from <HOST>:\d+$
^[Bb]ad (?:PAM )?password attempt for '<F-USER>.+</F-USER>' from <HOST>(?::\d+)?$
^[Ee]xit before auth from \<?<ADDR>:\d+\>?: (?:\([^\)]*\): )?Max auth tries reached - user '<F-USER>.+</F-USER>'\s*$
^[Ee]xit before auth \(user '.+', \d+ fails\): Max auth tries reached - user '<F-USER>.+</F-USER>' from <HOST>:\d+\s*$Test dropbear in the browser or on the server: sudo fail2ban-regex /var/log/auth.log dropbear · source
selinux-ssh OpenSSH via SELinux audit
The selinux-ssh filter bans failed SSH logins recorded in the SELinux audit log. File: /etc/fail2ban/filter.d/selinux-ssh.conf; jail [selinux-ssh]; log on Debian/Ubuntu: /var/log/audit/audit.log.
Log lines it bans (from fail2ban's test suite):
type=USER_ERR msg=audit(1373330717.000:4070): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=173.242.116.187 addr=173.242.116.187 terminal=ssh res=failed'type=USER_ERR msg=audit(1372557500.000:61747): user pid=23684 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=113.240.248.18 addr=113.240.248.18 terminal=ssh res=failed'type=USER_AUTH msg=audit(1373129280.000:9): user pid=1277 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=pubkey acct="root" exe="/usr/sbin/sshd" hostname=? addr=194.228.20.113 terminal=ssh res=failed'
Show the failregex fail2ban compiles (1 line)
^type=USER_(ERR|AUTH) msg=audit\(:\d+\): (?:user )?pid=\d+ uid=0 auid=\d+ ses=\d+ subj=(?:unconfined_u|system_u):system_r:sshd_t:s0-s0:c0\.c1023 msg='(?:(?!acct=|exe=|addr=|terminal=|res=)\w+=(?:"[^"]+"|\S*) )*acct=(?:"<F-USER>[^"]+</F-USER>"|<F-ALT_USER>\S+</F-ALT_USER>) exe="/usr/sbin/sshd" (?:(?!acct=|exe=|addr=|terminal=|res=)\w+=(?:"[^"]+"|\S*) )*addr=<ADDR> terminal=ssh res=failed'(?:\x1D|$)Test selinux-ssh in the browser or on the server: sudo fail2ban-regex /var/log/audit/audit.log selinux-ssh · source
FAQ#
How do I enable fail2ban for SSH?#
Create /etc/fail2ban/jail.local with an [sshd] section containing enabled = true, plus backend = systemd if your system has no /var/log/auth.log. Run sudo fail2ban-client reload and confirm with sudo fail2ban-client status sshd. Debian and Ubuntu packages already enable the sshd jail by default.
How do I use fail2ban with a custom SSH port?#
Set port in the sshd jail to the port sshd listens on, for example port = 2222, or port = ssh,2222 for several ports. Detection works on any port, but the ban only blocks the ports listed, so leaving port = ssh lets attackers continue on 2222.
What does mode = aggressive do in the sshd jail?#
It combines the ddos and extra modes with the normal rules. Besides wrong passwords and invalid users it counts pre-auth disconnects, connections that never authenticate, and clients without a usable auth method or algorithm. Use it on servers that only accept SSH keys, where bots never produce "Failed password" lines.
Do I need fail2ban if I only use SSH key authentication?#
Keys stop password guessing, so fail2ban is no longer what keeps you safe. It still cuts log noise, saves resources and slows scanners down, and with mode = aggressive it bans clients that keep connecting without valid keys. Many admins run both: keys for security, fail2ban for hygiene.
Should I use sshd or ssh as the jail name?#
The jail is called sshd in fail2ban 0.10 and later, so use [sshd] in jail.local and fail2ban-client status sshd. Old tutorials use [ssh]; that section doesn't exist in current jail.conf, so settings placed under it have no effect.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.