From ad69d7cb831887040d9dc1dd2f9429e09691f2b5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 11 May 2026 12:04:06 +0200 Subject: [PATCH] feat(infra): commit fail2ban jail for /api/auth/login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../filter.d/familienarchiv-auth.conf | 29 +++++++++++++++++++ infra/fail2ban/jail.d/familienarchiv.conf | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 infra/fail2ban/filter.d/familienarchiv-auth.conf create mode 100644 infra/fail2ban/jail.d/familienarchiv.conf diff --git a/infra/fail2ban/filter.d/familienarchiv-auth.conf b/infra/fail2ban/filter.d/familienarchiv-auth.conf new file mode 100644 index 00000000..6f06551f --- /dev/null +++ b/infra/fail2ban/filter.d/familienarchiv-auth.conf @@ -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":"".*?"uri":"/api/auth/login.*?"status":\s*401\b + +ignoreregex = + +# Caddy's ts field is a Unix epoch with sub-second precision. +datepattern = "ts":{EPOCH} diff --git a/infra/fail2ban/jail.d/familienarchiv.conf b/infra/fail2ban/jail.d/familienarchiv.conf new file mode 100644 index 00000000..e70d655f --- /dev/null +++ b/infra/fail2ban/jail.d/familienarchiv.conf @@ -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"]