If you have ever tailed
/var/log/auth.logon a server sitting on the public internet, you already know the story. Within minutes of boot, you'll see a steady drizzle of failed SSH logins from IPs you've never heard of, usually trying root, admin, or some default account name. Most of these are automated scanners working through a list of hosts, not targeted attacks, but that doesn't make them harmless. Given enough time and a weak password somewhere, one of those attempts eventually lands. Fail2Ban is the tool I reach for first on every new box I provision, specifically because it turns that background noise into something the server handles on its own, without me babysitting logs at 2 a.m.
In this guide I'll walk through setting up Fail2Ban on a typical Linux server, using sw-infrarunbook-01 as our example host, to protect SSH and a couple of other common services. I'll give you the full configuration I actually use, not just fragments, and I'll call out the mistakes I've personally made or seen colleagues make when they set this up in a hurry.
Prerequisites
Before you start, make sure you actually have these things in place. Skipping this step is how people end up locking themselves out of their own server, which is a genuinely bad way to start a Tuesday.
- A Linux server with root or sudo access. I'm using Ubuntu/Debian conventions here, but I'll note the RHEL/CentOS differences where they matter.
- A working firewall backend. Fail2Ban doesn't block traffic itself; it manipulates iptables, nftables, or firewalld rules. Know which one your system uses before you begin.
- SSH access via a method that won't get you banned accidentally, ideally key-based auth from a known IP, or at minimum, know your own public IP address so you can whitelist it.
- Root log access. Fail2Ban parses log files like
/var/log/auth.log
or, on systemd-based distros, reads directly from the journal.
One thing I always do before touching Fail2Ban configuration: open a second terminal session to the server and keep it connected. If a bad regex or an overly aggressive ban rule locks out your own IP, having a second live session means you can fix it without needing out-of-band console access. I have seen this bite people who edited configs over a single SSH session and then couldn't reconnect to undo their own mistake.
Step-by-Step Setup
Start with installation. On Debian-based systems:
sudo apt update
sudo apt install fail2ban -y
On RHEL-based systems, you'll need EPEL first:
sudo dnf install epel-release -y
sudo dnf install fail2ban -y
Once installed, do not edit
/etc/fail2ban/jail.confdirectly. This is the single most common mistake I see, and it's worth repeating: that file gets overwritten on package updates, and any custom rules you put there will silently vanish the next time Fail2Ban is upgraded. Fail2Ban is designed around a layered config system specifically to avoid this, so use it.
Create a local override file instead:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Inside
jail.local, the file is organized into a
[DEFAULT]section that applies globally, followed by per-service jail sections like
[sshd]. The defaults ship fairly conservative, so the first thing I adjust is the ban behavior itself. Find (or add) these under
[DEFAULT]:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8
bantimecontrols how long an IP stays blocked,
findtimeis the window in which failures are counted, and
maxretryis how many failures within that window trigger a ban. I set
ignoreipto include our internal management network (10.0.0.0/8 in this example) so our own monitoring tools and jump hosts never get caught in the net. Add your admin workstation's static IP here too if you have one.
Next, enable the SSH jail explicitly. Even though it's often enabled by default, I like to be explicit rather than trust defaults I haven't read:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 4
bantime = 3600
If you've moved SSH off port 22, which I'd recommend doing anyway as a baseline hardening step, make sure the
portline reflects that, either as a number or a named service from
/etc/services.
After editing, restart the service and check that it actually started without errors, since a typo in jail.local will fail silently in ways that only show up when you check status:
sudo systemctl restart fail2ban
sudo systemctl status fail2ban
Full Configuration Example
Here is a complete
jail.localI've used on production hosts, covering SSH and a couple of other commonly exposed services. Adjust the paths and ports to match what's actually running on sw-infrarunbook-01.
[DEFAULT]
# Global settings applied to all jails unless overridden
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8
banaction = iptables-multiport
destemail = infrarunbook-admin@solvethenetwork.com
sender = fail2ban@solvethenetwork.com
mta = sendmail
action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 4
bantime = 3600
findtime = 600
[sshd-aggressive]
enabled = false
filter = sshd
logpath = %(sshd_log)s
maxretry = 2
bantime = 86400
findtime = 300
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
action = %(action_mwl)s
bantime = 604800
findtime = 86400
maxretry = 3
A few notes on choices in that file. I keep a
[sshd-aggressive]jail defined but disabled by default, so it's there to flip on quickly during an active attack wave without having to write it from scratch under pressure. The
[recidive]jail is one people often skip, but it's genuinely useful: it watches Fail2Ban's own log for IPs that keep getting banned repeatedly across different jails, and escalates them to a week-long ban. In my experience this catches the more persistent scanners that time their retries to just outlast a one-hour ban.
The
destemailand
action = %(action_mwl)scombination sends an email with the log lines whenever a ban happens. This is optional and adds mail server dependency, so if you don't have local mail delivery configured, drop it back to
action = %(action_)sto just ban without notification, which is simpler and has fewer moving parts to break.
Verification Steps
Don't just assume it's working because the service started. Verify it properly, because a jail that's "enabled" on paper but pointed at the wrong log path will never ban anyone.
First, check that fail2ban-client sees your jails:
sudo fail2ban-client status
You should see a list like
Jail list: sshd, nginx-http-auth, recidive. If a jail you enabled is missing from that list, check
journalctl -u fail2banfor the parsing error that kept it from loading.
Next, check the details of a specific jail:
sudo fail2ban-client status sshd
This shows currently failed attempts, total banned IPs, and the actual ban list. To confirm the filter regex is actually matching your log format, use fail2ban-regex against the live log before you trust it in production:
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
This dry-runs the filter against real log lines and tells you how many matched. If it reports zero matches on a server you know has failed logins, your log format doesn't match what the filter expects, which is common when logging is customized or when using a non-standard SSH daemon build.
Finally, test an actual ban safely. From a separate machine (not your primary access point), deliberately fail an SSH login a few times against your test IP allowance, then check:
sudo fail2ban-client status sshd
sudo iptables -L -n | grep
You should see the IP listed in both the jail's banned list and the actual firewall rule chain. If it shows in Fail2Ban's status but not in iptables, your
banactionis misconfigured or your firewall backend doesn't match what Fail2Ban expects.
Common Mistakes
The mistake I see most often, by a wide margin, is editing
jail.confinstead of
jail.local. I mentioned it above, but it deserves repeating because it's the one that causes people to lose their custom configuration silently, sometimes months later during a routine package update, with no obvious link between the update and the sudden loss of protection.
Second is forgetting to whitelist your own management IPs in
ignoreip. I've had colleagues set
maxretry = 3to be aggressive, then get locked out themselves after a string of typos in their own password during a late-night session. It's not fun explaining to a client why their access got blocked by their own security tool.
Third is mismatched log paths, particularly on systems that have switched to journald-only logging without a traditional
/var/log/auth.logfile. If
logpathpoints to a file that doesn't exist or is empty, the jail loads without error but simply never sees any failures. Switching
backend = systemdand removing an explicit
logpathfor jails that support journal mode fixes this.
Fourth, people assume a ban is permanent when it's not.
bantimeof 1 hour means exactly that. Persistent attackers cycle back after the ban expires and get banned again, which usually stays fine because it's the same net effect, but if you want a genuinely permanent block for known bad actors, use
bantime = -1in a dedicated jail, or better yet, feed those IPs into a separate blocklist maintained outside Fail2Ban entirely.
Last, I've seen people configure Fail2Ban and then never look at it again. Attack patterns change, log formats change when you upgrade services, and filters written for one version of a daemon's log format sometimes stop matching after an upgrade changes the message wording. Check
fail2ban-client status sshdevery so often, not because you expect it to be broken, but because a quiet Fail2Ban with zero bans over months on an internet-facing SSH port is itself worth investigating rather than celebrating.
Fail2Ban isn't a replacement for good authentication hardening, key-based SSH, disabling root login, and a properly configured firewall still matter more. But as a layer that quietly absorbs the constant background noise of automated brute-force attempts, it's one of the highest-value, lowest-effort tools you can add to a server, and it's worth setting up correctly the first time rather than patching it together under pressure during an actual incident.
