Files
server-config/tests/fail2ban-jellyfin.nix

151 lines
4.2 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
https = {
domain = "test.local";
};
ports = {
jellyfin = 8096;
};
jellyfin = {
dataDir = "/var/lib/jellyfin";
cacheDir = "/var/cache/jellyfin";
};
media_group = "media";
};
testLib = lib.extend (
final: prev: {
serviceMountWithZpool =
serviceName: zpool: dirs:
{ ... }:
{ };
serviceFilePerms =
serviceName: tmpfilesRules:
{ ... }:
{ };
optimizePackage = pkg: pkg; # No-op for testing
}
);
jellyfinModule =
{ config, pkgs, ... }:
{
imports = [
(import ../services/jellyfin.nix {
inherit config pkgs;
lib = testLib;
service_configs = testServiceConfigs;
})
];
};
in
pkgs.testers.runNixOSTest {
name = "fail2ban-jellyfin";
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [
../modules/security.nix
jellyfinModule
];
# Create the media group
users.groups.media = { };
# Disable ZFS mount dependency
systemd.services."jellyfin-mounts".enable = lib.mkForce false;
systemd.services.jellyfin = {
wants = lib.mkForce [ ];
after = lib.mkForce [ ];
requires = lib.mkForce [ ];
};
# Override for faster testing and correct port
services.fail2ban.jails.jellyfin.settings = {
maxretry = lib.mkForce 3;
# In test, we connect directly to Jellyfin port, not via Caddy
port = lib.mkForce "8096";
};
# Create log directory and placeholder log file for fail2ban
# Jellyfin logs to files, not systemd journal
systemd.tmpfiles.rules = [
"d /var/lib/jellyfin/log 0755 jellyfin jellyfin"
"f /var/lib/jellyfin/log/log_placeholder.log 0644 jellyfin jellyfin"
];
# Make fail2ban start after Jellyfin
systemd.services.fail2ban = {
wants = [ "jellyfin.service" ];
after = [ "jellyfin.service" ];
};
# Give jellyfin more disk space and memory
virtualisation.diskSize = 3 * 1024;
virtualisation.memorySize = 2 * 1024;
};
client = {
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
import time
import re
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(8096)
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
time.sleep(2)
# Wait for Jellyfin to create real log files and reload fail2ban
server.wait_until_succeeds("ls /var/lib/jellyfin/log/log_2*.log", timeout=30)
server.succeed("fail2ban-client reload jellyfin")
with subtest("Verify jellyfin jail is active"):
status = server.succeed("fail2ban-client status")
assert "jellyfin" in status, f"jellyfin 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:8096/Users/authenticatebyname \
-H 'Content-Type: application/json' \
-H 'X-Emby-Authorization: MediaBrowser Client="test", Device="test", DeviceId="test", Version="1.0"' \
-d '{"Username":"baduser","Pw":"badpass"}' || true
""")
time.sleep(0.5)
with subtest("Verify IP is banned"):
time.sleep(3)
status = server.succeed("fail2ban-client status jellyfin")
print(f"jellyfin 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:8096/ 2>&1")[0]
assert exit_code != 0, "Connection should be blocked"
'';
}