Any Debian box with SSH open to the internet collects failed logins within minutes of going online. Fail2ban is still the standard fix. It reads logs, counts failures per IP and adds a firewall ban when an IP crosses a threshold. On Debian 13 (trixie) the package works differently from the old iptables and /var/log/auth.log setup: it reads the systemd journal and bans through nftables. This article covers what the stock fail2ban package on Debian 13 does, how to tune it without editing vendor files, and how to check that bans are really reaching the nftables ruleset.
What Debian 13 ships
Trixie ships fail2ban 1.1.0-8. Upstream released 1.1.0 in April 2024, mainly to support Python 3.12 and 3.13. The Debian package depends on python3 and python3-systemd. It only recommends nftables | iptables, python3-pyinotify, python3-setuptools and whois, so if you install with --no-install-recommends, the firewall tool may be missing.
The Debian-specific defaults live in one small file, /etc/fail2ban/jail.d/defaults-debian.conf:
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
enabled = trueThis file does three things:
- Every jail bans through the
nftablesaction by default, and not throughiptables-multiport. - The
sshdjail reads the journal directly instead of a log file. The match usesssh.service, which is Debian's unit name. It does not usesshd.service. - The
sshdjail is enabled as soon as the package is installed.
| Setting | Upstream jail.conf | Debian 13 package |
|---|---|---|
banaction | iptables-multiport | nftables |
backend (sshd) | %(sshd_backend)s (systemd on Debian, via paths-debian.conf) | systemd |
bantime | 10m | unchanged |
findtime | 10m | unchanged |
maxretry | 5 | unchanged |
sshd enabled | no | yes |
A 10-minute ban after 5 failures is a gentle default. It cuts down log noise but barely slows a botnet. The tuning section below fixes that.
Installing fail2ban on Debian 13 with nftables
apt update
apt install fail2ban nftables
systemctl enable --now fail2ban
systemctl status fail2banYou do not need nftables.service running for fail2ban to work. The action loads its own table with nft the first time it bans. You do need the nft binary, which comes in the nftables package.
Check that the sshd jail is running and reading the right journal source:
fail2ban-client status
fail2ban-client status sshdThe sshd status output should list Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd. If Total failed stays at 0 on a server that you know gets attacked, the journal match is wrong. This often happens after someone copies an old jail.local that switches the sshd jail back to backend = auto with logpath = /var/log/auth.log. Recent Debian releases no longer install rsyslog by default, so that file may not exist at all.
How the nftables action works
The upstream action.d/nftables.conf doesn't modify your own ruleset. It builds a separate one:
- a table named
f2b-tablein theinetfamily, so one table covers IPv4 and IPv6 - a chain named
f2b-chainon theinputhook with priority-1 - one set per jail:
addr-set-<jail>for IPv4 andaddr6-set-<jail>for IPv6 blocktype = rejectby default- a
multiportmode that matches only the jail's ports, and anallportsmode that matches every port for the protocol
Inspect it after the first ban:
nft list table inet f2b-table
nft list set inet f2b-table addr-set-sshdBecause the chain runs at priority -1, fail2ban's rules are evaluated before a normal filter chain at priority 0. A reject there is final. Your own later chains cannot let a banned IP back in, which is the behaviour you want.
The "flush ruleset" trap
Debian's stock /etc/nftables.conf begins with flush ruleset. Restarting or reloading nftables.service therefore deletes every table, including f2b-table. Fail2ban does not notice this right away: the IPs stay listed as banned in fail2ban-client status but are no longer blocked. The nftables action has an actioncheck, so the next time a banned IP shows up again or a new ban fails, fail2ban logs Invariant check failed. Trying to restore a sane environment, recreates f2b-table and re-bans. Until that happens, the old bans are gone.
There are two ways to handle it. You can restart fail2ban after every firewall reload:
systemctl restart nftables
systemctl restart fail2banOr you can flush only your own table in /etc/nftables.conf instead of the whole ruleset:
-flush ruleset
+table inet filter
+delete table inet filter
table inet filter {
...
}With the second approach, reloading your firewall leaves fail2ban's table untouched. If you use an allports action and drop instead of reject, set it per jail and leave the global default alone.
Tuning jails without touching vendor files
Never edit jail.conf or defaults-debian.conf. Package upgrades replace them. According to jail.conf(5), the load order is:
jail.confjail.d/*.conf(alphabetical)jail.localjail.d/*.local(alphabetical)
Later files win. A single jail.d/*.local file is enough:
[DEFAULT]
# never ban yourself: admin VPN and office range
ignoreip = 127.0.0.1/8 ::1 192.0.2.0/24 2001:db8:100::/48
bantime = 1h
findtime = 10m
maxretry = 4
# grow bans for repeat offenders
bantime.increment = true
bantime.maxtime = 4w
bantime.rndtime = 10m
[sshd]
mode = aggressive
port = 22Notes on those settings:
ignoreselfistrueby default, so the host's own addresses are never banned.ignoreipadds to that list. Put your management IPs in it before you tighten anything else.bantime.incrementlooks up an IP's earlier bans in fail2ban's database and multiplies the next ban. With the defaultbantime.factor = 1each repeat ban doubles: 1h, 2h, 4h, 8h and so on. A factor of 2 would double that again (1h, 4h, 8h, 16h).bantime.maxtimecaps the growth.bantime.rndtimeadds random jitter, so a botnet can't compute exactly when it will be unbanned.mode = aggressiveon the sshd filter combines thenormal,ddosandextrapatterns. It catches pre-auth disconnects and similar noise as well as wrong passwords.- If SSH runs on a non-standard port, set
portto it. Otherwise the multiport rule protects the wrong port.
Apply and verify:
fail2ban-client -t
fail2ban-client reload
fail2ban-client get sshd bantime
fail2ban-client get sshd maxretry-t checks the configuration before you reload, which matters most on a remote box.
Recidive jail for persistent attackers
The upstream recidive jail watches fail2ban's own log. IPs that get banned again and again are blocked on all ports for a week:
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1dOn Debian this uses nftables[type=allports] through banaction_allports. This jail reads a file rather than the journal, so check that fail2ban actually writes to it:
grep -E '^logtarget' /etc/fail2ban/fail2ban.conf /etc/fail2ban/fail2ban.d/*.local 2>/dev/null
ls -l /var/log/fail2ban.logUpstream's jail.conf also suggests raising dbpurgeage (default 86400 seconds, one day) when you rely on long history. This matters with bantime.increment: if the database forgets an IP after a day, the escalation starts from zero again.
[DEFINITION]
dbpurgeage = 30dTesting and day-to-day operations
Test the filter against the journal
fail2ban-regex can read the journal directly. That shows what the sshd filter would match without waiting for a real attack. Without extra options it uses the filter's own journal match, which on Debian is _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session:
fail2ban-regex systemd-journal sshdTo test exactly the match the jail uses, pass it with -m:
fail2ban-regex -m '_SYSTEMD_UNIT=ssh.service + _COMM=sshd' systemd-journal sshdLook at the Failregex hit count. A count of zero on a busy host usually means the journal match is wrong, not that nobody is attacking.
Ban, unban, inspect
fail2ban-client set sshd banip 203.0.113.10
nft list set inet f2b-table addr-set-sshd
fail2ban-client set sshd unbanip 203.0.113.10
fail2ban-client unban --allThe manual ban is the quickest end-to-end test. It proves that fail2ban can call nft and that the set is filled. Use a documentation address or a test host you control, never your own session IP.
Logs
journalctl -u fail2ban -e
tail -f /var/log/fail2ban.logA ban looks like this:
fail2ban.actions [1234]: NOTICE [sshd] Ban 203.0.113.10Known issue: reload fails with "No data available"
An upstream bug report (fail2ban#4238) from a trixie system describes systemctl reload fail2ban failing with OSError(61, 'No data available') when zero-byte journal files are present under /var/log/journal. The reporter was running an upstream 1.1.1 build on a VPS image, not the stock 1.1.0-8. The issue is still open: the reporter got it working by deleting an old journal directory, and the maintainer called the empty files an unusual leftover of that image and posted a possible workaround that is not merged. It is unlikely to hit a normal install, but it is cheap to check:
find /var/log/journal -type f -name '*.journal' -size 0Where fail2ban fits
Fail2ban reduces noise and slows down password guessing. It doesn't stop a determined attacker with many addresses, and it adds nothing if the service is already safe against guessing. Key-only SSH authentication does far more, as described in hardening SSH on Debian 13 with OpenSSH 10. Use fail2ban as one layer on top of that. The same applies to web services: jails for Nginx or Apache auth failures are useful, but they don't replace the basics in web server security for self-hosters.
On security status: when I checked the Debian security tracker, it listed no open CVEs against trixie's fail2ban 1.1.0-8. All the entries it shows are old issues, marked fixed. Check the tracker yourself before relying on that.
Takeaways
- Debian 13 ships fail2ban 1.1.0-8. By default it bans through nftables and runs the sshd jail on the systemd journal (
ssh.service). - Put overrides in
/etc/fail2ban/jail.d/*.local. Never editjail.confordefaults-debian.conf. - Add your management IPs to
ignoreipbefore you tighten anything. - Raise
bantime, turn onbantime.incrementand raisedbpurgeageso repeat offenders get longer bans. - Enable
recidiveafter confirming that/var/log/fail2ban.logis being written. - Look at
nft list table inet f2b-tableafter a test ban. Don't rely only on fail2ban's own status. - A
flush rulesetin/etc/nftables.confdeletes fail2ban's table. Restart fail2ban after firewall reloads, or flush only your own table. - Run
fail2ban-client -tbefore every reload.
Sources
- Debian -- Details of package fail2ban in trixie
- Debian sources: fail2ban 1.1.0-8 jail.d_defaults-debian.conf
- fail2ban upstream action.d/nftables.conf
- fail2ban upstream jail.conf
- jail.conf(5) manual page, Debian trixie
- fail2ban 1.1.0 release notes
- fail2ban issue #4238: systemd backend error with empty journal files
- Debian security tracker: fail2ban
Comments