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:
29
infra/fail2ban/filter.d/familienarchiv-auth.conf
Normal file
29
infra/fail2ban/filter.d/familienarchiv-auth.conf
Normal 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}
|
||||
27
infra/fail2ban/jail.d/familienarchiv.conf
Normal file
27
infra/fail2ban/jail.d/familienarchiv.conf
Normal 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"]
|
||||
Reference in New Issue
Block a user