
Fail2ban is an intrusion prevention tool that monitors log files for failed authentication attempts and automatically bans offending IP addresses using firewall rules. It is one of the first security tools you should deploy on any internet-facing Linux server, protecting services like SSH, Nginx, Apache, and mail servers from brute-force attacks.
This guide covers a full Fail2ban setup on Rocky Linux 10, AlmaLinux 10, and RHEL 10 – from installation through SSH protection, custom jails for web and mail services, email notifications, and monitoring. Every command is tested against the current EPEL packages for EL10.
What is Fail2ban
Fail2ban is an open-source log-parsing application written in Python. It scans log files (such as /var/log/secure or systemd journal) for patterns that indicate failed login attempts, password guessing, or other malicious behavior. When a configured threshold is reached, Fail2ban executes an action – typically adding a firewall rule through firewalld or nftables to block the offending IP address for a set period.
The core concept in Fail2ban is the jail. Each jail combines a filter (regex patterns that match log entries) with an action (what happens when the threshold is hit). You can have separate jails for SSH, Nginx, Postfix, and any other service – each with its own ban time, retry limit, and notification settings.
Prerequisites
- A server running Rocky Linux 10, AlmaLinux 10, or RHEL 10
- Root or sudo access
- Firewalld installed and running (default on all three distros)
- Internet access for package installation
Verify that firewalld is active before proceeding:
sudo systemctl status firewalld
The output should show active (running). If firewalld is not running, start and enable it with sudo systemctl enable --now firewalld.
Step 1: Install Fail2ban from EPEL
Fail2ban is not included in the default Rocky Linux / AlmaLinux repositories. It ships through the EPEL (Extra Packages for Enterprise Linux) repository.
Enable the EPEL repository:
sudo dnf install -y epel-release
On RHEL 10, use the following instead since epel-release is not available directly:
sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
Install Fail2ban along with the firewalld integration package:
sudo dnf install -y fail2ban fail2ban-firewalld
The fail2ban-firewalld package provides the ban action that integrates with firewalld instead of raw iptables. This is the recommended setup on RHEL-family systems where firewalld is the default firewall manager.
Step 2: Start and Enable Fail2ban
Enable and start the Fail2ban service so it survives reboots:
sudo systemctl enable --now fail2ban
Verify the service is running:
sudo systemctl status fail2ban
You should see the service as active with no errors in the output:
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: disabled)
Active: active (running) since Fri 2026-03-21 10:15:32 UTC; 5s ago
Docs: man:fail2ban(1)
Process: 1234 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS)
Main PID: 1235 (fail2ban-server)
Tasks: 3 (limit: 23060)
Memory: 12.4M
CPU: 245ms
CGroup: /system.slice/fail2ban.service
└─1235 /usr/bin/python3 -s /usr/bin/fail2ban-server -xf start
Step 3: Understanding Fail2ban Configuration
Fail2ban stores all configuration under /etc/fail2ban/. The directory structure looks like this:
/etc/fail2ban/jail.conf– the main default configuration file. Never edit this directly – it gets overwritten on package updates/etc/fail2ban/jail.local– your custom configuration. Settings here override anything injail.conf/etc/fail2ban/jail.d/– drop-in directory for per-jail configuration files/etc/fail2ban/filter.d/– filter definitions containing regex patterns for matching log entries/etc/fail2ban/action.d/– action definitions that specify what happens when a ban is triggered
The key parameters you will configure in most jails:
| Parameter | Description |
|---|---|
bantime | How long an IP stays banned (seconds, or use suffix like 1h, 1d) |
findtime | The time window in which maxretry failures must occur to trigger a ban |
maxretry | Number of failures allowed within findtime before banning |
banaction | The firewall action to use for banning (firewallcmd-rich-rules for firewalld) |
ignoreip | Space-separated list of IPs/CIDRs that are never banned |
backend | How Fail2ban reads logs – systemd for journal, auto for log files |
enabled | Set to true to activate a jail |
The fail2ban-firewalld package installs /etc/fail2ban/jail.d/00-firewalld.conf which sets banaction = firewallcmd-rich-rules and banaction_allports = firewallcmd-rich-rules. This makes all jails use firewalld by default.
Step 4: Configure SSH Protection
SSH brute-force attacks are the most common threat on any public-facing server. Create a jail.local file with default settings and an SSH jail.
Create the configuration file:
sudo vi /etc/fail2ban/jail.local
Add the following configuration:
[DEFAULT]
# Ban for 1 hour (3600 seconds)
bantime = 1h
# Look for failures within a 10-minute window
findtime = 10m
# Ban after 5 failed attempts
maxretry = 5
# Use systemd journal as log backend
backend = systemd
# Never ban these IPs (add your own management IPs)
ignoreip = 127.0.0.1/8 ::1 10.0.1.0/24
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 6h
findtime = 10m
The [DEFAULT] section applies to all jails unless overridden. The [sshd] jail is stricter – only 3 failed attempts are allowed before a 6-hour ban. Replace 10.0.1.0/24 in ignoreip with your actual management network or IP so you never lock yourself out.
If you have changed your SSH port, update the port value to match. For example, port = 2222.
Restart Fail2ban to apply the configuration:
sudo systemctl restart fail2ban
Verify the sshd jail is active:
sudo fail2ban-client status
The output confirms how many jails are running and lists them by name:
Status
|- Number of jail: 1
`- Jail list: sshd
Step 5: Test SSH Protection
Test the sshd jail by deliberately failing SSH logins from another machine. Attempt to log in with a wrong password multiple times until the ban threshold is reached.
After the failed attempts, check the sshd jail status:
sudo fail2ban-client status sshd
The output shows the number of failed attempts and any currently banned IPs:
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 4
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 192.168.1.50
You can also verify the firewalld rules that Fail2ban created. The banned IP appears as a rich rule in firewalld:
sudo firewall-cmd --list-rich-rules
The output shows a reject rule for the banned IP address:
rule family="ipv4" source address="192.168.1.50" port port="22" protocol="tcp" reject type="icmp-port-unreachable"
You can also check the Fail2ban log for ban events:
sudo tail -20 /var/log/fail2ban.log
Look for lines containing Ban to confirm the action was taken:
2026-03-21 10:25:14,123 fail2ban.actions [1235]: NOTICE [sshd] Ban 192.168.1.50
Step 6: Protect Additional Services
Fail2ban ships with filters for dozens of services out of the box. Add jails for your running services by appending them to /etc/fail2ban/jail.local.
Nginx Jails
Protect Nginx against HTTP authentication brute-force and bot scanners. Add these jails to your jail.local:
sudo vi /etc/fail2ban/jail.local
Append the following Nginx jail configuration:
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 1h
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 1d
The nginx-http-auth jail catches failed HTTP basic authentication attempts. The nginx-botsearch jail catches bots probing for common vulnerability paths like /wp-admin, /phpmyadmin, and similar URLs that return 404 errors.
Apache Jails
For servers running Apache (httpd), add these jails:
[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/httpd/error_log
maxretry = 3
bantime = 1h
[apache-badbots]
enabled = true
port = http,https
filter = apache-badbots
logpath = /var/log/httpd/access_log
maxretry = 2
bantime = 1d
Note the log paths – on RHEL-family systems, Apache logs to /var/log/httpd/ with error_log and access_log (no .log extension).
Postfix and Dovecot Jails
If you run a mail server with Postfix and Dovecot, protect against SMTP and IMAP brute-force attacks:
[postfix]
enabled = true
port = smtp,465,submission
filter = postfix
maxretry = 5
bantime = 1h
[postfix-sasl]
enabled = true
port = smtp,465,submission,imap,imaps,pop3,pop3s
filter = postfix[mode=auth]
maxretry = 3
bantime = 6h
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps,submission,465,sieve
filter = dovecot[mode=aggressive]
maxretry = 3
bantime = 6h
The postfix-sasl jail specifically targets SASL authentication failures, which are common in credential-stuffing attacks against mail servers. The dovecot jail uses aggressive mode to catch more authentication failure patterns.
WordPress Login Protection
WordPress does not have a built-in Fail2ban filter. You need to create a custom one. First, create the filter file:
sudo vi /etc/fail2ban/filter.d/wordpress.conf
Add the following filter definition that matches failed WordPress login attempts from the access log:
[Definition]
failregex = ^ .* "POST /wp-login\.php
^ .* "POST /xmlrpc\.php
ignoreregex =
Then add the WordPress jail to your jail.local:
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 5m
bantime = 1h
Change logpath to /var/log/httpd/access_log if you use Apache instead of Nginx.
After adding all the jails you need, restart Fail2ban:
sudo systemctl restart fail2ban
Verify all jails are loaded:
sudo fail2ban-client status
The jail list should show all the jails you enabled:
Status
|- Number of jail: 4
`- Jail list: nginx-botsearch, nginx-http-auth, sshd, wordpress
Step 7: Custom Filters
When a service does not have a built-in filter, you can write your own. A custom filter is a file in /etc/fail2ban/filter.d/ that contains regex patterns matching failed authentication lines in log files.
Here is an example custom filter for a web application that logs failed logins to its own log file. Create the filter:
sudo vi /etc/fail2ban/filter.d/myapp.conf
Add the filter definition with regex patterns that match your application’s log format:
[Definition]
# Match lines like: 2026-03-21 10:15:32 Authentication failure from 192.168.1.50 for user admin
failregex = ^.* Authentication failure from for user .*$
^.* Failed login attempt from .*$
ignoreregex = ^.* Authentication failure from for user root$
The <HOST> tag is a special Fail2ban placeholder that captures the IP address from the log line. The ignoreregex section excludes specific patterns you do not want to trigger a ban.
Before activating the filter, test it against your log file using the fail2ban-regex tool:
fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conf
The output shows how many lines matched and which IPs were captured. This confirms your regex patterns work before you deploy the jail:
Running tests
=============
Use failregex filter file : myapp, basedir: /etc/fail2ban
Use log file : /var/log/myapp/auth.log
Use encoding : UTF-8
Results
=======
Failregex: 12 total
|- #) [# of hits] regular expression
| 1) [8] ^.* Authentication failure from for user .*$
| 2) [4] ^.* Failed login attempt from .*$
`-
Ignoreregex: 2 total
Date template hits:
|- [# of hits] date format
| [14] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day 24hour:Minute:Second
`-
Lines: 14 lines, 0 ignored, 12 matched, 2 missed
Once the regex matches correctly, add the jail to your jail.local:
[myapp]
enabled = true
port = http,https
filter = myapp
logpath = /var/log/myapp/auth.log
maxretry = 5
bantime = 1h
Step 8: Unban an IP Address
If a legitimate user or your own IP gets banned, you can remove the ban immediately.
Unban a specific IP from a specific jail:
sudo fail2ban-client set sshd unbanip 192.168.1.50
Unban an IP from all jails at once:
sudo fail2ban-client unban 192.168.1.50
Unban every currently banned IP across all jails:
sudo fail2ban-client unban --all
To prevent accidental bans on your own IPs, always add your management IPs to the ignoreip setting in the [DEFAULT] section of jail.local.
Step 9: Email Notifications
Fail2ban can send email alerts when it bans an IP. This requires a working mail setup on the server – either a local MTA like Postfix or Sendmail.
Install the sendmail MTA if you do not have Postfix running:
sudo dnf install -y sendmail sendmail-cf
sudo systemctl enable --now sendmail
Update the [DEFAULT] section in your jail.local with email settings:
sudo vi /etc/fail2ban/jail.local
Add or update these parameters in the [DEFAULT] section:
[DEFAULT]
# Email settings
destemail = [email protected]
sender = [email protected]
mta = sendmail
# Action with email notification including whois and log lines
action = %(action_mwl)s
The action_mwl action bans the IP, sends an email with whois information, and includes the relevant log lines that triggered the ban. Other action options:
%(action_)s– ban only, no notification%(action_mw)s– ban and send email with whois info%(action_mwl)s– ban and send email with whois info plus log lines
For whois lookups in the email alerts, install the whois package:
sudo dnf install -y whois
Restart Fail2ban after changing the configuration:
sudo systemctl restart fail2ban
Step 10: Monitor Fail2ban
Check the overall Fail2ban status and all active jails:
sudo fail2ban-client status
Get detailed status for a specific jail, including banned IPs and failure counts:
sudo fail2ban-client status sshd
View Fail2ban’s own log file for ban and unban events:
sudo tail -50 /var/log/fail2ban.log
Use journalctl to check Fail2ban’s systemd journal entries:
sudo journalctl -u fail2ban -f
The -f flag follows the log in real time, which is useful when testing jails.
Check how many IPs a specific jail has banned in total:
sudo fail2ban-client get sshd banip --with-time
This displays each banned IP along with the time it was banned and when the ban expires.
Fail2ban Command Reference
The following table covers the most common fail2ban-client commands for daily administration:
| Command | Description |
|---|---|
fail2ban-client status | Show all active jails |
fail2ban-client status JAIL | Show details for a specific jail (banned IPs, failure count) |
fail2ban-client set JAIL unbanip IP | Unban a specific IP from a jail |
fail2ban-client unban IP | Unban an IP from all jails |
fail2ban-client unban --all | Unban all currently banned IPs |
fail2ban-client set JAIL banip IP | Manually ban an IP in a jail |
fail2ban-client get JAIL bantime | Show the current ban time for a jail |
fail2ban-client get JAIL maxretry | Show the max retry value for a jail |
fail2ban-client get JAIL findtime | Show the find time for a jail |
fail2ban-client get JAIL ignoreip | Show ignored IPs for a jail |
fail2ban-client set JAIL addignoreip IP | Add an IP to the ignore list at runtime |
fail2ban-client reload | Reload all jails and configuration |
fail2ban-client reload JAIL | Reload a specific jail |
fail2ban-client start | Start the Fail2ban server |
fail2ban-client stop | Stop the Fail2ban server |
fail2ban-client get JAIL banip --with-time | List banned IPs with ban and expiry times |
fail2ban-client ping | Check if the Fail2ban server is alive |
Troubleshooting
Here are the most common Fail2ban issues and how to fix them.
Fail2ban fails to start
Check the journal for error details:
sudo journalctl -u fail2ban -e --no-pager
Common causes include syntax errors in jail.local (missing brackets, wrong indentation) or a filter file referencing a non-existent log path. Fix the configuration and restart.
Jail is configured but not activating
Verify the jail appears in the status output:
sudo fail2ban-client status
If a jail is missing, check that enabled = true is set and that the filter file exists in /etc/fail2ban/filter.d/. Also verify the logpath points to an existing file. For jails using the systemd backend, make sure the service generates journal entries.
Firewalld conflicts
If Fail2ban bans are not working despite showing in fail2ban-client status, check that firewalld is running and that the fail2ban-firewalld package is installed. Verify rich rules are present:
sudo firewall-cmd --list-rich-rules
If rules are missing, check that /etc/fail2ban/jail.d/00-firewalld.conf exists and contains the correct banaction. If you had previously set banaction = iptables-multiport in jail.local, remove that line and let the firewalld drop-in take precedence.
Log path issues
On RHEL-family systems with systemd, many services log to the journal instead of traditional log files. If a jail is not detecting failures, switch the backend to systemd:
[sshd]
enabled = true
backend = systemd
For services that still use traditional log files (Nginx, Apache), verify the log path exists and that Fail2ban has read permission. Check with:
sudo ls -la /var/log/nginx/error.log
If SELinux is blocking Fail2ban from reading log files, check for AVC denials:
sudo ausearch -m AVC -ts recent
You may need to set the correct SELinux context on custom log files or create a policy module to allow Fail2ban access.
Conclusion
Fail2ban is now protecting your Rocky Linux 10 / AlmaLinux 10 server against brute-force attacks on SSH and any other services you configured. The Fail2ban wiki has additional filter examples and advanced configuration options for more specialized setups.
For production servers, consider setting longer ban times (24h or more) for repeat offenders, enabling incremental banning with bantime.increment = true, and monitoring the Fail2ban log as part of your regular server health checks.