From 954e124b49b2aedf7be489e9afbcf8d39696f2d2 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 5 Feb 2026 15:11:17 -0500 Subject: [PATCH] potentially fix fail2ban --- services/caddy.nix | 9 +++++---- tests/fail2ban-caddy.nix | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/services/caddy.nix b/services/caddy.nix index f31ea1d..856bb8b 100644 --- a/services/caddy.nix +++ b/services/caddy.nix @@ -92,13 +92,14 @@ in # Ignore local network IPs - NAT hairpinning causes all LAN traffic to # appear from the router IP (192.168.1.1). Banning it blocks all internal access. - # Browser subrequests for static assets (favicon.ico, etc.) without Authorization - # headers cause 401s that quickly trigger the ban threshold. ignoreip = "127.0.0.1/8 ::1 192.168.1.0/24"; }; filter.Definition = { - # Match Caddy JSON logs with 401 Unauthorized status (failed basic auth) - failregex = ''^.*"remote_ip":"".*"status":401.*$''; + # Only match 401s where an Authorization header was actually sent. + # Without this, the normal HTTP Basic Auth challenge-response flow + # (browser probes without credentials, gets 401, then resends with + # credentials) counts every page visit as a "failure." + failregex = ''^.*"remote_ip":"".*"Authorization":\["REDACTED"\].*"status":401.*$''; ignoreregex = ""; datepattern = ''"ts":{Epoch}\.''; }; diff --git a/tests/fail2ban-caddy.nix b/tests/fail2ban-caddy.nix index baf7829..54b11a9 100644 --- a/tests/fail2ban-caddy.nix +++ b/tests/fail2ban-caddy.nix @@ -46,7 +46,8 @@ pkgs.testers.runNixOSTest { maxretry = 3; # Lower for testing }; filter.Definition = { - failregex = ''^.*"remote_ip":"".*"status":401.*$''; + # Only match 401s where an Authorization header was actually sent + failregex = ''^.*"remote_ip":"".*"Authorization":\["REDACTED"\].*"status":401.*$''; ignoreregex = ""; datepattern = ''"ts":{Epoch}\.''; }; @@ -86,13 +87,28 @@ pkgs.testers.runNixOSTest { print(f"Curl result: {result}") assert "Authenticated" in result, f"Auth should succeed: {result}" - with subtest("Generate failed basic auth attempts"): + with subtest("Unauthenticated requests (browser probes) should not trigger ban"): + # Simulate browser probe requests - no Authorization header sent + # This is the normal HTTP Basic Auth challenge-response flow: + # browser sends request without credentials, gets 401, then resends with credentials + for i in range(5): + client.execute("curl -4 -s http://server/ || true") + time.sleep(0.5) + time.sleep(3) + status = server.succeed("fail2ban-client status caddy-auth") + print(f"caddy-auth jail status after unauthenticated requests: {status}") + match = re.search(r"Currently banned:\s*(\d+)", status) + banned = int(match.group(1)) if match else 0 + assert banned == 0, f"Unauthenticated 401s should NOT trigger ban, but {banned} IPs were banned: {status}" + + with subtest("Generate failed basic auth attempts (wrong password)"): # Use -4 to force IPv4 for consistent IP tracking + # These send an Authorization header with wrong credentials for i in range(4): client.execute("curl -4 -s -u testuser:wrongpass http://server/ || true") time.sleep(1) - with subtest("Verify IP is banned"): + with subtest("Verify IP is banned after wrong password attempts"): time.sleep(5) status = server.succeed("fail2ban-client status caddy-auth") print(f"caddy-auth jail status: {status}")