security(fail2ban): widen jail to /forgot-password and rate-limit 429

The filter only watched /api/auth/login 401 — leaving the forgot-password
endpoint open to:

  - email enumeration (slow brute-force probing which addresses exist)
  - password-reset brute-force against accounts whose addresses leak

Widens the failregex to /api/auth/(login|forgot-password) and adds 429 to
the status alternation so a future in-app rate-limiter response is also
caught by the jail (defense in depth).

CI assertions extended to cover both new dimensions plus a negative case
on an unrelated 401 endpoint (/api/documents) — pins that the widening
did not over-match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 13:10:08 +02:00
parent 156afa14a2
commit 7e430998b8
2 changed files with 36 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
# fail2ban filter for credential-stuffing attempts against the
# Familienarchiv login endpoint.
# Familienarchiv authentication endpoints.
#
# Parses Caddy JSON access log entries (configured in
# infra/caddy/Caddyfile via the (access_log) snippet).
@@ -12,6 +12,16 @@
# "uri":"/api/auth/login",…},
# "status":401,…}
#
# Watched endpoints:
# - /api/auth/login — credential stuffing
# - /api/auth/forgot-password — email enumeration + slow brute-force
# against accounts whose addresses leak
#
# Watched statuses:
# - 401 — bad credentials
# - 429 — server-side rate limit (in case a future in-app limiter
# returns 429 before fail2ban catches the volume)
#
# Caddy emits remote_ip *inside* the request object and status at the
# top level. The order within the request object is stable
# (remote_ip → … → uri) across Caddy 2.7+. Lazy `.*?` keeps the regex
@@ -21,7 +31,7 @@
before = common.conf
[Definition]
failregex = ^\s*\{.*?"remote_ip":"<HOST>".*?"uri":"/api/auth/login.*?"status":\s*401\b
failregex = ^\s*\{.*?"remote_ip":"<HOST>".*?"uri":"/api/auth/(login|forgot-password).*?"status":\s*4(01|29)\b
ignoreregex =