KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Fail2ban with nftables and Crowd-Sourced Blacklists

2026/05/13 in Security

Step 6: Configure all jails

Create /etc/fail2ban/jail.local. This overrides the package defaults and survives updates. All four jails go in one file:

[DEFAULT]
ignoreip  = 127.0.0.1/8 ::1 YOUR_SERVER_IP YOUR_HOME_RANGE

# ── SSH brute-force jail ──────────────────────────────────
[sshd]
enabled   = true
port      = ssh
mode      = aggressive
filter    = sshd
logpath   = /var/log/auth.log

maxretry  = 3
findtime  = 3600
bantime   = 604800

# Incremental banning — doubles the ban duration on repeat offenses
bantime.increment = true
bantime.factor    = 2
bantime.formula   = ban.Time * (1<<(ban.Count if ban.Count<8 else 8))
bantime.maxtime   = 2419200   # 4-week hard cap on exponential growth

action    = nftables-multiport[name=sshd, port="ssh", protocol=tcp]
            %(action_abuseipdb)s[abuseipdb_category="18,22"]

# ── Nginx forbidden-request jail ──────────────────────────
[nginx-forbidden]
enabled   = true
port      = http,https
filter    = nginx-forbidden
logpath   = /var/log/nginx/*error.log

maxretry  = 3
findtime  = 7200
bantime   = 604800

bantime.increment = true
bantime.factor    = 2
bantime.formula   = ban.Time * (1<<(ban.Count if ban.Count<8 else 8))
bantime.maxtime   = 2419200

action    = nftables-multiport[name=nginx-forbidden, port="80,443", protocol=tcp]
            %(action_abuseipdb)s[abuseipdb_category="19,21"]

# ── Rate-limit jail ───────────────────────────────────────
[nginx-ratelimit]
enabled   = true
port      = http,https
filter    = nginx-ratelimit
logpath   = /var/log/nginx/*.access.log
findtime  = 120
maxretry  = 5
bantime   = 86400
action    = nftables-multiport[name=nginx-ratelimit, port="80,443", protocol=tcp]

# ── 404-storm jail ────────────────────────────────────────
[nginx-404]
enabled   = true
port      = http,https
filter    = nginx-404
logpath   = /var/log/nginx/*.access.log
maxretry  = 15
findtime  = 120
bantime   = 7200
action    = nftables-multiport[name=nginx-404, port="80,443", protocol=tcp]

Add your own server IPs and trusted networks to ignoreip — you don't want to ban yourself during testing. Use CIDR notation for ranges (e.g., 2001:db8:abc::/48 for a home IPv6 prefix). The ignoreip in [DEFAULT] applies to all jails; you can override it per jail if needed.

How the incremental banning works:

Offense Formula Ban duration
1st ban.Time * 2 ~2 weeks
2nd ban.Time * 4 ~4 weeks
3rd ban.Time * 8 ~8 weeks
8th+ ban.Time * 256 ~4 weeks (capped by bantime.maxtime)

The formula caps at 8 offenses to avoid integer overflow. A repeat offender escalates quickly — two hits in a week puts them at 4 weeks. Three hits, 8 weeks. At 8 hits they're capped at 4 weeks. Without a bantime.maxtime, the formula would grow without bound — reaching ~2.5 years at 8 offenses — which would eventually bloat the nftables set and permanently lock out IPs on dynamic address pools. This punishes persistent bots far more harshly than one-off scanners while giving legitimate users who fat-finger a password plenty of time to recover before their ban expires.

Jail rationale at a glance:

sshd
3 failed auths in 1 hour → ~2 weeks (escalating)
Brute-force is always hostile. Incremental bans punish repeat offenders and report to AbuseIPDB.
nginx-forbidden
3× 403 in 2 hours → ~2 weeks (escalating)
Hitting blocked paths is always hostile. Each offense reported to AbuseIPDB.
nginx-ratelimit
5× 429 in 2 minutes → 1 day
Softer signal — aggressive browser prefetch or a shared office IP could trigger this once. Short ban keeps the penalty proportionate.
nginx-404
15× 404 in 2 minutes → 2 hours
Weakest signal — broken links happen. But 15 unique 404s in two minutes is scanning, not browsing. Short ban breaks momentum without punishing real users.

Configuring the PHP rate limit that feeds nginx-ratelimit:

Add a limit_req zone to your php.conf to throttle any PHP request regardless of response code. This catches bots that probe random framework paths (/core/secrets/config%2ejson, /django/settings%2epy) that fall through to your CMS and return 404 through the full PHP stack — consuming a worker and a database query each time.

# In nginx.conf http { } block — alongside existing limit_req_zone definitions
limit_req_zone $binary_remote_addr zone=phpzone:10m rate=5r/s;

# In php.conf
location ~ \.php$ {
    limit_req zone=phpzone burst=10 nodelay;
    limit_req_status 429;
    limit_conn phplimit 5;
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}

5 requests per second with a burst of 10. A legitimate browser loading a page with a few uncached assets stays under the burst. A bot firing 40 sequential probes at 2-second intervals stays under it too — but it can't speed up or go parallel without hitting the wall. The limit_req_status 429 returns HTTP 429 (Too Many Requests) instead of the default 503, giving fail2ban an unambiguous signal.

Why not use a keyword-based 404 filter instead? It's tempting to write a failregex that matches config, settings, or env in 404 paths. Don't. Legitimate visitors hit 404s containing those words — /wp-content/uploads/2023/env-photo.jpg that you deleted, /config-guide/ that someone bookmarked three years ago. The filter can't distinguish a scanner probing /core/secrets/config%2ejson from a real user following a stale link. Rate limiting the PHP handler avoids false positives entirely — it caps everyone equally, and only bots that sustain an unreasonable request rate get banned.

Why the 404-storm jail exists separately: Bots that probe random non-existent paths — /swagger-ui.html, /actuator/env, /sitemap.xml — generate 404s without ever hitting PHP (which would trigger a 429) and without hitting a deny all rule (which would trigger a 403). Real scanners like LeakIX fire off 30+ unique 404s in under a minute. The 404 jail's threshold — 15 unique 404s in 2 minutes — catches these without snagging normal traffic. The 2-hour ban is proportionate: long enough to break a scanner's momentum, short enough to not permanently lock out a legitimate user behind a misconfigured proxy.

  • ← Previous
  • 1
  • 2
  • 3
  • 4
  • Next →
Tags: fail2ban, nftables, abuseipdb, spamhaus, bitwire, blacklist, hardening
©2026 KBeezie | Disclaimer | Privacy Notice