Fail2ban — Ban Abusive IPs Automatically¶
Fail2ban watches your service logs for signs of abuse — repeated failed SSH logins, HTTP auth failures, bad-bot scans — and temporarily bans the offending IP address by adding a firewall rule. It turns the constant background noise of brute-force attempts into a non-event.
Tested on
AlmaLinux 9.4 with firewalld and Fail2ban 1.0.2 from EPEL. Debian/Ubuntu notes are called out inline.
How it works¶
Fail2ban is built from a few simple pieces:
- Filter — a set of regular expressions that recognise a "failure" line in a log (e.g.
Failed passwordin the SSH log). - Jail — ties a filter to a log source and a set of thresholds. A jail says "watch this log with this filter; if an IP trips it too often, act."
- Action — what to do on a ban (almost always: insert a firewall rule to drop the IP), and what to do when the ban expires (remove it).
- Backend — how Fail2ban reads logs. On modern systemd distros,
backend = systemdreads the journal directly instead of tailing a file, which is more reliable than guessing log paths.
Key thresholds, all configurable per jail:
| Setting | Meaning |
|---|---|
maxretry |
Number of failures before a ban |
findtime |
Window in which those failures must occur |
bantime |
How long the ban lasts |
ignoreip |
IPs/CIDRs that are never banned |
Install¶
RHEL / AlmaLinux 9 (via EPEL)¶
Fail2ban is not in the base repos — enable EPEL first:
The fail2ban-firewalld subpackage wires bans into firewalld using the rich-rules action, which is what you want on RHEL 9.
Debian / Ubuntu¶
On Debian/Ubuntu the [sshd] jail is typically enabled out of the box; on RHEL you must enable it yourself (below).
Configuration¶
Never edit jail.conf
The package ships /etc/fail2ban/jail.conf, and a package update will overwrite it. Put all your changes in /etc/fail2ban/jail.local (or drop-in files under /etc/fail2ban/jail.d/). Fail2ban reads .conf first, then .local on top, so jail.local wins.
Create /etc/fail2ban/jail.local:
[DEFAULT]
# Read logs from the systemd journal rather than flat files
backend = systemd
# Never ban these (loopback + your office / VPN range)
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
# A failure window of 10 minutes
findtime = 10m
# Ban for 1 hour after 5 failures
bantime = 1h
maxretry = 5
# Optional: ban repeat offenders for progressively longer
bantime.increment = true
bantime.maxtime = 1w
[sshd]
enabled = true
port = ssh
If you moved SSH off port 22
Set port = 2222 (or your chosen port) in the [sshd] jail so the firewall action blocks the right port. If SELinux is enforcing, also register that port — see SELinux Essentials.
Enable and start the service¶
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled)
Active: active (running) since Sat 2026-06-07 09:14:02 UTC; 3s ago
After editing config, reload without dropping existing bans:
firewalld vs. iptables actions¶
The action decides how an IP gets blocked. On RHEL 9 the default banaction is firewalld (specifically firewallcmd-rich-rules), which inserts a firewalld rich rule per banned IP. On systems still using raw iptables, the action is iptables-multiport.
Override it in [DEFAULT] if needed:
[DEFAULT]
# RHEL 9 default — integrate with firewalld
banaction = firewalld
# Use this instead only on legacy iptables-only hosts:
# banaction = iptables-multiport
Match the action to your actual firewall
If firewalld is your active firewall (the RHEL 9 default), use the firewalld action so bans live alongside your zones and rules. Mixing the iptables action with a running firewalld can produce rules that look applied but get bypassed. See Firewalls for how firewalld is structured.
Operating Fail2ban¶
Check the overall status and which jails are active:
Drill into a specific jail to see counts and currently banned IPs:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 47
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 6
`- Banned IP list: 198.51.100.23
Manually banning and unbanning¶
Unban an IP that was caught by mistake (e.g. a colleague who fat-fingered their password):
Proactively ban an IP you know is hostile:
Unban everything in a jail at once:
Adding jails for web servers¶
Brute-force protection is not just for SSH. Fail2ban ships filters for common web attacks; enable them in jail.local.
Apache / nginx HTTP basic-auth brute force¶
[apache-auth]
enabled = true
port = http,https
logpath = /var/log/httpd/error_log
maxretry = 3
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
These jails watch for repeated failed 401 authentication attempts and ban the source.
Bad bots and aggressive scanners¶
[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
findtime = 10m
bantime = 1d
nginx-botsearch catches scanners probing for /wp-login.php, /.env, /phpmyadmin, and similar non-existent paths. Because real users never hit these, a low maxretry and a long bantime are appropriate.
Filters live in /etc/fail2ban/filter.d/
Before enabling a jail, glance at its filter (e.g. /etc/fail2ban/filter.d/nginx-http-auth.conf) to confirm the regex matches your log format. If your web server uses a custom log format, the stock regex may miss lines.
After adding web jails, reload and confirm they came up:
Verify your work¶
# 1. Service is enabled and running
systemctl is-enabled fail2ban && systemctl is-active fail2ban
# Expect: enabled / active
# 2. Your jails are loaded
sudo fail2ban-client status
# Expect: sshd (plus any web jails) in the jail list
# 3. The sshd jail is reading the journal and counting failures
sudo fail2ban-client status sshd
# Expect: real numbers under "Total failed"
# 4. Bans actually land in the firewall (firewalld example)
sudo firewall-cmd --list-rich-rules
# Expect: rich rules dropping any currently-banned IPs
# 5. Test an unban round-trip
sudo fail2ban-client set sshd banip 198.51.100.250
sudo fail2ban-client status sshd | grep "Banned IP list"
sudo fail2ban-client set sshd unbanip 198.51.100.250
Working as intended
Within minutes of going live on a public IP you should see Total banned climbing in the sshd jail — that is the internet's background brute-force traffic being silently dropped.
Summary¶
- Fail2ban reads service logs, matches filters, and when an IP trips a jail's thresholds it triggers a firewall action to ban it.
- Install via EPEL on RHEL (
dnf install fail2ban fail2ban-firewalld) orapt install fail2banon Debian/Ubuntu; preferbackend = systemd. - Configure everything in
/etc/fail2ban/jail.local, neverjail.conf; tunebantime,findtime,maxretry, andignoreip. - Enable the
[sshd]jail, then add web jails likenginx-http-auth,apache-auth, andnginx-botsearch. - On RHEL 9 use the firewalld ban action so bans integrate with your existing firewall.
- Operate with
fail2ban-client status [jail]and manuallyset <jail> banip/unbanipas needed.
Pair this with the Server Hardening Checklist, keep SELinux enforcing, and review your perimeter rules under Firewalls.