freedzhost.cloud

Homelab 18: ban the scanners with fail2ban, and the Docker trap that makes bans useless

Level IntermediateEstimated time 45 minRead first Homelab 2: one HTTPS entrance for all your servicesA word you don't know? Glossary

As soon as ports 80 and 443 are open, scanners arrive: thousands of requests for admin pages, old exploits, passwords guessed in a loop. fail2ban reads your proxy's logs and bans an address for a while after a few errors. It is simple and effective, with one condition that a Docker setup quietly breaks.

AttackerThe host's firewallINPUTdefault ban lands here: not crossedFORWARD → DOCKER-USERthe proxy's traffic goes hereProxy containerfail2ban5 errors in 5 min → ban 1 hits logsban here
fail2ban reads the proxy's logs and bans an address after too many errors. If the proxy runs in Docker, its traffic is forwarded to the container and never crosses INPUT: a ban written there blocks nothing. Write it in DOCKER-USER.

A bit of history

fail2ban dates from 2004. Its idea has not aged: read the logs, count failures per address, and when an address fails too often, add a firewall rule against it for a while. It knows nothing about attacks; it only knows patterns in log lines, which is why a pattern that does not match your log format leaves it silently blind.

1. A filter for the proxy's logs

For Nginx Proxy Manager, a filter that catches errors and refused requests:

# filter.d/npm-docker.conf
[Definition]
failregex = ^.* \[error\] \d+#\d+: \*\d+ .*client: <HOST>,.*$
            ^<HOST> .* "(GET|POST|HEAD).*" (401|403|404|429) .*$

Test it against a real log before trusting it: fail2ban-regex /path/to/proxy-host-1_access.log filter.d/npm-docker.conf shows how many lines it would catch.

2. A jail, in the right chain

# jail.d/npm.conf
[npm-docker]
enabled  = true
filter   = npm-docker
logpath  = /var/log/npm/proxy-host-*_access.log
           /var/log/npm/proxy-host-*_error.log
maxretry = 5
findtime = 300
bantime  = 3600
chain    = DOCKER-USER
action   = iptables-multiport[name=npm, port="80,443", protocol=tcp, chain="DOCKER-USER"]

Five errors in five minutes, one hour of ban.

The Docker trap

By default, fail2ban writes its bans in the INPUT chain of the firewall. Traffic to a port published by Docker is forwarded to the container and never crosses INPUT: the ban exists, the counters rise, and the attacker keeps going. Docker provides a chain for your own rules on that path, DOCKER-USER: put the ban there.

Check where the traffic really goes, not only that a rule exists: the packet counters tell you.

sudo nft list ruleset | grep -A3 f2b      # or: sudo iptables -L DOCKER-USER -v -n

Then ban an address you control, from your phone on mobile data, and check that the site stops answering it.

Traps

Do not ban your own network. Add your local range and your private network to ignoreip, or a misbehaving script at home locks you out of your own services.

A 404 alone is not an attack: a broken link on your site produces them too. Five in five minutes from the same address is a reasonable threshold; ban on your own logs for a week in "count only" mode first, and look at what would have been banned.

Sources

Found this useful? Tell us what to measure next.