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>
128 lines
4.2 KiB
Markdown
128 lines
4.2 KiB
Markdown
---
|
|
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)
|