Introduction
Every internet-facing Linux server faces a constant stream of automated login attempts. Bots continuously scan public IP space probing SSH on port 22 (and common alternates), trying credential lists thousands of times per hour. Left unchecked, these attacks congest your logs, waste CPU cycles, and — if you have any weak credentials — eventually succeed.
Fail2ban is the standard answer. It tails your log files, detects repeated authentication failures matching configurable patterns, and instructs your firewall (iptables, nftables, or firewalld) to temporarily ban the offending IP. When the ban expires, the IP is unbanned automatically. No manual intervention required.
This guide walks through a production-ready Fail2ban setup: installation, SSH jail configuration, adding jails for Nginx and Apache, writing a custom filter, and verifying the system is actively defending your server.
Prerequisites
Before starting, ensure you have:
- A running Linux server reachable from the internet
systemdfor service management (standard on Ubuntu 22.04+, RHEL 9+)- A firewall backend —
ufw(Ubuntu) orfirewalld(RHEL/AlmaLinux) is fine - SSH access with
sudorights
Check your OS and kernel:
uname -r
cat /etc/os-release | grep PRETTY_NAMEStep 1 — Install Fail2ban
Ubuntu / Debian
sudo apt update
sudo apt install -y fail2banRHEL / AlmaLinux / Rocky Linux 9
Enable EPEL first, then install:
sudo dnf install -y epel-release
sudo dnf install -y fail2ban fail2ban-systemdVerify the install
fail2ban-client --version
# Fail2Ban v1.x.xStart and enable the service:
sudo systemctl enable --now fail2ban
sudo systemctl status fail2banStep 2 — Understand the Configuration Layout
Fail2ban splits its config into two layers to make upgrades safe:
| Path | Purpose |
|---|---|
/etc/fail2ban/fail2ban.conf | Global defaults (log level, socket path) |
/etc/fail2ban/jail.conf | Default jail definitions — do not edit directly |
/etc/fail2ban/fail2ban.d/ | Override directory for global settings |
/etc/fail2ban/jail.d/ | Override directory for jail config — edit here |
/etc/fail2ban/filter.d/ | Regex filter definitions for log parsing |
/etc/fail2ban/action.d/ | Ban/unban action scripts (iptables, nftables, etc.) |
The override directories (*.d/) take precedence over the base config files. Always put your customizations in .d/ so package upgrades don't clobber your changes.
Step 3 — Create a Local Jail Configuration
Create your primary override file:
sudo nano /etc/fail2ban/jail.d/local.confPaste the following, adjusting ignoreip to include your own trusted IPs:
[DEFAULT]
# Space-separated list of IPs/CIDRs to never ban
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 YOUR.ADMIN.IP.HERE
# Ban duration: 1 hour (use -1 for permanent)
bantime = 3600
# Window to count failures
findtime = 600
# Number of failures before ban
maxretry = 5
# Use systemd journal as log backend (modern default)
backend = systemd
# Email alerts (optional — set to your address)
# destemail = alerts@yourdomain.com
# sender = fail2ban@yourserver.com
# mta = sendmail
# action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 4
bantime = 7200Key parameters explained:
bantime— how long (seconds) an IP stays banned.3600= 1 hour.findtime— the sliding window (seconds) in which failures are counted.maxretry— failures withinfindtimebefore the ban triggers.ignoreip— never ban these. Include your admin IP to avoid locking yourself out.
Warning: Always add your management/VPN IP to
ignoreipbefore reloading. Banning yourself over SSH requires console access to recover.
Step 4 — Configure the SSH Jail in Detail
The [sshd] jail shown above uses the bundled sshd filter (/etc/fail2ban/filter.d/sshd.conf), which already handles most OpenSSH failure messages. Let's verify the filter matches your actual log entries.
First, find a failed SSH attempt in your journal:
journalctl -u ssh --since "1 hour ago" | grep "Failed\|Invalid\|Connection closed"Example output:
Jul 06 14:22:11 srv01 sshd[19842]: Failed password for invalid user admin from 185.220.101.45 port 52001 ssh2
Jul 06 14:22:13 srv01 sshd[19843]: Invalid user ubuntu from 185.220.101.45 port 52003
Test the filter against those lines:
sudo fail2ban-regex \
"Jul 06 14:22:11 srv01 sshd[19842]: Failed password for invalid user admin from 185.220.101.45 port 52001 ssh2" \
/etc/fail2ban/filter.d/sshd.confExpected output shows a match:
Results
=======
Failregex: 1 total
...
Lines: 1 lines, 0 ignored, 1 matched, 0 missed
If lines show "missed", the filter needs adjusting (rare with modern OpenSSH).
Step 5 — Add Jails for Web Services
Nginx (HTTP Auth / Bad Bots)
sudo nano /etc/fail2ban/jail.d/nginx.conf[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 6
[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 300Apache
sudo nano /etc/fail2ban/jail.d/apache.conf[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/error.log
maxretry = 6
[apache-badbots]
enabled = true
port = http,https
logpath = /var/log/apache2/access.log
bantime = 86400
maxretry = 2Step 6 — Write a Custom Filter
Suppose your application logs failed API logins like this in /var/log/myapp/auth.log:
2026-07-06T15:04:22Z [AUTH] Failed login for user "badguy" from 203.0.113.55
Create a filter file:
sudo nano /etc/fail2ban/filter.d/myapp-auth.conf[Definition]
failregex = \[AUTH\] Failed login for user ".*" from <HOST>
ignoreregex =The <HOST> token is a built-in Fail2ban macro that matches IPv4 and IPv6 addresses.
Test the filter against a real log line:
sudo fail2ban-regex \
'2026-07-06T15:04:22Z [AUTH] Failed login for user "badguy" from 203.0.113.55' \
/etc/fail2ban/filter.d/myapp-auth.confWire up the jail:
sudo nano /etc/fail2ban/jail.d/myapp.conf[myapp-auth]
enabled = true
port = 443
logpath = /var/log/myapp/auth.log
filter = myapp-auth
maxretry = 5
bantime = 3600Step 7 — Choose a Firewall Action
Fail2ban needs to know how to actually block IPs. The action is set globally in [DEFAULT] or per-jail.
Ubuntu with UFW
sudo nano /etc/fail2ban/jail.d/local.confAdd under [DEFAULT]:
banaction = ufwRHEL/AlmaLinux with firewalld
banaction = firewallcmd-rich-rulesPlain iptables (any distro)
banaction = iptables-multiportReload after any config change:
sudo fail2ban-client reloadStep 8 — Apply Configuration and Reload
After any edit to jail or filter files, reload without restarting:
sudo fail2ban-client reloadTo reload a single jail only:
sudo fail2ban-client reload sshdCheck that all enabled jails are running:
sudo fail2ban-client statusOutput should list your active jails:
Status
|- Number of jail: 4
`- Jail list: myapp-auth, nginx-http-auth, nginx-limit-req, sshd
Verification and Testing
View ban status for a jail
sudo fail2ban-client status sshdStatus for the jail: sshd
|- Filter
| |- Currently failed: 3
| |- Total failed: 47
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 2
|- Total banned: 12
`- Banned IP list: 185.220.101.45 45.155.205.10
Manually ban an IP (for testing)
sudo fail2ban-client set sshd banip 198.51.100.99Verify with iptables:
sudo iptables -L f2b-sshd -n --line-numbersManually unban an IP
sudo fail2ban-client set sshd unbanip 198.51.100.99Watch bans happen in real time
sudo journalctl -fu fail2banLook for lines like:
fail2ban.actions [INFO] Ban 185.220.101.45
fail2ban.actions [INFO] Unban 45.155.205.10
Simulate SSH brute force (from a safe test IP)
On a second machine you don't mind getting banned, attempt several failed SSH logins:
for i in {1..6}; do ssh baduser@YOUR_SERVER_IP; doneWithin seconds, the IP should appear in fail2ban-client status sshd.
Troubleshooting
Fail2ban not starting:
Check the config for syntax errors:
sudo fail2ban-client --testJail not triggering bans:
- Confirm the log path exists and is readable:
sudo ls -l /var/log/auth.log - Test the filter directly with
fail2ban-regexagainst a real log line. - Check backend: if using
systemd, thelogpathis ignored — the journal is used. - Increase verbosity temporarily in
/etc/fail2ban/fail2ban.conf: setloglevel = DEBUG, reload, then checkjournalctl -u fail2ban.
You banned yourself:
If you can still reach the server another way (console, second IP), unban yourself:
sudo fail2ban-client set sshd unbanip YOUR.IP.ADDRESSIf locked out entirely, access via your hosting provider's out-of-band console and run the above. Then add your IP to ignoreip immediately.
Bans not persisting across Fail2ban restarts:
By default, bans are in-memory only. To persist them across restarts, add to [DEFAULT]:
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 86400Fail2ban will restore active bans from the database on startup.
High log volume causing lag:
If findtime is long and logs are large, Fail2ban can consume CPU scanning history. Reduce findtime or switch from pyinotify to systemd backend for better performance.
Summary
You now have Fail2ban defending your server with:
- SSH jail with tightened
maxretryand extendedbantime - Nginx/Apache jails blocking web authentication abuse
- Custom application filter matched to your own log format
- Safe ignoreip list protecting your admin IPs from self-banning
- Persistent ban database surviving service restarts
Fail2ban is a lightweight but effective first line of defence against automated attacks. Pair it with SSH key-only authentication (disable password login entirely in sshd_config) and a firewall that restricts SSH to known CIDR ranges where possible. For deeper intrusion detection, consider layering Fail2ban with CrowdSec for community threat intelligence, or Wazuh SIEM/XDR for centralized log correlation across your fleet.