Security beginner ⏱ 30 minutes

Fail2ban: Automated Intrusion Prevention

Ban IPs based on failed authentication attempts. Essential protection for exposed services.

By homelabbing.io • 2025-12-16
#fail2ban#security#intrusion-prevention#ssh#protection

Fail2ban is an intrusion prevention system that monitors log files and automatically bans IP addresses showing malicious behavior. For homelabs exposed to the internet, it’s an essential security layer that stops brute force attacks before they succeed.

Why Fail2ban is Essential

Without fail2ban, your services face constant automated attacks:

  • SSH brute force: Thousands of login attempts per day
  • Web application attacks: Authentication bypass attempts
  • API abuse: Credential stuffing, enumeration attacks
  • Bandwidth waste: Log bloat from repeated attempts
  • Security incidents: Eventually, weak passwords get cracked

Fail2ban provides:

  • Automatic protection: Bans happen without manual intervention
  • Flexible filtering: Custom rules for any log format
  • Multi-service support: Protect SSH, web, mail, custom apps
  • Whitelist capability: Never ban trusted IPs
  • Integration: Works with iptables, firewalld, CloudFlare

Installation

Native Installation

Ubuntu/Debian:

sudo apt update
sudo apt install fail2ban

RHEL/CentOS/Fedora:

sudo dnf install epel-release
sudo dnf install fail2ban fail2ban-firewalld

Verify installation:

sudo systemctl status fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Docker Installation

Docker Compose - docker-compose.yml:

version: '3.8'

services:
  fail2ban:
    container_name: fail2ban
    image: crazymax/fail2ban:latest
    restart: unless-stopped
    network_mode: host
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - /var/log:/var/log:ro
      - /var/log/auth.log:/var/log/auth.log:ro
      - ./fail2ban/data:/data
      - ./fail2ban/config:/etc/fail2ban
    environment:
      - TZ=America/New_York
      - F2B_LOG_LEVEL=INFO
      - F2B_DB_PURGE_AGE=30d

Deploy:

mkdir -p fail2ban/{data,config}
docker-compose up -d

Check status:

docker exec fail2ban fail2ban-client status

Core Concepts

Jails

A jail monitors specific log files for patterns and takes action:

  • Filter: Defines regex patterns to match attacks
  • Action: What to do when pattern matches (ban IP)
  • logpath: Which log files to monitor
  • maxretry: How many failures before ban
  • findtime: Time window to count failures
  • bantime: How long to ban the IP

Example: SSH jail bans IPs with 5 failed logins within 10 minutes for 1 hour.

Filters

Filters are regex patterns that identify malicious behavior in logs:

# Match failed SSH login
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error|failed) for .* from <HOST>

Actions

Actions define what happens when an IP is banned:

  • iptables: Add DROP rule
  • firewalld: Add rich rule to block
  • cloudflare: Add to CloudFlare firewall
  • email: Send notification

Basic Configuration

Configuration File Structure

  • /etc/fail2ban/fail2ban.conf: Main daemon config (don’t edit)
  • /etc/fail2ban/fail2ban.local: Override daemon settings
  • /etc/fail2ban/jail.conf: Default jail configuration (don’t edit)
  • /etc/fail2ban/jail.local: Your custom jail configuration
  • /etc/fail2ban/jail.d/: Additional jail files
  • /etc/fail2ban/filter.d/: Filter definitions
  • /etc/fail2ban/action.d/: Action definitions

Never edit .conf files directly - use .local files instead.

Initial Setup: /etc/fail2ban/jail.local

[DEFAULT]
# Ban settings
bantime = 1h
findtime = 10m
maxretry = 5

# IP whitelist - never ban these
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 10.0.0.0/8

# Email alerts (optional)
destemail = [email protected]
sender = [email protected]
mta = sendmail
action = %(action_mwl)s

# Backend (systemd, auto, polling)
backend = systemd

# SSH jail
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 10m
bantime = 1h

Reload configuration:

sudo systemctl restart fail2ban
sudo fail2ban-client reload

Checking Status

# Check fail2ban service
sudo systemctl status fail2ban

# List all jails
sudo fail2ban-client status

# Check specific jail
sudo fail2ban-client status sshd

# View banned IPs
sudo fail2ban-client get sshd banned

# View statistics
sudo fail2ban-client status sshd

Essential Jails

SSH Protection

Enhanced SSH jail in /etc/fail2ban/jail.d/sshd.local:

[sshd]
enabled = true
port = ssh,22
filter = sshd
logpath = /var/log/auth.log
backend = systemd
maxretry = 3
findtime = 10m
bantime = 24h

[sshd-ddos]
enabled = true
port = ssh,22
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 10
findtime = 2m
bantime = 1h

[sshd-aggressive]
enabled = true
port = ssh,22
filter = sshd
logpath = /var/log/auth.log
maxretry = 1
findtime = 1d
bantime = 1w

Web Server Protection (Nginx)

Create /etc/fail2ban/jail.d/nginx.local:

[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
findtime = 10m
bantime = 1h

[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 1m
bantime = 1h

[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
findtime = 5m
bantime = 24h

[nginx-noscript]
enabled = true
port = http,https
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 6
findtime = 10m
bantime = 1h

[nginx-bad-request]
enabled = true
port = http,https
filter = nginx-bad-request
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 5m
bantime = 1h

Custom Nginx filter for 404 abuse - /etc/fail2ban/filter.d/nginx-404.conf:

[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*" 404
ignoreregex =

Jail for 404 abuse:

[nginx-404]
enabled = true
port = http,https
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 2m
bantime = 30m

Apache Protection

Create /etc/fail2ban/jail.d/apache.local:

[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 3
findtime = 10m
bantime = 1h

[apache-badbots]
enabled = true
port = http,https
filter = apache-badbots
logpath = /var/log/apache2/access.log
maxretry = 2
findtime = 2d
bantime = 1w

[apache-noscript]
enabled = true
port = http,https
filter = apache-noscript
logpath = /var/log/apache2/error.log
maxretry = 6
findtime = 10m
bantime = 1h

[apache-overflows]
enabled = true
port = http,https
filter = apache-overflows
logpath = /var/log/apache2/error.log
maxretry = 2
findtime = 10m
bantime = 1h

Docker Integration

Protecting Docker Services

Docker containers need special handling since they have separate log paths:

Docker SSH jail - /etc/fail2ban/jail.d/docker-ssh.local:

[docker-ssh]
enabled = true
filter = sshd
logpath = /var/lib/docker/containers/*/*.log
action = docker-action
maxretry = 3
findtime = 10m
bantime = 24h

Custom Docker action - /etc/fail2ban/action.d/docker-action.conf:

[Definition]
actionstart =
actionstop =
actioncheck =
actionban = docker exec YOUR-CONTAINER-NAME iptables -I INPUT -s <ip> -j DROP
actionunban = docker exec YOUR-CONTAINER-NAME iptables -D INPUT -s <ip> -j DROP

Nginx Proxy Manager

For Nginx Proxy Manager Docker container:

Create filter - /etc/fail2ban/filter.d/npm-general.conf:

[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*" (404|403|400|401)
ignoreregex =

Jail - /etc/fail2ban/jail.d/npm.local:

[npm-general]
enabled = true
port = http,https
filter = npm-general
logpath = /path/to/npm/data/logs/proxy-host-*.log
maxretry = 10
findtime = 1m
bantime = 1h
action = iptables-allports[name=npm-general]

Protecting Specific Applications

Home Assistant

Filter - /etc/fail2ban/filter.d/homeassistant.conf:

[Definition]
failregex = ^%(__prefix_line)s<HOST>.*Login attempt or request with invalid authentication from
ignoreregex =

Jail:

[homeassistant]
enabled = true
port = 8123
filter = homeassistant
logpath = /var/log/homeassistant/home-assistant.log
maxretry = 5
findtime = 10m
bantime = 1h

Nextcloud

Filter - /etc/fail2ban/filter.d/nextcloud.conf:

[Definition]
failregex = \{"reqId":".*","level":2,"time":".*","remoteAddr":"<HOST>","user":".*","app":"core","method":".*","url":".*","message":"Login failed:
            \{"reqId":".*","level":2,"time":".*","remoteAddr":"<HOST>","user":".*","app":"core","method":".*","url":".*","message":"Bruteforce attempt from
ignoreregex =

Jail:

[nextcloud]
enabled = true
port = http,https
filter = nextcloud
logpath = /var/www/nextcloud/data/nextcloud.log
maxretry = 3
findtime = 10m
bantime = 1h

Jellyfin

Filter - /etc/fail2ban/filter.d/jellyfin.conf:

[Definition]
failregex = ^.*Authentication request for .* has been denied (IP: "<HOST>").
ignoreregex =

Jail:

[jellyfin]
enabled = true
port = 8096
filter = jellyfin
logpath = /var/log/jellyfin/*.log
maxretry = 3
findtime = 10m
bantime = 1h

Vaultwarden (Bitwarden)

Filter - /etc/fail2ban/filter.d/vaultwarden.conf:

[Definition]
failregex = ^.*Username or password is incorrect. Try again. IP: <HOST>. Username:.*$
ignoreregex =

Jail:

[vaultwarden]
enabled = true
port = http,https
filter = vaultwarden
logpath = /path/to/vaultwarden/vaultwarden.log
maxretry = 3
findtime = 10m
bantime = 24h

Advanced Configuration

Incremental Banning

Ban longer each time an IP reoffends:

Create /etc/fail2ban/action.d/iptables-repeater.conf:

[Init]
bantime = 3600

[Definition]
actionstart =
actionstop =
actioncheck =

actionban = BANTIME=<bantime>
            if [ -f /var/run/fail2ban/repeat-offender-<ip>.count ]; then
                COUNT=$(cat /var/run/fail2ban/repeat-offender-<ip>.count)
                COUNT=$((COUNT + 1))
                BANTIME=$((BANTIME * COUNT))
            else
                COUNT=1
            fi
            echo $COUNT > /var/run/fail2ban/repeat-offender-<ip>.count
            iptables -I f2b-<name> 1 -s <ip> -j DROP

actionunban = iptables -D f2b-<name> -s <ip> -j DROP

Use in jail:

[sshd]
enabled = true
filter = sshd
action = iptables-repeater[name=sshd]
logpath = /var/log/auth.log
maxretry = 3
bantime = 1h

Persistent Bans

Some IPs should stay banned permanently:

Create persistent ban database:

sudo touch /etc/fail2ban/ip.blocklist

Add to jail.local DEFAULT section:

[DEFAULT]
# Permanent ban list
banaction = iptables-multiport
            iptables-persistent[name=persistent]

Create persistent action - /etc/fail2ban/action.d/iptables-persistent.conf:

[Definition]
actionstart = if [ -f /etc/fail2ban/ip.blocklist ]; then
                  while read IP; do
                      iptables -I f2b-<name> 1 -s $IP -j DROP
                  done < /etc/fail2ban/ip.blocklist
              fi

actionban = echo "<ip>" >> /etc/fail2ban/ip.blocklist
            iptables -I f2b-<name> 1 -s <ip> -j DROP

actionunban = sed -i '/<ip>/d' /etc/fail2ban/ip.blocklist
              iptables -D f2b-<name> -s <ip> -j DROP

actionstop =

Manually add persistent ban:

echo "203.0.113.50" | sudo tee -a /etc/fail2ban/ip.blocklist
sudo fail2ban-client reload

CloudFlare Integration

Ban at CloudFlare edge (if using CF proxy):

Install CloudFlare action:

sudo curl -o /etc/fail2ban/action.d/cloudflare.conf https://raw.githubusercontent.com/fail2ban/fail2ban/master/config/action.d/cloudflare.conf

Configure /etc/fail2ban/action.d/cloudflare.local:

[Init]
cftoken = YOUR-CLOUDFLARE-API-TOKEN
cfuser = [email protected]

Use in jail:

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
action = iptables-multiport[name=nginx-http-auth]
         cloudflare[cfuser=%(cfuser)s, cftoken=%(cftoken)s]
maxretry = 3
bantime = 24h

Email Notifications

Get notified when bans happen:

Install sendmail:

sudo apt install sendmail

Configure in /etc/fail2ban/jail.local:

[DEFAULT]
# Email settings
destemail = [email protected]
sender = [email protected]
mta = sendmail

# Action with email (includes whois info)
action = %(action_mwl)s

Custom email action with ban details:

action = %(action_)s
         sendmail-whois-lines[name=%(__name__)s, dest=%(destemail)s, logpath=%(logpath)s]

Geo-Blocking

Block entire countries (use with caution):

Install geoip:

sudo apt install geoip-bin geoip-database

Filter - /etc/fail2ban/filter.d/geoip.conf:

[Definition]
failregex = <HOST>
ignoreregex =

Action - /etc/fail2ban/action.d/geoip.conf:

[Definition]
actionban = COUNTRY=$(geoiplookup <ip> | grep -Po '(?<=Country: )[A-Z]{2}')
            if [ "$COUNTRY" = "CN" ] || [ "$COUNTRY" = "RU" ]; then
                iptables -I INPUT -s <ip> -j DROP
            fi

actionunban = iptables -D INPUT -s <ip> -j DROP

Monitoring and Management

Command-Line Management

# Check all jails status
sudo fail2ban-client status

# Check specific jail
sudo fail2ban-client status sshd

# Manually ban IP
sudo fail2ban-client set sshd banip 203.0.113.50

# Manually unban IP
sudo fail2ban-client set sshd unbanip 203.0.113.50

# Reload configuration
sudo fail2ban-client reload

# Reload specific jail
sudo fail2ban-client reload sshd

# Get jail configuration
sudo fail2ban-client get sshd maxretry
sudo fail2ban-client get sshd bantime

# Set jail parameters (temporary)
sudo fail2ban-client set sshd maxretry 5
sudo fail2ban-client set sshd bantime 3600

Log Analysis

View fail2ban logs:

# Recent activity
sudo tail -f /var/log/fail2ban.log

# Search for bans
sudo grep "Ban" /var/log/fail2ban.log

# Search for specific IP
sudo grep "203.0.113.50" /var/log/fail2ban.log

# Count bans by jail
sudo grep "Ban" /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn

Statistics Script

Create /usr/local/bin/fail2ban-stats.sh:

#!/bin/bash

echo "=== Fail2ban Statistics ==="
echo ""

# Active jails
echo "Active Jails:"
sudo fail2ban-client status | grep "Jail list" | sed 's/.*Jail list://' | tr ',' '\n'
echo ""

# Current bans by jail
for JAIL in $(sudo fail2ban-client status | grep "Jail list" | sed 's/.*Jail list://' | tr ',' ' '); 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}')
    echo "$JAIL: $BANNED currently banned, $TOTAL total"
done
echo ""

# Top banned IPs
echo "Top Banned IPs (last 7 days):"
sudo journalctl -u fail2ban --since "7 days ago" | grep "Ban" | awk '{print $NF}' | sort | uniq -c | sort -rn | head -10
echo ""

# Bans by date
echo "Bans per day (last 7 days):"
sudo journalctl -u fail2ban --since "7 days ago" | grep "Ban" | awk '{print $1}' | sort | uniq -c

Make executable:

sudo chmod +x /usr/local/bin/fail2ban-stats.sh
sudo /usr/local/bin/fail2ban-stats.sh

Security Best Practices

Whitelisting

Always whitelist your own IPs to prevent self-lockout:

[DEFAULT]
ignoreip = 127.0.0.1/8
           ::1
           192.168.1.0/24
           10.0.0.0/8
           YOUR.PUBLIC.IP.ADDRESS

Dynamic whitelist for VPN users:

Add to /etc/fail2ban/jail.local:

ignorecommand = /usr/local/bin/fail2ban-ignore-command.sh <ip>

Script /usr/local/bin/fail2ban-ignore-command.sh:

#!/bin/bash
# Whitelist IPs in VPN subnet
IP=$1
if echo "$IP" | grep -q "^10\.8\."; then
    exit 0  # Ignore (whitelist)
else
    exit 1  # Don't ignore
fi

Tuning Parameters

Too aggressive? Legitimate users getting banned:

maxretry = 10    # Increase attempts allowed
findtime = 5m    # Shorter window
bantime = 30m    # Shorter ban

Too lenient? Attacks getting through:

maxretry = 3     # Fewer attempts
findtime = 1h    # Longer window (catches slow attacks)
bantime = 24h    # Longer ban

Balance: Start conservative, monitor logs, adjust based on false positives.

Regular Maintenance

Weekly tasks:

# Review banned IPs
sudo fail2ban-client status | grep "Jail list" | tr ',' '\n'

# Check for false positives
sudo fail2ban-client status sshd

# Clear old bans if needed
sudo fail2ban-client unban --all

Monthly tasks:

# Rotate logs
sudo logrotate -f /etc/logrotate.d/fail2ban

# Review and update filters
sudo fail2ban-client reload

# Update permanent ban list
sudo vim /etc/fail2ban/ip.blocklist

Database Cleanup

Fail2ban’s database can grow large:

# Check database size
du -h /var/lib/fail2ban/fail2ban.sqlite3

# Set auto-purge age in jail.local
[DEFAULT]
dbpurgeage = 30d

# Manual purge (bans older than 30 days)
sudo fail2ban-client set sshd dbpurgeage 30d

Troubleshooting

Jail Not Starting

Symptom: Jail shows as enabled but not active.

Check:

# View detailed error
sudo fail2ban-client -vvv reload sshd

# Check log file path exists
ls -la /var/log/auth.log

# Test filter regex
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# Check systemd backend
sudo fail2ban-client get sshd backend

Common issues:

  • Log file doesn’t exist or wrong path
  • Regex pattern doesn’t match log format
  • Wrong backend (use systemd for journalctl logs)

No Bans Happening

Symptom: Attacks visible in logs, but no bans.

Debug:

# Test filter against logs
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched

# Increase logging verbosity
sudo fail2ban-client set loglevel DEBUG

# Watch live
sudo tail -f /var/log/fail2ban.log

Check:

  • Filter regex not matching log format
  • maxretry too high
  • findtime too short
  • IP in whitelist

Self-Lockout

Prevention:

# Always whitelist your IP first
ignoreip = YOUR.IP.ADDRESS

Recovery (requires console/IPMI access):

# Stop fail2ban
sudo systemctl stop fail2ban

# Clear iptables rules
sudo iptables -F f2b-sshd

# Or disable fail2ban completely
sudo systemctl disable fail2ban

Unban yourself remotely (if you have another access method):

ssh admin@server-backup-ip
sudo fail2ban-client set sshd unbanip YOUR.IP.ADDRESS

Docker Container Issues

Symptom: Can’t access Docker container logs.

Solution:

# Check log path
docker inspect CONTAINER-NAME | grep LogPath

# Mount logs into fail2ban container
volumes:
  - /var/lib/docker/containers:/var/lib/docker/containers:ro

High Memory Usage

Symptom: fail2ban consuming excessive memory.

Solutions:

# Limit database size
[DEFAULT]
dbpurgeage = 7d
dbmaxmatches = 10

# Use polling instead of systemd
backend = polling

# Reduce log verbosity
loglevel = INFO

Real-World Use Cases

Case 1: Public Web Server

Scenario: Nginx reverse proxy for multiple services.

# Aggressive protection for public-facing services
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 2
findtime = 10m
bantime = 24h

[nginx-404-flood]
enabled = true
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 50
findtime = 1m
bantime = 6h

[nginx-ddos]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 20
findtime = 1m
bantime = 1h

Case 2: SSH Hardening

Scenario: SSH exposed to internet for remote access.

# Multi-layered SSH protection
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 10m
bantime = 1w

[sshd-ddos]
enabled = true
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 6
findtime = 1m
bantime = 24h

# Permanent ban after 3rd offense
[sshd-permanent]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 1
findtime = 30d
bantime = -1

Case 3: Home Assistant Security

Scenario: Home Assistant with external access.

[homeassistant]
enabled = true
port = 8123
filter = homeassistant
logpath = /home/homeassistant/.homeassistant/home-assistant.log
maxretry = 5
findtime = 15m
bantime = 24h
action = iptables-allports[name=homeassistant]

Filter:

[Definition]
failregex = ^%(__prefix_line)s<HOST>.*Login attempt or request with invalid authentication
            ^%(__prefix_line)s<HOST>.*Unauthorized
ignoreregex =

Integration with Monitoring

Prometheus Exporter

Monitor fail2ban metrics:

# Install exporter
docker run -d \
  --name fail2ban-exporter \
  -p 9191:9191 \
  -v /var/run/fail2ban:/var/run/fail2ban:ro \
  registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter

Prometheus config:

scrape_configs:
  - job_name: 'fail2ban'
    static_configs:
      - targets: ['localhost:9191']

Grafana Dashboard

Import dashboard ID: 11731 for fail2ban visualization.

Conclusion

Fail2ban is an essential security tool for any internet-facing homelab. Key takeaways:

  • Layer protection: Combine with firewall rules and strong authentication
  • Start simple: Enable SSH jail first, add others incrementally
  • Monitor regularly: Review bans weekly for false positives
  • Whitelist properly: Never lock yourself out
  • Tune parameters: Adjust based on your threat model and usage patterns

A well-configured fail2ban setup dramatically reduces attack surface and provides peace of mind for your homelab services.