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>
This commit is contained in:
2026-04-17 21:06:06 -04:00
parent 9c1a8c95d5
commit 961ce75b88
4 changed files with 459 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
---
title: "Fail2ban Custom Jail: Apache Bad Request Detection"
domain: selfhosting
category: security
tags: [fail2ban, apache, security, firewall, bad-request]
status: published
created: 2026-04-17
updated: 2026-04-17
---
# Fail2ban Custom Jail: Apache Bad Request Detection
## The Problem
fail2ban ships a stock `nginx-bad-request` filter for catching malformed HTTP requests (400s), but **there is no Apache equivalent**. Apache servers are left unprotected against the same class of attack: scanners that send garbage request lines to probe for vulnerabilities or overwhelm the access log.
Unlike the nginx version, this filter has to be written from scratch.
## The Solution
Create a custom filter targeting **400 Bad Request** responses in Apache's Combined Log Format, then wire it to a jail.
### Step 1 — Create the filter
Create `/etc/fail2ban/filter.d/apache-bad-request.conf`:
```ini
# Fail2Ban filter: catch 400 Bad Request responses in Apache access logs
# Targets malformed HTTP requests — garbage request lines, empty methods, etc.
# No stock equivalent exists; nginx-bad-request ships with fail2ban but Apache does not.
[Definition]
# Match 400 responses in Apache Combined/Common Log Format
failregex = ^<HOST> -.*".*" 400 \d+
ignoreregex =
datepattern = %%d/%%b/%%Y:%%H:%%M:%%S %%z
```
### Step 2 — Validate the filter
Always test before deploying:
```bash
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/apache-bad-request.conf
```
Against a live server under typical traffic this matched **155 lines with zero false positives**. If you see unexpected matches, refine `ignoreregex`.
### Step 3 — Create the jail drop-in
Create `/etc/fail2ban/jail.d/apache-bad-request.conf`:
```ini
[apache-bad-request]
enabled = true
port = http,https
filter = apache-bad-request
logpath = /var/log/apache2/access.log
maxretry = 10
findtime = 60
bantime = 1h
```
> **Note:** On Fedora/RHEL, the log path may be `/var/log/httpd/access_log`. If your `[DEFAULT]` sets `backend = systemd`, add `backend = polling` to the jail — otherwise it silently ignores `logpath` and reads journald instead.
### Step 4 — Reload fail2ban
```bash
systemctl reload fail2ban
fail2ban-client status apache-bad-request
```
## Deploy Fleet-Wide with Ansible
If you run multiple Apache hosts, use Ansible to deploy both the filter and jail atomically:
```yaml
- name: Deploy apache-bad-request fail2ban filter
ansible.builtin.template:
src: templates/fail2ban_apache_bad_request_filter.conf.j2
dest: /etc/fail2ban/filter.d/apache-bad-request.conf
notify: Reload fail2ban
- name: Deploy apache-bad-request fail2ban jail
ansible.builtin.template:
src: templates/fail2ban_apache_bad_request_jail.conf.j2
dest: /etc/fail2ban/jail.d/apache-bad-request.conf
notify: Reload fail2ban
```
## Why Not Use nginx-bad-request on Apache?
The `nginx-bad-request` filter parses nginx's log format, which differs from Apache's Combined Log Format. The timestamp format, field ordering, and quoting differ enough that the regex won't match. You need a separate filter.
| | nginx-bad-request | apache-bad-request |
|---|---|---|
| Ships with fail2ban | ✅ Yes | ❌ No — must write custom |
| Log source | nginx access log | Apache access log |
| What it catches | 400 responses (malformed requests) | 400 responses (malformed requests) |
| Regex target | nginx Combined Log Format | Apache Combined Log Format |
## Diagnostic Commands
```bash
# Validate filter against live log
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/apache-bad-request.conf
# Check jail status
fail2ban-client status apache-bad-request
# Confirm the jail is monitoring the correct log file
fail2ban-client get apache-bad-request logpath
# Watch bans in real time
tail -f /var/log/fail2ban.log | grep apache-bad-request
# Count 400s in today's access log
grep '" 400 ' /var/log/apache2/access.log | wc -l
```
## See Also
- [fail2ban-nginx-bad-request-jail](fail2ban-nginx-bad-request-jail.md) — the nginx equivalent (stock filter, just needs wiring)
- [fail2ban-apache-404-scanner-jail](fail2ban-apache-404-scanner-jail.md) — catches 404 probe scanners
- [fail2ban-apache-php-probe-jail](fail2ban-apache-php-probe-jail.md)