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>
90 lines
3.3 KiB
Markdown
90 lines
3.3 KiB
Markdown
---
|
|
title: "Fail2ban: Enable the nginx-bad-request Jail"
|
|
domain: selfhosting
|
|
category: security
|
|
tags: [fail2ban, nginx, security, firewall, bad-request]
|
|
status: published
|
|
created: 2026-04-17
|
|
updated: 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`:
|
|
|
|
```ini
|
|
[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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
systemctl reload fail2ban
|
|
fail2ban-client status nginx-bad-request
|
|
```
|
|
|
|
You can also ban an IP manually while the jail is loading:
|
|
|
|
```bash
|
|
fail2ban-client set nginx-bad-request banip 185.177.72.70
|
|
```
|
|
|
|
## Verify It's Working
|
|
|
|
```bash
|
|
# 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](fail2ban-apache-404-scanner-jail.md).
|
|
|
|
## See Also
|
|
|
|
- [fail2ban-apache-bad-request-jail](fail2ban-apache-bad-request-jail.md) — Apache equivalent (no stock filter; custom filter required)
|
|
- [fail2ban-apache-404-scanner-jail](fail2ban-apache-404-scanner-jail.md)
|
|
- [fail2ban-apache-php-probe-jail](fail2ban-apache-php-probe-jail.md)
|