Fail2Ban Complete Guide: Protecting Your Server from Brute Force Attacks
Every server exposed to the internet is under constant attack. Automated bots cycle through password lists, scan for known vulnerabilities, and probe every open port. A typical SSH server sees thousands of brute force login attempts per day. Fail2Ban is the standard defense: it monitors log files for patterns indicating malicious behavior and automatically bans offending IP addresses using the firewall.
This guide covers Fail2Ban from basic installation through advanced custom jails, including Docker-specific configurations that many guides overlook.
How Fail2Ban Works
Fail2Ban operates on a simple but effective model:
- Monitor: Watch log files for patterns matching known attack signatures (filters)
- Count: Track the number of matches per source IP within a time window (findtime)
- Ban: When the threshold is exceeded (maxretry), execute a ban action (typically a firewall rule)
- Unban: After the ban duration expires (bantime), remove the firewall rule
The architecture is modular: filters define what to look for, jails combine filters with log paths and thresholds, and actions define what happens when a ban is triggered.
Installation and Basic Setup
# Debian/Ubuntu
sudo apt install fail2ban
# RHEL/CentOS/Fedora
sudo dnf install fail2ban
# Arch Linux
sudo pacman -S fail2ban
# Enable and start
sudo systemctl enable --now fail2ban
# Check status
sudo fail2ban-client status
/etc/fail2ban/jail.conf, /etc/fail2ban/fail2ban.conf) directly. They are overwritten on package updates. Always create override files in /etc/fail2ban/jail.local or /etc/fail2ban/jail.d/.
Global Defaults
# /etc/fail2ban/jail.local
[DEFAULT]
# Ban duration (1 hour)
bantime = 3600
# Time window for counting failures
findtime = 600
# Number of failures before ban
maxretry = 5
# What happens when Fail2Ban itself starts/stops
banaction = nftables-multiport
banaction_allports = nftables-allports
# Ignore local addresses
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
# Use systemd journal backend where available
backend = systemd
# Email notifications (optional)
destemail = [email protected]
sender = [email protected]
mta = sendmail
action = %(action_mwl)s
Progressive Banning
Fail2Ban supports increasing ban durations for repeat offenders:
# /etc/fail2ban/jail.d/recidive.conf
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
banaction = nftables-allports
bantime = 604800 # 1 week
findtime = 86400 # Look back 1 day
maxretry = 3 # 3 bans in a day = week-long ban
SSH Protection
SSH is the most critical service to protect. Here is a comprehensive SSH jail configuration:
# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
findtime = 600
bantime = 3600
# Aggressive mode - catches more attack patterns
mode = aggressive
The sshd filter in aggressive mode catches these patterns:
- Failed password authentication
- Invalid user attempts
- Failed publickey authentication
- Connection closed before authentication
- Too many authentication failures
- PAM authentication failures
# Check banned IPs for SSH
sudo fail2ban-client status sshd
# Output:
# Status for the jail: sshd
# |- Filter
# | |- Currently failed: 2
# | |- Total failed: 847
# | `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
# `- Actions
# |- Currently banned: 15
# |- Total banned: 423
# `- Banned IP list: 203.0.113.10 198.51.100.5 ...
# Manually ban an IP
sudo fail2ban-client set sshd banip 203.0.113.50
# Manually unban an IP
sudo fail2ban-client set sshd unbanip 203.0.113.50
# Check if a specific IP is banned
sudo fail2ban-client get sshd banned | grep "203.0.113.50"
Web Server Protection
Nginx Jails
# /etc/fail2ban/jail.d/nginx.conf
# Block repeated 401/403 errors (authentication probing)
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 600
bantime = 3600
# Block bots scanning for vulnerabilities
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 600
bantime = 86400
# Block excessive 4xx errors (path enumeration)
[nginx-4xx]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 30
findtime = 600
bantime = 3600
filter = nginx-4xx
Create the custom 4xx filter:
# /etc/fail2ban/filter.d/nginx-4xx.conf
[Definition]
failregex = ^ .* "(GET|POST|HEAD|PUT|DELETE|PATCH|OPTIONS) .* HTTP/\d\.\d" (400|401|403|404|405|408|429) .*$
ignoreregex = .*(\.css|\.js|\.png|\.jpg|\.jpeg|\.gif|\.ico|\.svg|\.woff|\.woff2).*
Apache Jails
# /etc/fail2ban/jail.d/apache.conf
[apache-auth]
enabled = true
port = http,https
logpath = %(apache_error_log)s
maxretry = 5
[apache-badbots]
enabled = true
port = http,https
logpath = %(apache_access_log)s
maxretry = 5
bantime = 172800
[apache-overflows]
enabled = true
port = http,https
logpath = %(apache_error_log)s
maxretry = 2
Custom Jails
You can create jails for any service that writes login attempts to a log file. Here are examples for common self-hosted applications:
Nextcloud
# /etc/fail2ban/filter.d/nextcloud.conf
[Definition]
failregex = ^.*Login failed: '.*' \(Remote IP: ''\).*$
^.*Trusted domain error\..* Remote IP: ''.*$
ignoreregex =
# /etc/fail2ban/jail.d/nextcloud.conf
[nextcloud]
enabled = true
filter = nextcloud
port = http,https
logpath = /var/log/nextcloud/nextcloud.log
maxretry = 5
findtime = 600
bantime = 3600
Vaultwarden
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect.*IP:\s*.*$
ignoreregex =
# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
filter = vaultwarden
port = http,https
logpath = /var/log/vaultwarden/vaultwarden.log
maxretry = 3
findtime = 600
bantime = 7200
Ban Actions
Fail2Ban supports multiple firewall backends for executing bans:
| Action | Firewall | Notes |
|---|---|---|
iptables-multiport |
iptables | Legacy, widely compatible |
nftables-multiport |
nftables | Modern replacement for iptables |
firewallcmd-rich-rules |
firewalld | For RHEL/Fedora with firewalld |
ufw |
UFW | For Ubuntu systems using UFW |
route |
Routing table | Null-routes the IP (no firewall needed) |
# Using nftables (recommended for modern systems)
# /etc/fail2ban/jail.local
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
# Verify the nftables rules
sudo nft list ruleset | grep fail2ban
# Using firewalld (RHEL/Fedora)
[DEFAULT]
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules
Email Notifications
# /etc/fail2ban/jail.local
[DEFAULT]
destemail = [email protected]
sender = fail2ban@$(hostname -f)
mta = sendmail
# Action levels:
# action_ = just ban
# action_mw = ban + whois report
# action_mwl = ban + whois + relevant log lines
action = %(action_mwl)s
For Slack or webhook notifications, create a custom action:
# /etc/fail2ban/action.d/slack-notify.conf
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -s -o /dev/null -X POST \
-H 'Content-type: application/json' \
--data '{"text":"[Fail2Ban] Banned from jail on %(hostname)s for s after failures"}' \
actionunban = curl -s -o /dev/null -X POST \
-H 'Content-type: application/json' \
--data '{"text":"[Fail2Ban] Unbanned from jail on %(hostname)s"}' \
[Init]
webhook_url = https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# Use the Slack action alongside the ban action
# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
action = nftables-multiport[name=sshd, port="ssh"]
slack-notify[name=sshd]
Docker Integration
Docker containers behind a reverse proxy present a challenge for Fail2Ban because the source IP may be in Docker's internal network range. There are two approaches:
Approach 1: Monitor the reverse proxy logs
# If Nginx runs on the host and proxies to Docker containers,
# monitor the host Nginx logs normally:
[nginx-proxy-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
# For Docker-based Nginx, mount the log volume:
# docker-compose.yml
services:
nginx:
image: nginx:alpine
volumes:
- nginx_logs:/var/log/nginx
# Then point Fail2Ban at the Docker volume mount:
logpath = /var/lib/docker/volumes/mystack_nginx_logs/_data/access.log
Approach 2: Docker-aware Fail2Ban with FORWARD chain
# Docker uses the FORWARD chain, not INPUT
# Standard iptables bans in INPUT don't affect Docker containers
# Custom action for Docker with iptables
# /etc/fail2ban/action.d/docker-iptables.conf
[Definition]
actionban = iptables -I DOCKER-USER -s -j DROP
actionunban = iptables -D DOCKER-USER -s -j DROP
# For nftables with Docker
# /etc/fail2ban/action.d/docker-nftables.conf
[Definition]
actionstart = nft add table ip fail2ban
nft add chain ip fail2ban docker-bans { type filter hook forward priority -1 \; }
actionban = nft add rule ip fail2ban docker-bans ip saddr drop
actionunban = nft delete rule ip fail2ban docker-bans handle $(nft -a list chain ip fail2ban docker-bans | grep | awk '{print $NF}')
actionstop = nft delete table ip fail2ban
X-Forwarded-For or X-Real-IP headers, and configure the upstream application to log the real IP rather than the Docker bridge IP. Without this, Fail2Ban will ban Docker's internal IP addresses, which is useless.
Whitelisting
# Global whitelist in jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
10.0.0.0/8
192.168.1.0/24
203.0.113.50 # Your static IP
# Per-jail whitelist
[sshd]
ignoreip = 10.0.0.0/8 192.168.1.100
# Whitelist by command (useful for dynamic IPs)
ignorecommand = /etc/fail2ban/scripts/check-whitelist.sh
Testing and Debugging
# Test a filter against a log file
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# Output shows matches:
# Results
# =======
# Failregex: 847 total
# Ignoreregex: 0 total
# Date template hits: 847
# Test with verbose output to see matched lines
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched
# Check jail status
sudo fail2ban-client status sshd
# View Fail2Ban's own log
sudo tail -f /var/log/fail2ban.log
# Reload configuration after changes
sudo fail2ban-client reload
# Reload a specific jail
sudo fail2ban-client reload sshd
Monitoring Fail2Ban
# Quick summary of all jails
sudo fail2ban-client status
# Detailed stats script
#!/bin/bash
echo "=== Fail2Ban Status ==="
for jail in $(sudo fail2ban-client status | grep "Jail list:" | sed 's/.*://;s/,/ /g'); do
banned=$(sudo fail2ban-client status "$jail" | grep "Currently banned:" | awk '{print $NF}')
total=$(sudo fail2ban-client status "$jail" | grep "Total banned:" | awk '{print $NF}')
printf "%-25s Currently: %-5s Total: %s\n" "$jail" "$banned" "$total"
done
Fail2Ban is a battle-tested tool that provides a significant first line of defense against automated attacks. Combined with SSH key authentication, proper firewall rules, and a network-level security solution like CrowdSec (which adds community intelligence), you can dramatically reduce your attack surface.