Files
MajorWiki/02-selfhosting/security/fail2ban-nginx-bad-request-jail.md
MajorLinux 961ce75b88 Add 4 articles: nginx/apache bad-request jails, SSH fleet hardening, Watchtower localhost relay
All sourced from 2026-04-17 work sessions:
- fail2ban-nginx-bad-request-jail: enable stock jail (just needs wiring)
- fail2ban-apache-bad-request-jail: custom filter from scratch, no stock equivalent
- ssh-hardening-ansible-fleet: drop-in approach with Fedora/Ubuntu edge cases
- watchtower-smtp-localhost-relay: credential-free localhost postfix relay pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 21:06:09 -04:00

3.3 KiB

title, domain, category, tags, status, created, updated
title domain category tags status created updated
Fail2ban: Enable the nginx-bad-request Jail selfhosting security
fail2ban
nginx
security
firewall
bad-request
published 2026-04-17 2026-04-17

Fail2ban: Enable the nginx-bad-request Jail

The Problem

Automated scanners sometimes send malformed HTTP requests — empty request lines, truncated headers, or garbage data — that nginx rejects with a 400 Bad Request. These aren't caught by the default fail2ban jails (nginx-botsearch, nginx-http-auth) because those target URL-probe patterns and auth failures, not raw protocol abuse.

In a real incident: a single IP (185.177.72.70) sent 2,778 malformed requests in ~4 minutes, driving Netdata's web_log_1m_bad_requests to 93.7% and triggering a CRITICAL alert. The neighboring IP (185.177.72.61) was already banned — the /24 was known-bad and operating in shifts.

The Solution

fail2ban ships a nginx-bad-request filter out of the box. It's just not wired to a jail by default. Enabling it is a one-step drop-in.

Step 1 — Create the jail drop-in

Create /etc/fail2ban/jail.d/nginx-bad-request.conf:

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

Settings rationale:

  • maxretry = 10 — a legitimate browser never sends 10 malformed requests; this threshold catches burst scanners immediately
  • findtime = 60 — 60-second window; the attack pattern fires dozens of requests per minute
  • bantime = 1h — reasonable starting point; pair with recidive for repeat offenders

Step 2 — Verify the filter matches your log format

Before reloading, confirm the stock filter matches your nginx logs:

fail2ban-regex /var/log/nginx/access.log nginx-bad-request

In a real-world test against an active server this matched 2,829 lines with zero false positives.

Step 3 — Reload fail2ban

systemctl reload fail2ban
fail2ban-client status nginx-bad-request

You can also ban an IP manually while the jail is loading:

fail2ban-client set nginx-bad-request banip 185.177.72.70

Verify It's Working

# Check jail status and active bans
fail2ban-client status nginx-bad-request

# Watch bans in real time
tail -f /var/log/fail2ban.log | grep nginx-bad-request

# Confirm the jail is monitoring the right file
fail2ban-client get nginx-bad-request logpath

Key Notes

  • The stock filter is at /etc/fail2ban/filter.d/nginx-bad-request.conf — no need to create it.
  • If your [DEFAULT] section sets backend = systemd (common on Fedora/RHEL), add backend = polling to the jail or it will silently ignore logpath and monitor journald instead — where nginx doesn't write.
  • Make sure your Tailscale subnet (100.64.0.0/10) is in ignoreip under [DEFAULT] to avoid banning your own monitoring.
  • This jail targets 400 Bad Request responses. For 404 scanner detection, see fail2ban-apache-404-scanner-jail.

See Also