potentially fix fail2ban

This commit is contained in:
2026-02-05 15:11:17 -05:00
parent a7d6018592
commit 954e124b49
2 changed files with 24 additions and 7 deletions

View File

@@ -92,13 +92,14 @@ in
# Ignore local network IPs - NAT hairpinning causes all LAN traffic to # 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. # 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"; ignoreip = "127.0.0.1/8 ::1 192.168.1.0/24";
}; };
filter.Definition = { filter.Definition = {
# Match Caddy JSON logs with 401 Unauthorized status (failed basic auth) # Only match 401s where an Authorization header was actually sent.
failregex = ''^.*"remote_ip":"<HOST>".*"status":401.*$''; # 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":"<HOST>".*"Authorization":\["REDACTED"\].*"status":401.*$'';
ignoreregex = ""; ignoreregex = "";
datepattern = ''"ts":{Epoch}\.''; datepattern = ''"ts":{Epoch}\.'';
}; };

View File

@@ -46,7 +46,8 @@ pkgs.testers.runNixOSTest {
maxretry = 3; # Lower for testing maxretry = 3; # Lower for testing
}; };
filter.Definition = { filter.Definition = {
failregex = ''^.*"remote_ip":"<HOST>".*"status":401.*$''; # Only match 401s where an Authorization header was actually sent
failregex = ''^.*"remote_ip":"<HOST>".*"Authorization":\["REDACTED"\].*"status":401.*$'';
ignoreregex = ""; ignoreregex = "";
datepattern = ''"ts":{Epoch}\.''; datepattern = ''"ts":{Epoch}\.'';
}; };
@@ -86,13 +87,28 @@ pkgs.testers.runNixOSTest {
print(f"Curl result: {result}") print(f"Curl result: {result}")
assert "Authenticated" in result, f"Auth should succeed: {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 # Use -4 to force IPv4 for consistent IP tracking
# These send an Authorization header with wrong credentials
for i in range(4): for i in range(4):
client.execute("curl -4 -s -u testuser:wrongpass http://server/ || true") client.execute("curl -4 -s -u testuser:wrongpass http://server/ || true")
time.sleep(1) time.sleep(1)
with subtest("Verify IP is banned"): with subtest("Verify IP is banned after wrong password attempts"):
time.sleep(5) time.sleep(5)
status = server.succeed("fail2ban-client status caddy-auth") status = server.succeed("fail2ban-client status caddy-auth")
print(f"caddy-auth jail status: {status}") print(f"caddy-auth jail status: {status}")