S
Home How-Tos Updated 2026-07-20

Implement Fail2ban on AlmaLinux 9 / RHEL 9

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 sudo access.
  • firewalld active and running (AlmaLinux/RHEL 9 default) — confirm with systemctl 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 = systemd reads directly from the journal — appropriate here because AlmaLinux/RHEL 9 route sshd auth events through journald. %(sshd_log)s and %(sshd_backend)s resolve 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 a systemd fallback.
  • banaction = firewallcmd-rich-rules uses firewalld rich rules, which is the straightforward, human-inspectable option. For very high-volume environments, firewallcmd-ipset scales better (bans go into an ipset instead of individual rich rules) — swap it in once volume justifies the extra complexity.
  • The sshd filter matches more than just Failed password — it also catches Invalid user, disconnects before authentication, and repeated Failed publickey attempts. So it still provides value even on a host where PasswordAuthentication no is 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

References

The Security Architecture Site — for internal reference use. Back to How-Tos