Even with SSH key authentication enabled, a badly configured or temporarily accessible server with password auth still faces a constant barrage of brute-force login attempts. Fail2ban monitors log files for authentication failures, counts them per source IP, and when a configured threshold is crossed it issues a temporary ban by injecting a drop rule via firewalld or iptables. The ban is automatically lifted after a configurable duration — or left permanent for repeat offenders. On a RHEL 9 server, Fail2ban is particularly effective because it integrates directly with firewalld: each ban becomes a rich rule in the appropriate zone rather than a raw iptables rule.

This guide covers installing Fail2ban from EPEL, configuring the SSH jail, tuning ban duration and retry thresholds, building an incremental banning strategy, manually banning and unbanning IPs, and monitoring Fail2ban activity.

Prerequisites

  • RHEL 9 server with root or sudo access
  • SSH server running
  • firewalld active — Fail2ban will use it as the ban backend
  • EPEL repository enabled

Step 1 — Enable the EPEL Repository

Fail2ban is not in the default RHEL 9 BaseOS or AppStream repositories. It is provided by the EPEL (Extra Packages for Enterprise Linux) repository:

dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm

Verify EPEL is enabled:

dnf repolist | grep epel

Step 2 — Install Fail2ban

dnf install -y fail2ban fail2ban-firewalld

The fail2ban-firewalld package adds the firewalld action backend and the RHEL-appropriate default configuration. Without it, Fail2ban defaults to iptables which may conflict with firewalld on RHEL 9.

Enable and start the service:

systemctl enable --now fail2ban

Step 3 — Understand the Configuration File Hierarchy

Fail2ban uses a layered configuration system to keep your changes safe during package upgrades:

  • /etc/fail2ban/fail2ban.conf — global daemon settings (do not edit directly)
  • /etc/fail2ban/jail.conf — all jail defaults and pre-defined jails (do not edit directly)
  • /etc/fail2ban/fail2ban.local — your overrides to the global daemon config
  • /etc/fail2ban/jail.local — your global overrides to jail defaults
  • /etc/fail2ban/jail.d/ — individual .conf files, one per jail (preferred approach)

When Fail2ban starts it merges *.conf and *.local files in lexicographic order. Values in .local files and jail.d/ override their .conf counterparts. Never modify jail.conf directly: package updates will overwrite it.

Step 4 — Create the SSH Jail Configuration

Create a dedicated jail file for SSH:

vi /etc/fail2ban/jail.d/sshd.conf
[DEFAULT]
# Whitelist your own management IPs — never ban these
ignoreip = 127.0.0.1/8 ::1 203.0.113.50

# Number of seconds an IP is banned
bantime  = 3600

# Time window in which failures are counted (seconds)
findtime = 600

# Number of failures before ban
maxretry = 5

# Use firewalld as the ban action
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-allports

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/secure
maxretry = 5
findtime = 600
bantime  = 3600

Restart Fail2ban to apply the new jail:

systemctl restart fail2ban

Verify the jail is active:

fail2ban-client status
fail2ban-client status sshd

Step 5 — Tune the Ban Parameters

The default parameters above are reasonable for most servers, but should be adjusted based on risk level:

  • Low-value server, conservative settings: maxretry=5, findtime=600, bantime=3600 — ban after 5 failures in 10 min for 1 hour
  • Production server, tighter settings: maxretry=3, findtime=300, bantime=86400 — ban after 3 failures in 5 min for 24 hours
  • High-security server: maxretry=2, findtime=60, bantime=-1 — 2 failures in 1 min earns a permanent ban

A bantime of -1 means permanent until manually removed.

Step 6 — Implement Incremental Ban Lengths for Repeat Offenders

Fail2ban supports exponential back-off via the bantime.multiplier setting. This is more effective against sophisticated attackers who return after each ban expires:

vi /etc/fail2ban/jail.local
[DEFAULT]
# Enable incremental banning
bantime.increment = true

# Each ban is multiplied by this factor for repeat offenders
bantime.multiplier = 2

# Maximum ban length even with multiplier (30 days in seconds)
bantime.maxtime = 2592000

# Formula: if an IP was banned before within this window, apply multiplier
bantime.overalljails = false

With these settings, the ban lengths become: 1h → 2h → 4h → 8h → 16h → 32h, capping at 720 hours (30 days).

Step 7 — Manually Ban and Unban IP Addresses

# Manually ban an IP in the sshd jail
fail2ban-client set sshd banip 198.51.100.55

# Manually unban an IP
fail2ban-client set sshd unbanip 198.51.100.55

# List all currently banned IPs across all jails
fail2ban-client status | grep "Jail list" | sed 's/.*Jail list://g' | tr ',' 'n' | while read j; do
    echo "=== Jail: $j ==="; fail2ban-client status "$j" | grep "Banned IP"; done

Step 8 — Test the Configuration

Verify your own IP is in the ignore list before testing. Then deliberately trigger a ban from a test IP (or check the existing ban log):

# View the current action configuration
fail2ban-client -d | grep action

# Dry-run the filter against the actual log
fail2ban-regex /var/log/secure /etc/fail2ban/filter.d/sshd.conf

# Check if a specific IP is currently banned
fail2ban-client get sshd banip | grep 198.51.100.55

Step 9 — Add Jails for Other Services

Fail2ban can protect any service that logs failures. Add jails for Nginx, Postfix, or WordPress:

vi /etc/fail2ban/jail.d/nginx.conf
[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log
maxretry = 5

[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 10
findtime = 60

[wordpress-xmlrpc]
enabled  = true
port     = http,https
filter   = wordpress
logpath  = /var/log/nginx/access.log
maxretry = 3

Step 10 — Monitor Fail2ban in Real Time

# Live log stream
journalctl -fu fail2ban

# Count of bans per jail
fail2ban-client status sshd | grep "Total banned"

# View currently active ban rules in firewalld
firewall-cmd --zone=public --list-rich-rules | grep fail2ban

# Fail2ban log file
tail -f /var/log/fail2ban.log

Verification Checklist

# Service is running
systemctl status fail2ban

# sshd jail is enabled and watching logs
fail2ban-client status sshd

# No parse errors in configuration
fail2ban-client -d 2>&1 | grep -i error

# Your own IP is whitelisted
fail2ban-client get sshd ignoreip

Troubleshooting

  • Fail2ban starts but sshd jail shows 0 detected failures — check logpath. On RHEL 9 with systemd journal, SSH logs to /var/log/secure. If that file is empty, edit the jail to use backend = systemd instead of auto.
  • Ban is applied but attacker can still connect — confirm the ban action is firewalld, not iptables: fail2ban-client get sshd action. If it shows iptables-multiport, the fail2ban-firewalld package may not be installed, or banaction was not overridden in your jail.
  • Own IP was accidentally banned — run fail2ban-client set sshd unbanip YOUR_IP from a secondary access path (console, secondary server, VPN).

Security Considerations

  • Fail2ban is a rate-limiting tool, not an intrusion prevention system. It reduces noise and blocks unsophisticated scanners, but a distributed botnet using different IPs per attempt will not be stopped by it.
  • Always whitelist your management IPs in ignoreip. A misconfigured maxretry or a legitimate login failure loop can lock you out of your own server.
  • Combine Fail2ban with SSH key-only authentication for maximum effect. Fail2ban catches the attacks that get through; key auth ensures passwords cannot be brute-forced even if Fail2ban is temporarily down.

Conclusion

Fail2ban is now protecting your RHEL 9 server from SSH brute-force attacks. You have configured the SSH jail, tuned retry and ban duration parameters, set up incremental banning for repeat offenders, and know how to monitor and manually manage bans. The firewalld integration ensures bans are applied cleanly without conflicting with your existing network security rules.

Next steps: How to Configure Two-Factor Authentication for SSH on RHEL 9, How to Set a Hostname and FQDN on RHEL 9, and How to Monitor System Resources with htop and vmstat on RHEL 9.