{ config, lib, pkgs, ... }: let testServiceConfigs = { zpool_ssds = ""; gitea = { dir = "/var/lib/gitea"; domain = "git.test.local"; }; postgres = { socket = "/run/postgresql"; }; ports = { gitea = 3000; }; }; testLib = lib.extend ( final: prev: { serviceMountWithZpool = serviceName: zpool: dirs: { ... }: { }; } ); giteaModule = { config, pkgs, ... }: { imports = [ (import ../services/gitea.nix { inherit config pkgs; lib = testLib; service_configs = testServiceConfigs; }) ]; }; in pkgs.testers.runNixOSTest { name = "fail2ban-gitea"; nodes = { server = { config, lib, pkgs, ... }: { imports = [ ../modules/security.nix giteaModule ]; # Enable postgres for gitea services.postgresql.enable = true; # Disable ZFS mount dependency systemd.services."gitea-mounts".enable = lib.mkForce false; systemd.services.gitea = { wants = lib.mkForce [ ]; after = lib.mkForce [ "postgresql.service" ]; requires = lib.mkForce [ ]; }; # Override for faster testing and correct port services.fail2ban.jails.gitea.settings = { maxretry = lib.mkForce 3; # In test, we connect directly to Gitea port, not via Caddy port = lib.mkForce "3000"; }; networking.firewall.allowedTCPPorts = [ 3000 ]; }; client = { environment.systemPackages = [ pkgs.curl ]; }; }; testScript = '' import time import re start_all() server.wait_for_unit("postgresql.service") server.wait_for_unit("gitea.service") server.wait_for_unit("fail2ban.service") server.wait_for_open_port(3000) time.sleep(3) with subtest("Verify gitea jail is active"): status = server.succeed("fail2ban-client status") assert "gitea" in status, f"gitea jail not found in: {status}" with subtest("Generate failed login attempts"): # Use -4 to force IPv4 for consistent IP tracking for i in range(4): client.execute( "curl -4 -s -X POST http://server:3000/user/login -d 'user_name=baduser&password=badpass' || true" ) time.sleep(0.5) with subtest("Verify IP is banned"): time.sleep(3) status = server.succeed("fail2ban-client status gitea") print(f"gitea jail status: {status}") # Check that at least 1 IP is banned match = re.search(r"Currently banned:\s*(\d+)", status) assert match and int(match.group(1)) >= 1, f"Expected at least 1 banned IP, got: {status}" with subtest("Verify banned client cannot connect"): # Use -4 to test with same IP that was banned exit_code = client.execute("curl -4 -s --max-time 3 http://server:3000/ 2>&1")[0] assert exit_code != 0, "Connection should be blocked" ''; }