Implement Fail2ban on AlmaLinux 9 / RHEL 9
Keep a second, out-of-band session open (console, IPMI/iLO, or a hypervisor console) before you start. Fail2ban bans IPs at the firewall based on log activity — a misconfigured jail or an aggressive maxretry value can lock out the very SSH session you're using to configure it. Never do this work over the only access path to a box.
This guide covers installing and configuring Fail2ban on AlmaLinux 9.8 and is directly portable to RHEL 9 — the two differ only in how you enable the EPEL repository, called out explicitly below. It complements SSH Hardening: SSH hardening reduces what an attacker can do once they reach the daemon, Fail2ban reduces how many attempts they get to try.
What Fail2ban Does
Fail2ban tails log files (SSH auth logs, by default, but also web servers, mail, etc.), matches failures against regex filters, and when a source IP exceeds a failure threshold within a time window, dynamically inserts a temporary firewall rule blocking that IP. It's a compensating control — brute-force mitigation, not a substitute for key-based auth, MFA, or network segmentation.
Prerequisites
- Root or
sudoaccess. firewalldactive and running (AlmaLinux/RHEL 9 default) — confirm withsystemctl status firewalld.- Note your current SELinux mode (
getenforce) — this guide assumes the default Enforcing. - Know your own management/admin source IP or subnet — you'll whitelist it explicitly so you can't be banned by your own testing.
1. Enable the EPEL Repository
Fail2ban isn't in the AlmaLinux/RHEL BaseOS or AppStream repos — it ships via EPEL (Extra Packages for Enterprise Linux). How you enable EPEL is the one real difference between the two distributions.
AlmaLinux 9.8
AlmaLinux's extras repo carries the convenience package directly:
dnf install epel-release -y
RHEL 9 Equivalent
RHEL doesn't ship the epel-release convenience package in its own repos, and some EPEL packages depend on the CodeReady Linux Builder (CRB) repo for build dependencies. Enable both:
subscription-manager repos --enable codeready-builder-for-rhel-9-$(arch)-rpms
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y
Verify on either distro:
dnf repolist | grep -i epel
2. Install Fail2ban
Install the core package and the firewalld action integration — AlmaLinux/RHEL 9 use firewalld as the default firewall backend (not raw iptables), so this subpackage matters:
dnf install fail2ban fail2ban-firewalld -y
3. Enable and Start the Service
systemctl enable --now fail2ban
systemctl status fail2ban
4. Configure via jail.local — Never Edit jail.conf Directly
/etc/fail2ban/jail.conf is the package-provided default and gets overwritten on every Fail2ban update. Fail2ban reads jail.conf first and lets anything in jail.local (and jail.d/*.conf) override it — put your changes there instead:
vi /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-allports
# Your management network / jump host — never leave this empty during initial testing
ignoreip = 127.0.0.1/8 ::1 10.10.5.0/24
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 4
bantime = 1h
A few things worth understanding rather than just pasting:
backend = systemdreads directly from the journal — appropriate here because AlmaLinux/RHEL 9 routesshdauth events throughjournald.%(sshd_log)sand%(sshd_backend)sresolve via/etc/fail2ban/paths-fedora.conf, which the package auto-selects on RHEL-family distros and points at/var/log/secure(the RHEL/AlmaLinux equivalent of Debian's/var/log/auth.log) with asystemdfallback.banaction = firewallcmd-rich-rulesusesfirewalldrich rules, which is the straightforward, human-inspectable option. For very high-volume environments,firewallcmd-ipsetscales better (bans go into an ipset instead of individual rich rules) — swap it in once volume justifies the extra complexity.- The
sshdfilter matches more than justFailed password— it also catchesInvalid user, disconnects before authentication, and repeatedFailed publickeyattempts. So it still provides value even on a host wherePasswordAuthentication nois already enforced.
5. Confirm the Firewalld Action Files Are Present
ls /etc/fail2ban/action.d/firewallcmd*
You should see firewallcmd-rich-rules.conf, firewallcmd-allports.conf, and related files — these come from the fail2ban-firewalld package installed in step 2. If they're missing, the jail will fail to start with an action-not-found error in fail2ban.log.
6. Restart and Verify
systemctl restart fail2ban
fail2ban-client status
fail2ban-client status sshd
Expected output for status sshd includes Currently failed, Total failed, Currently banned, and Banned IP list — all zero on a fresh install.
Confirm firewalld itself still allows SSH before relying on Fail2ban to protect it — Fail2ban blocks specific bad actors, it doesn't replace basic firewall service rules:
firewall-cmd --list-services
# If ssh is not listed:
firewall-cmd --add-service=ssh --permanent
firewall-cmd --reload
7. Test It — From a Second Source, Not Your Only Session
From a different machine or IP than your admin session (ideally one you're comfortable getting banned), deliberately fail SSH authentication past maxretry:
# Run this from the TEST source, not your management session
for i in {1..5}; do ssh -o PreferredAuthentications=password wronguser@<target-host>; done
Then, back on the target, confirm the ban landed:
fail2ban-client status sshd
firewall-cmd --list-rich-rules
You should see the test IP under Banned IP list and a matching rule family="ipv4" source address="<ip>" reject (or drop) entry in the rich rules output.
Unban an IP
fail2ban-client set sshd unbanip <ip-address>
Useful both for cleaning up your own test bans and for a legitimate user who got caught by a false positive (e.g., a shared NAT egress IP).
8. Persistence and Repeat-Offender Escalation
By default, Fail2ban's ban state lives only in memory and a SQLite database (/var/lib/fail2ban/fail2ban.sqlite3) used to survive service restarts — bans don't automatically persist across a reboot unless dbpurgeage and the database are intact, which they are by default. No extra config needed for standard restart persistence.
For repeat offenders — an IP that gets banned, waits out bantime, and comes back — enable the built-in recidive jail to apply escalating, much longer bans:
# /etc/fail2ban/jail.local — append
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 5
This watches Fail2ban's own log for hosts that trip multiple jails or get banned repeatedly, and escalates them to a week-long, all-ports ban.
9. SELinux Considerations
AlmaLinux/RHEL 9 run SELinux Enforcing by default, and this guide assumes that's unchanged (never disable SELinux as a troubleshooting shortcut). Fail2ban's EPEL package is built with SELinux-aware defaults and normally requires no policy changes to call firewall-cmd. If a jail fails to apply bans and fail2ban.log shows the action succeeding but firewall-cmd --list-rich-rules shows nothing, check for denials before assuming it's a Fail2ban bug:
ausearch -m avc -ts recent
10. Logging and Monitoring
Fail2ban logs its own activity to /var/log/fail2ban.log. Forward it to your SIEM alongside /var/log/secure so bans and the underlying auth failures that triggered them are correlated in one place, not siloed:
| Signal | Where |
|---|---|
| Raw SSH auth failures | /var/log/secure (journald-backed) |
| Ban/unban actions, jail start/stop | /var/log/fail2ban.log |
| Active bans right now | fail2ban-client status <jail> |
| Firewall rules currently in effect | firewall-cmd --list-rich-rules |
Alert on a high rate of new bans in a short window — it often indicates an active, broader scanning campaign against the host rather than an isolated attempt, and may warrant tightening ignoreip-scoped access further or engaging upstream blocking (WAF/cloud security group) in addition to host-level Fail2ban.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
dnf install fail2ban fails, package not found |
EPEL not enabled | Repeat step 1 for your distro; confirm with dnf repolist \| grep epel |
Jail sshd shows enabled but never bans |
logpath/backend mismatch, no matching log lines |
fail2ban-client status sshd; confirm journalctl -u sshd actually shows failures |
| Fail2ban logs the ban, but the IP still connects | firewalld action files missing, or wrong banaction |
Confirm fail2ban-firewalld package installed; check firewall-cmd --list-rich-rules |
| Legitimate admin got banned | ignoreip not set for the management subnet |
Add the subnet to ignoreip in jail.local, then fail2ban-client reload |
Action fails silently, firewall-cmd calls have no effect |
SELinux AVC denial | ausearch -m avc -ts recent; do not disable SELinux to "fix" this |
| Config changes don't take effect | Edited jail.conf instead of jail.local |
Move overrides to jail.local; jail.conf is overwritten on package updates |