Skip to content

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 password in 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 = systemd reads 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:

sudo dnf install -y epel-release
sudo dnf install -y fail2ban fail2ban-firewalld

The fail2ban-firewalld subpackage wires bans into firewalld using the rich-rules action, which is what you want on RHEL 9.

Debian / Ubuntu

sudo apt update
sudo apt install -y fail2ban

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

sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
● 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:

sudo fail2ban-client reload

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:

sudo fail2ban-client status
Status
|- Number of jail:      1
`- Jail list:   sshd

Drill into a specific jail to see counts and currently banned IPs:

sudo fail2ban-client status sshd
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):

sudo fail2ban-client set sshd unbanip 198.51.100.23

Proactively ban an IP you know is hostile:

sudo fail2ban-client set sshd banip 198.51.100.99

Unban everything in a jail at once:

sudo fail2ban-client unban --all

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:

sudo fail2ban-client reload
sudo fail2ban-client status
Status
|- Number of jail:      3
`- Jail list:   nginx-http-auth, nginx-botsearch, sshd

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) or apt install fail2ban on Debian/Ubuntu; prefer backend = systemd.
  • Configure everything in /etc/fail2ban/jail.local, never jail.conf; tune bantime, findtime, maxretry, and ignoreip.
  • Enable the [sshd] jail, then add web jails like nginx-http-auth, apache-auth, and nginx-botsearch.
  • On RHEL 9 use the firewalld ban action so bans integrate with your existing firewall.
  • Operate with fail2ban-client status [jail] and manually set <jail> banip/unbanip as needed.

Pair this with the Server Hardening Checklist, keep SELinux enforcing, and review your perimeter rules under Firewalls.

Test yourself