Fail2ban is an intrusion prevention software framework written in the Python programming language that is designed to protect against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as nftables or iptables.
Fail2ban works together with a packet-control system or firewall installed on your server, and is commonly used to block connection attempts after a number of failed tries.
It operates by monitoring log files (or the systemd journal) for certain types of entries and runs predetermined actions based on its findings.
This guide was tested on Debian 12 (bookworm) with the Debian fail2ban 1.0.2 package. The output shown is from that test, with a second machine playing the attacker.
Related content:
The Debian 12 gotcha: there is no /var/log/auth.log
Debian 12 no longer installs rsyslog by default. SSH logins go to the systemd journal only, so /var/log/auth.log does not exist. Fail2ban’s stock sshd jail still looks for that file, and on a fresh install the service fails to start:
ERROR Failed during configuration: Have not found any log file for sshd jail
ERROR Async configuration of server failed
fail2ban.service: Main process exited, code=exited, status=255/EXCEPTION
The fix, covered below, is to tell the jail to read the journal with backend = systemd. That backend needs the python3-systemd module. Without it the error changes to:
ERROR Failed to initialize any backend for Jail 'sshd'
The second gotcha is the firewall: fail2ban’s default ban action is iptables-multiport, but Debian 12 uses nftables and does not install the iptables command by default. Use the nftables actions instead.
Ensure that your system is up to date
Ensure that your OS packages are up to date:
sudo apt update
sudo apt upgrade
Installing Fail2ban
To install Fail2ban, together with the journal module and nftables, use this command:
sudo apt install -y fail2ban python3-systemd nftables
python3-systemd is only a recommended dependency of fail2ban, so it is skipped if you install with --no-install-recommends. Naming it explicitly makes sure it is there.
Configuring the SSH jail in jail.local
Never edit /etc/fail2ban/jail.conf: package upgrades overwrite it. Older guides (including the first version of this one) copy the whole file to jail.local, which also freezes every default at today’s values. Instead create a short /etc/fail2ban/jail.local that contains only what you change:
sudo vim /etc/fail2ban/jail.local
Paste in the following:
[DEFAULT]
# Never ban localhost or your own admin IP/subnet
ignoreip = 127.0.0.1/8 ::1 203.0.113.5
bantime = 1h
findtime = 10m
maxretry = 5
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = ssh
backend = systemd
First up are the basic defaults for ignoreip, which allows you to exclude certain IP addresses from being banned. If your own computer or office has a fixed IP, enter it here (space separated, CIDR ranges allowed) so you cannot lock yourself out. Next, bantime determines how long an offending host will remain blocked until automatically unblocked. Lastly findtime and maxretry: a host that fails maxretry times within findtime is banned. Fail2ban accepts suffixes like 10m, 1h and 1d as well as plain seconds.
banaction = nftables-multiport makes bans nftables rules, and backend = systemd in the [sshd] section is the Debian 12 fix: the jail reads sshd’s messages from the journal instead of /var/log/auth.log.
If you have changed the default ssh port make sure to update it as well, otherwise the ban rule blocks port 22 while the attacker keeps hitting your real port:
[sshd]
enabled = true
port = 3922
backend = systemd
If you have a mail transfer agent configured on your server, you can enable email notifications from Fail2ban by setting destemail and sender in [DEFAULT] and adding action = %(action_mw)s, which bans and sends a whois report.
Jails are the rules which fail2ban applies to any given application or log file. On Debian, the package already enables the sshd jail in /etc/fail2ban/jail.d/defaults-debian.conf; your jail.local is read after it and wins. You can enable any other jail from jail.conf (for example nginx-http-auth or postfix) the same way, by adding a section with enabled = true to jail.local. Jails that read the journal need backend = systemd too.
Test the configuration, then restart the service and enable it at boot:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
fail2ban-client -t should end with OK: configuration test is successful. On Debian 12 it also prints a harmless WARNING 'allowipv6' not defined in 'Definition'. To silence it, create /etc/fail2ban/fail2ban.local with:
[Definition]
allowipv6 = auto
Checking the jail status
List the active jails, then look at the SSH jail:
sudo fail2ban-client status
sudo fail2ban-client status sshd
Status
|- Number of jail: 1
`- Jail list: sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
The Journal matches line confirms the jail reads the journal. If it shows File list: instead, the backend = systemd line is not being applied.
You can also check that the filter actually matches the failures already in your journal:
sudo fail2ban-regex systemd-journal sshd
The summary shows how many lines matched; a non-zero matched count after a few failed logins means the filter works.
Testing the ban from a second host
Test from a machine whose IP is not in ignoreip, and keep your current SSH session open in case you ban yourself. From the second host, fail the password a few more times than maxretry:
ssh demo@server.example.com
Back on the server, the jail now lists the attacker:
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 6
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 192.0.2.50
/var/log/fail2ban.log records each failure and the ban:
fail2ban.filter [3059]: INFO [sshd] Found 192.0.2.50 - 2026-11-10 08:28:29
fail2ban.filter [3059]: INFO [sshd] Found 192.0.2.50 - 2026-11-10 08:28:32
fail2ban.actions [3059]: NOTICE [sshd] Ban 192.0.2.50
The banned host’s next attempt is rejected before it reaches sshd:
ssh: connect to host server.example.com port 22: Connection refused
Viewing the nftables rules
With the nftables actions, fail2ban creates its own f2b-table table, with a set of banned addresses per jail. View it with:
sudo nft list table inet f2b-table
table inet f2b-table {
set addr-set-sshd {
type ipv4_addr
elements = { 192.0.2.50 }
}
chain f2b-chain {
type filter hook input priority filter - 1; policy accept;
tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
}
}
The table only appears after the jail starts. Because it lives in its own table, it does not interfere with your own nftables ruleset. On older systems that use the iptables actions, the equivalent is sudo iptables -L -n.
Banning and unbanning IP addresses
You can manually ban and unban IP addresses for any jail with the following commands:
sudo fail2ban-client set <jail> banip <ip address>
sudo fail2ban-client set <jail> unbanip <ip address>
# For example
sudo fail2ban-client set sshd unbanip 192.0.2.50
To see every banned IP across all jails, or lift all bans at once:
sudo fail2ban-client banned
sudo fail2ban-client unban --all
If you lock yourself out, log in from the console or another IP, unban yourself, then add your address to ignoreip and run sudo fail2ban-client reload.
Troubleshooting
Have not found any log file for sshd jail: the jail is still looking for/var/log/auth.log. Addbackend = systemdto the[sshd]section ofjail.local.Failed to initialize any backend for Jail 'sshd':python3-systemdis missing. Install it and restart fail2ban.- Bans appear in
fail2ban-client statusbut the attacker can still connect: check theportin the jail matches the port sshd really listens on (sudo ss -tlnp | grep sshd), and that the ban action matches your firewall (nftables actions on Debian 12). Total failedstays at 0 while logins fail: runsudo fail2ban-regex systemd-journal sshdandsudo journalctl -u ssh -n 20to see whether the failures reach the journal and match the filter.
Fail2ban is a handy addition to the nftables firewall and access control in general, but it only slows down password guessing. The stronger fix is to use SSH keys and set PasswordAuthentication no in /etc/ssh/sshd_config; fail2ban then mostly keeps your logs quiet.