Every internet-facing SSH port takes thousands of brute-force attempts a day. fail2ban stops the noise cheaply, but the defaults are timid and the most common way to get it wrong is to lock yourself out of your own box.
fail2ban watches your auth logs, counts failed logins per source IP, and drops a firewall rule when a threshold is crossed. That is the whole idea. The trouble is that a lot of guides paste a jail from 2016, aimed at iptables and a text log file, onto a machine that now uses nftables and logs only to the systemd journal. The result is a jail that either never bans anyone or, worse, bans the admin who fat-fingers a passphrase from the office. This is a working jail.local for fail2ban 1.1 on Ubuntu 24.04, with the two things the defaults get wrong: an allowlist for your own ranges, and a backend that actually reads the journal.
Why you edit jail.local, never jail.conf
fail2ban ships /etc/fail2ban/jail.conf as its stock configuration. A package upgrade overwrites that file, so anything you put there is lost on the next apt upgrade. Local settings go in /etc/fail2ban/jail.local, which fail2ban reads after jail.conf and which takes precedence key by key. You only need to specify what you are changing; everything else falls through to the stock values. The 1.1.0 defaults, for the record, are a bantime of 10 minutes, a findtime of 10 minutes, and a maxretry of 5 — short enough that a determined botnet barely notices.
Free · 4 minutes
Would you survive contact with a determined attacker — or an auditor?
Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.
The jail.local
Create /etc/fail2ban/jail.local with the following. Replace the two example ranges in ignoreip with your real office egress and VPN CIDRs before you reload anything.
# /etc/fail2ban/jail.local
# fail2ban 1.1 on Ubuntu 24.04 (systemd-journald + nftables)
[DEFAULT]
# Never ban these. Space- or comma-separated; CIDR and hostnames allowed.
# 127.0.0.1/8 and ::1 are already trusted; list YOUR ranges here.
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.42
# Read events from the systemd journal, not a log file. See note below.
backend = systemd
# Ban with nftables rather than the legacy iptables actions.
banaction = nftables-multiport
banaction_allports = nftables-allports
# Detection window: 5 failures inside 10 minutes trips the jail.
findtime = 10m
maxretry = 5
# First ban is an hour. Repeat offenders escalate automatically.
bantime = 1h
bantime.increment = true
bantime.maxtime = 1w
bantime.rndtime = 15m
[sshd]
enabled = true
backend = systemd
port = ssh
Three of those lines are doing the real work. ignoreip is the allowlist: any source inside those ranges is exempt from counting and banning, which is what keeps you out of your own trap. bantime.increment = true turns on escalating bans — the first offence costs an hour, and each subsequent ban of the same address roughly doubles, capped by bantime.maxtime at one week. bantime.rndtime adds up to fifteen minutes of jitter so a swarm banned together does not all come back at the same instant. A persistent attacker climbs from an hour to a day to a week without you touching a thing; a human who mistypes once waits an hour at most, and if they are on an allowlisted range, not even that.
The journald backend gotcha
This is the one that wastes an afternoon. Ubuntu 24.04 cloud and minimal images do not install rsyslog, so there is no /var/log/auth.log — SSH events live only in the systemd journal. The stock backend = auto is supposed to detect this, but if it resolves to a file backend pointing at a log that does not exist, the jail loads cleanly, reports as enabled, and silently never matches a single line. No error, no bans, no clue. Setting backend = systemd explicitly, as above, forces fail2ban to read the journal directly and removes the ambiguity.
Never trust that it works — prove it. Test the filter against the actual journal before you rely on it:
# Does the sshd filter match real journal entries?
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
# Apply the config and confirm the jail is live.
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
The fail2ban-regex run should report a non-zero number of matched lines if your machine has seen any failed logins. If it matches nothing on a box that is clearly being probed, your backend or filter is wrong and no amount of ban tuning will help. The status sshd output shows the journal matches in use, the current failure count, and the list of banned IPs — that list growing over a few hours is your confirmation the jail is doing its job.
When you lock yourself out anyway
It happens. You test from an address you forgot to allowlist, or a colleague does. If you still have any session open, unbanning is one command:
# Lift a specific ban immediately.
sudo fail2ban-client set sshd unbanip 198.51.100.42
If every session is gone, this is what a cloud provider’s serial or recovery console is for — get in out-of-band, run the same command, then add the range to ignoreip so it does not recur. The lasting fix is to stop treating the allowlist as an afterthought: keep jail.local in version control alongside the rest of your host configuration, so the allowlisted ranges are reviewed and deployed like any other change rather than hand-edited on a live box. If you run a pre-commit baseline with CI enforcement, a syntax check on the config is a cheap thing to add.
What fail2ban is and isn’t
fail2ban raises the cost of brute-forcing a credential. It does not replace the controls that make the credential hard to guess in the first place. On any host that matters, SSH should be key-only with password authentication disabled outright, which makes the sshd jail a backstop against noise and log volume rather than your primary defence. The same discipline applies a layer up: the tokens and keys your applications accept want their own hygiene, whether that is hashing and scoping API keys or validating JWTs correctly. fail2ban handles one attack surface well and cheaply. Treat it as one control among several, allowlist your own ranges before you arm it, and it will ban the brute-force without ever banning you.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Most technology problems are not technology problems. They are control problems.
The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.