feat(infra): commit fail2ban jail for /api/auth/login

Adds two files mirroring the on-host install layout:

  infra/fail2ban/filter.d/familienarchiv-auth.conf
  infra/fail2ban/jail.d/familienarchiv.conf

Filter parses the JSON access log emitted by Caddy (previous commit) and
matches 401 responses on /api/auth/login. Jail bans the offending IP for
30 min after 10 attempts in a 10-minute window.

Verified the failregex against four sample log lines via fail2ban-regex
in an alpine container:
  - 2 brute-force 401 attempts        → matched (ban)
  - 1 successful login (POST /api/auth/login 200) → not matched
  - 1 unrelated GET /login 200        → not matched
Date template "ts":{EPOCH} parses Caddy's Unix-epoch ts field.

The previous review iteration described this jail in DEPLOYMENT.md prose
only; committing it makes the security posture reproducible from a
fresh server build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 12:04:06 +02:00
parent 8d27c82e6d
commit ad69d7cb83
2 changed files with 56 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# fail2ban filter for credential-stuffing attempts against the
# Familienarchiv login endpoint.
#
# Parses Caddy JSON access log entries (configured in
# infra/caddy/Caddyfile via the (access_log) snippet).
#
# Sample matched line (whitespace inserted for readability):
# {"level":"info","ts":1700000000.12,"logger":"http.log.access",
# "msg":"handled request",
# "request":{"remote_ip":"203.0.113.42","method":"POST",
# "host":"archiv.raddatz.cloud",
# "uri":"/api/auth/login",…},
# "status":401,…}
#
# 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
# robust to header-dict size growth.
[INCLUDES]
before = common.conf
[Definition]
failregex = ^\s*\{.*?"remote_ip":"<HOST>".*?"uri":"/api/auth/login.*?"status":\s*401\b
ignoreregex =
# Caddy's ts field is a Unix epoch with sub-second precision.
datepattern = "ts":{EPOCH}

View File

@@ -0,0 +1,27 @@
# Jail definition for the Familienarchiv login endpoint.
#
# Install: ln -sf /opt/familienarchiv/infra/fail2ban/jail.d/familienarchiv.conf \
# /etc/fail2ban/jail.d/familienarchiv.conf
# ln -sf /opt/familienarchiv/infra/fail2ban/filter.d/familienarchiv-auth.conf \
# /etc/fail2ban/filter.d/familienarchiv-auth.conf
# systemctl reload fail2ban
#
# Verify with:
# fail2ban-client status familienarchiv-auth
# fail2ban-regex /var/log/caddy/access.log familienarchiv-auth
#
# Tuning rationale:
# - maxretry 10: legitimate users mistyping passwords don't trip the jail
# - findtime 10m: rolling window that catches automated brute force
# - bantime 30m: long enough to discourage scripted attacks, short
# enough that a user who fat-fingered their VPN comes
# back online within a coffee break
[familienarchiv-auth]
enabled = true
filter = familienarchiv-auth
logpath = /var/log/caddy/access.log
maxretry = 10
findtime = 10m
bantime = 30m
action = iptables-multiport[name=familienarchiv-auth, port="http,https"]