Post

fail2ban: automating the lockouts iptables doesn't

A short walkthrough of fail2ban's mental model, the minimum useful config, and how it complements iptables and a hardened SSH setup.

Why fail2ban Exists

iptables decides who is allowed to connect. SSH hardening decides what they can do if they reach you. Neither of them addresses the in-between problem: an attacker who is technically allowed to reach port 22 and is now spending all day guessing passwords.

You could solve this manually: tail journalctl, watch for failed logins, write a rule. Nobody actually does that. fail2ban automates exactly that loop. Read a log, match a pattern, push a temporary firewall rule against the offending IP, expire the rule later.

That’s the entire mental model. Everything else is configuration.


How It Actually Works

Three pieces:

  • A filter is a regex that matches “this log line means a failed authentication.” Pre-shipped for SSH, nginx, postfix, and dozens of others.
  • A jail binds one filter to a backend (which log to watch), a threshold (how many matches before action), and an action (what to do).
  • An action is usually an iptables/nftables rule that blocks the source IP, plus a timer that reverses it.

fail2ban-server runs as a daemon, tails the relevant logs, and applies actions when thresholds are hit. fail2ban-client is how you talk to it.


Minimum Useful Config

The default install does almost nothing until you enable a jail. Override the defaults in /etc/fail2ban/jail.local (never edit jail.conf directly; it gets overwritten by package updates):

1
2
3
4
5
6
7
8
9
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd
ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10

[sshd]
enabled = true

What this says:

  • Five failed attempts within ten minutes earns a one-hour ban.
  • Logs come from the systemd journal, not a flat file. Relevant on any modern Fedora, Debian, or Ubuntu.
  • ignoreip exempts loopback and (in my case) the entire Tailscale CGNAT range. I never want a typo on my own tailnet to lock me out.
  • The [sshd] jail uses the built-in filter at /etc/fail2ban/filter.d/sshd.conf.

Enable and start:

1
sudo systemctl enable --now fail2ban

Verify it’s watching the right thing:

1
sudo fail2ban-client status sshd

You’ll see something like:

1
2
3
4
5
6
7
8
9
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:        (systemd journal)
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

That’s a working jail with no current offenders. On a server with port 22 exposed to the internet, the “Total banned” counter will start climbing within hours.


Watching It Work

The most informative single command:

1
sudo fail2ban-client status sshd

Lists currently-banned IPs and lifetime totals for that jail.

To unban an IP manually (yourself, after a self-inflicted lockout):

1
sudo fail2ban-client set sshd unbanip 203.0.113.42

To see what rules are currently active in the firewall from fail2ban’s actions:

1
sudo iptables -L f2b-sshd -n --line-numbers

The f2b-sshd chain is created by fail2ban on first ban and holds the active blocks. When a ban expires, the corresponding line is removed automatically.


Gotchas Worth Knowing

Bans don’t persist across restarts by default. Adding bantime.increment = true and tuning bantime.maxtime makes repeat offenders earn exponentially longer bans, which is closer to what you actually want.

The systemd backend reads from the journal, not /var/log/auth.log. If you’ve configured rsyslog to forward auth events and were expecting the file-based filter to work, it won’t. You would be silently watching nothing.

ignoreip is your safety net. If you’re remote and rely on a VPN or jump host to reach the machine, exempt that range explicitly. Locking yourself out of a remote host because of a sticky keyboard is a fixable mistake but an annoying one.

Don’t run fail2ban without iptables/nftables loaded. The action layer needs something to install rules into. On a minimal container or stripped-down system, you may need firewalld or raw iptables available first.


Where It Fits

In layers:

  1. iptables: default-deny on inbound, only the ports you actually serve.
  2. SSH hardening: keys only, no root login, no password auth, port 22 closed if you can manage it.
  3. fail2ban: catch what gets through anyway.

Each layer assumes the previous one is in place. fail2ban is not a substitute for either of the others; it’s the alarm system you install after the door is already locked. But it’s cheap to set up, costs nothing to run, and turns a constant stream of probe attempts into a problem that solves itself.

This post is licensed under CC BY 4.0 by the author.