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"]