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:

  1. Monitor: Watch log files for patterns matching known attack signatures (filters)
  2. Count: Track the number of matches per source IP within a time window (findtime)
  3. Ban: When the threshold is exceeded (maxretry), execute a ban action (typically a firewall rule)
  4. 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
Warning: Never edit the default configuration files (/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
Tip: When running services in Docker, ensure your reverse proxy passes the real client IP via 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.