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:
127
02-selfhosting/security/fail2ban-apache-bad-request-jail.md
Normal file
127
02-selfhosting/security/fail2ban-apache-bad-request-jail.md
Normal 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)
|
||||
Reference in New Issue
Block a user