Fail2ban: Automated Intrusion Prevention
Ban IPs based on failed authentication attempts. Essential protection for exposed services.
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
systemdfor 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.