Why Fail2ban Belongs in Every Homelab

Within hours of exposing any SSH server or web application to the internet, you will see failed login attempts in your logs. Automated scanners cycle through credential lists, targeting SSH, web login pages, and API endpoints. Fail2ban is the simplest and most effective first line of defense — it reads log files, detects repeated failures, and temporarily bans the offending IP using the host firewall.

I run Fail2ban on every homelab host alongside Docker containers managed by Traefik. This guide covers the complete setup: SSH jail, Docker log integration for Traefik and Nginx, custom jails for specific applications, email alerts, and management commands.

What you will end up with:

  • SSH brute-force attempts blocked after 5 failures
  • Traefik and Nginx authentication failures banned automatically
  • Custom jails for any service that writes to logs
  • Email notifications when bans occur

Prerequisites

  • A Debian or Ubuntu host (works on any Linux distribution)
  • SSH server running on the host
  • Docker with containers whose logs you want to monitor (Traefik, Nginx, etc.)
  • Root or sudo access

Installing Fail2ban

Installation is straightforward on any Debian-based system:

1
2
3
sudo apt update
sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban

Verify the service is running:

1
2
sudo systemctl status fail2ban
sudo fail2ban-client ping

A successful ping returns pong.

Fail2ban ships with a default configuration file at /etc/fail2ban/jail.conf. Never edit this file directly — package updates will overwrite it. Instead, create a local override:

1
2
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

The jail.local file overrides the defaults. Set global values first, then configure individual jails.

SSH Jail Configuration

The SSH jail is the most important one. It monitors /var/log/auth.log for authentication failures and bans IPs that exceed the threshold.

Configure it in /etc/fail2ban/jail.local:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.factor = 2
banaction = nftables-multiport
action = %(action_mwl)s

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 5

Key settings explained:

  • bantime = 1h — initial ban duration
  • bantime.increment = true — doubles ban time for repeat offenders (1h → 2h → 4h → …)
  • banaction = nftables-multiport — uses nftables instead of legacy iptables (default on modern Debian/Ubuntu)
  • action = %(action_mwl)s — sends email with whois and log lines (requires MTA, see below)

Activate and check:

1
2
sudo fail2ban-client reload
sudo fail2ban-client status sshd

The status output shows total bans, currently banned IPs, and the log file being monitored.

Docker Log Integration with Traefik Jail

Docker containers write logs to journald or to a JSON file under /var/lib/docker/containers/ by default — not where Fail2ban expects them. You have two options:

Option A: Configure the container’s logging driver to use local files.

Add a bind mount in your Traefik compose file so the access log is accessible to Fail2ban on the host:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    command:
      - "--providers.docker"
      - "--entrypoints.websecure.address=:443"
      - "--accesslog=true"
      - "--accesslog.filepath=/var/log/traefik/access.log"
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/log/traefik:/var/log/traefik

Create the log directory on the host:

1
2
sudo mkdir -p /var/log/traefik
sudo chown 1000:1000 /var/log/traefik  # match traefik UID or use world-readable

Option B: Use syslog or journald logging driver.

If you prefer not to write log files inside containers, configure journald as the log driver for Traefik:

1
2
3
4
5
6
services:
  traefik:
    image: traefik:v3.3
    logging:
      driver: journald
    ...

Then use the journald backend in Fail2ban:

1
2
3
4
5
6
7
[traefik-auth]
enabled = true
backend = journald
journalmatch = CONTAINER_NAME=traefik
maxretry = 5
findtime = 10m
bantime = 1h

I recommend Option A — plain log files are easier to debug and work with fail2ban-regex for testing.

Traefik Jail Configuration

Create a jail configuration for Traefik:

1
sudo nano /etc/fail2ban/jail.d/traefik.conf
1
2
3
4
5
6
7
[traefik-auth]
enabled = true
port = http,https
logpath = /var/log/traefik/access.log
maxretry = 5
findtime = 10m
bantime = 1h

Now create the filter file. Traefik access logs have a different format from standard Nginx or Apache logs:

1
sudo nano /etc/fail2ban/filter.d/traefik-auth.conf
1
2
3
4
[Definition]
failregex = ^<HOST> - - \[.*\] "\w+ /.*" 401 .*$
            ^<HOST> - - \[.*\] "\w+ /.*" 403 .*$
ignoreregex =

This regex matches Traefik access log lines with HTTP 401 (Unauthorized) or 403 (Forbidden) status codes. These are the responses Traefik returns when authentication middleware rejects a request.

Test the regex with your actual logs:

1
sudo fail2ban-regex /var/log/traefik/access.log /etc/fail2ban/filter.d/traefik-auth.conf

Reload and verify:

1
2
sudo fail2ban-client reload
sudo fail2ban-client status traefik-auth

Nginx Jail for Docker Containers

If you use Nginx as a reverse proxy instead of Traefik, the setup is similar — bind mount the Nginx log directory to the host.

Example Nginx compose log mount:

1
2
3
4
5
services:
  nginx:
    image: nginx:alpine
    volumes:
      - /var/log/nginx:/var/log/nginx

Jail configuration (uses the built-in filter):

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

Nginx also has a built-in filter for nginx-botsearch — useful for blocking bots that probe for vulnerabilities like wp-admin, phpmyadmin, or .env files:

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

The aggressive maxretry = 2 ensures bots get banned after just two suspicious requests.

Creating Custom Jails

Any service that writes failure events to a log file can be protected. The recipe is always the same:

  1. Determine the log file path and format
  2. Write a regex to match failure lines
  3. Create the jail and filter files
  4. Test with fail2ban-regex

Example: Custom Jail for a Web Application

Suppose you run Nextcloud in Docker and want to ban IPs that fail authentication:

1
sudo nano /etc/fail2ban/filter.d/nextcloud-auth.conf
1
2
3
4
[Definition]
failregex = ^.*Login failed: '.*' \(Remote IP: '<HOST>'\).*$
            ^.*Trusted domain error\. \"<HOST>\" is not trusted.*$
ignoreregex =
1
sudo nano /etc/fail2ban/jail.d/nextcloud.conf
1
2
3
4
5
6
7
[nextcloud-auth]
enabled = true
port = http,https
logpath = /path/to/nextcloud/data/nextcloud.log
maxretry = 5
findtime = 10m
bantime = 1h

Always test before reloading:

1
sudo fail2ban-regex /path/to/nextcloud/data/nextcloud.log /etc/fail2ban/filter.d/nextcloud-auth.conf

The tool shows matched lines, unmatched lines, and a summary — invaluable for debugging regex patterns.

Email Alerts and Management

The %(action_mwl)s action sends an email with whois information and the most recent log lines from the offending IP. This requires a working MTA on the host.

Install msmtp for Lightweight Email

For a minimal MTA that works without a full mail server:

1
sudo apt install msmtp msmtp-mta -y

Configure /etc/msmtprc:

defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt

account        default
host           smtp.gmail.com
port           587
from           [email protected]
user           [email protected]
password       your-app-password

Test it:

1
echo "Fail2ban test" | msmtp [email protected]

Then in /etc/fail2ban/jail.local, set the sender and recipient:

1
2
3
4
5
6
[DEFAULT]
action = %(action_mwl)s
sendername = Fail2ban (homelab)
sender = [email protected]
mta = msmtp
destemail = [email protected]

Fail2ban Management Commands

Essential fail2ban-client commands for day-to-day management:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Check status of all jails
sudo fail2ban-client status

# Check specific jail
sudo fail2ban-client status sshd

# Unban an IP from all jails
sudo fail2ban-client unban 192.168.1.100

# Unban from a specific jail
sudo fail2ban-client set sshd unbanip 192.168.1.100

# Ban an IP manually
sudo fail2ban-client set sshd banip 192.168.1.100

# Reload configuration
sudo fail2ban-client reload

# View the log
sudo tail -f /var/log/fail2ban.log

Testing Your Setup

Test the SSH jail by intentionally failing SSH logins:

1
2
ssh invalid@localhost
# Enter wrong password 6 times

Then check:

1
sudo fail2ban-client status sshd

You should see the localhost IP (or your IP) listed in the “Banned IP list”. Remember to unban it afterward:

1
sudo fail2ban-client unban 127.0.0.1

Test the Traefik jail by hitting a protected endpoint with an invalid token or no authentication:

1
2
3
4
curl -k https://your-service.example.com/
# Should return 401 or 403
# Repeat 5+ times
sudo fail2ban-client status traefik-auth

Monitoring Fail2ban with Prometheus

For visibility at scale, the fail2ban_exporter exposes per-jail ban counts as Prometheus metrics. Run it alongside your existing monitoring stack:

1
2
3
4
5
6
7
8
services:
  fail2ban-exporter:
    image: ghcr.io/ribbybibby/fail2ban_exporter:latest
    ports:
      - "9191:9191"
    volumes:
      - /var/run/fail2ban/fail2ban.sock:/var/run/fail2ban/fail2ban.sock
    restart: unless-stopped

Then add a scrape target in Prometheus and a Grafana dashboard to track bans over time.

Putting It All Together

Fail2ban is a essential security layer for any homelab exposed to the internet. It integrates cleanly with Docker containers through bind-mounted log files, uses nftables for efficient firewall rules, and keeps you informed through email alerts.

Key takeaways:

  • Install and configure the SSH jail first — it is the highest-impact change
  • Use bind mounts or journald for Docker container log access
  • Always test new jails with fail2ban-regex before enabling them
  • Start with conservative thresholds (maxretry = 5, bantime = 1h) and tighten over time
  • Combine with CrowdSec, Traefik rate limiting, and firewall rules for defense in depth

Your homelab will thank you the first time you see hundreds of banned IPs piling up in the fail2ban log instead of filling your auth log.