From 127764a06d3524ec149b4b50430ff3fe0fa5db04 Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Sat, 22 Mar 2025 23:07:58 +0100 Subject: [PATCH 01/13] chore: introduce central api key extraction service generator --- nixarr/lib/api-keys.nix | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 nixarr/lib/api-keys.nix diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix new file mode 100644 index 0000000..62b1d7d --- /dev/null +++ b/nixarr/lib/api-keys.nix @@ -0,0 +1,79 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.nixarr; + + # Helper to create API key extraction for a service + mkApiKeyExtractor = serviceName: serviceConfig: { + description = "Extract ${serviceName} API key"; + after = ["${serviceName}.service"]; + requires = ["${serviceName}.service"]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + # Use DynamicUser if the parent service does + DynamicUser = serviceConfig.serviceConfig.DynamicUser or false; + # Only set User if not using DynamicUser + ${ + if !(serviceConfig.serviceConfig.DynamicUser or false) + then "User" + else null + } = + serviceConfig.user or null; + Group = "api-keys"; + UMask = "0027"; # Results in 0640 permissions + + ExecStartPre = [ + "${pkgs.coreutils}/bin/mkdir -p ${cfg.stateDir}/api-keys" + "${pkgs.coreutils}/bin/chown root:api-keys ${cfg.stateDir}/api-keys" + "${pkgs.coreutils}/bin/chmod 750 ${cfg.stateDir}/api-keys" + # Wait for config file to exist + "${pkgs.bash}/bin/bash -c 'while [ ! -f ${serviceConfig.stateDir}/config.xml ]; do sleep 1; done'" + ]; + + ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" '' + ${pkgs.dasel}/bin/dasel -f "${serviceConfig.stateDir}/config.xml" \ + -s ".Config.ApiKey" | tr -d '\n\r' > "${cfg.stateDir}/api-keys/${serviceName}.key" + chown $USER:api-keys "${cfg.stateDir}/api-keys/${serviceName}.key" + ''; + }; + }; +in { + config = mkIf cfg.enable { + users.groups.api-keys = {}; + + # Ensure all services that need API keys are in the group + users.users = mkMerge [ + # Static users + (mkIf cfg.transmission.enable {torrenter.extraGroups = ["api-keys"];}) + (mkIf cfg.transmission.privateTrackers.cross-seed.enable {cross-seed.extraGroups = ["api-keys"];}) + ]; + + # Add api-keys group to services with DynamicUser + systemd.services = mkMerge [ + (mkIf cfg.sonarr.enable {sonarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.radarr.enable {radarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.lidarr.enable {lidarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.readarr.enable {readarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.prowlarr.enable {prowlarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.recyclarr.enable {recyclarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + + # Create API key extractors for enabled services + (mkIf cfg.sonarr.enable {"sonarr-api-key" = mkApiKeyExtractor "sonarr" cfg.sonarr;}) + (mkIf cfg.radarr.enable {"radarr-api-key" = mkApiKeyExtractor "radarr" cfg.radarr;}) + (mkIf cfg.lidarr.enable {"lidarr-api-key" = mkApiKeyExtractor "lidarr" cfg.lidarr;}) + (mkIf cfg.readarr.enable {"readarr-api-key" = mkApiKeyExtractor "readarr" cfg.readarr;}) + (mkIf cfg.prowlarr.enable {"prowlarr-api-key" = mkApiKeyExtractor "prowlarr" cfg.prowlarr;}) + ]; + + # Create the api-keys directory + systemd.tmpfiles.rules = [ + "d ${cfg.stateDir}/api-keys 0750 root api-keys - -" + ]; + }; +} From 122741de1f02f367eca51a8fb67b7df541ae31dd Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Tue, 25 Mar 2025 07:22:49 +0100 Subject: [PATCH 02/13] chore: use one api key group per service --- nixarr/lib/api-keys.nix | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix index 62b1d7d..4b6c9a9 100644 --- a/nixarr/lib/api-keys.nix +++ b/nixarr/lib/api-keys.nix @@ -25,12 +25,12 @@ with lib; let else null } = serviceConfig.user or null; - Group = "api-keys"; + Group = "${serviceName}-api"; UMask = "0027"; # Results in 0640 permissions ExecStartPre = [ "${pkgs.coreutils}/bin/mkdir -p ${cfg.stateDir}/api-keys" - "${pkgs.coreutils}/bin/chown root:api-keys ${cfg.stateDir}/api-keys" + "${pkgs.coreutils}/bin/chown root:${serviceName}-api ${cfg.stateDir}/api-keys" "${pkgs.coreutils}/bin/chmod 750 ${cfg.stateDir}/api-keys" # Wait for config file to exist "${pkgs.bash}/bin/bash -c 'while [ ! -f ${serviceConfig.stateDir}/config.xml ]; do sleep 1; done'" @@ -39,29 +39,44 @@ with lib; let ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" '' ${pkgs.dasel}/bin/dasel -f "${serviceConfig.stateDir}/config.xml" \ -s ".Config.ApiKey" | tr -d '\n\r' > "${cfg.stateDir}/api-keys/${serviceName}.key" - chown $USER:api-keys "${cfg.stateDir}/api-keys/${serviceName}.key" + chown $USER:${serviceName}-api "${cfg.stateDir}/api-keys/${serviceName}.key" ''; }; }; in { config = mkIf cfg.enable { - users.groups.api-keys = {}; - - # Ensure all services that need API keys are in the group - users.users = mkMerge [ - # Static users - (mkIf cfg.transmission.enable {torrenter.extraGroups = ["api-keys"];}) - (mkIf cfg.transmission.privateTrackers.cross-seed.enable {cross-seed.extraGroups = ["api-keys"];}) + # Create per-service API key groups + users.groups = mkMerge [ + (mkIf cfg.sonarr.enable {sonarr-api = {};}) + (mkIf cfg.radarr.enable {radarr-api = {};}) + (mkIf cfg.lidarr.enable {lidarr-api = {};}) + (mkIf cfg.readarr.enable {readarr-api = {};}) + (mkIf cfg.prowlarr.enable {prowlarr-api = {};}) ]; - # Add api-keys group to services with DynamicUser + # Add services that need API keys to their respective groups + users.users = mkMerge [ + # Static users + (mkIf cfg.transmission.enable { + torrenter.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; + }) + (mkIf cfg.transmission.privateTrackers.cross-seed.enable { + cross-seed.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; + }) + ]; + + # Add api groups to services with DynamicUser systemd.services = mkMerge [ - (mkIf cfg.sonarr.enable {sonarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) - (mkIf cfg.radarr.enable {radarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) - (mkIf cfg.lidarr.enable {lidarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) - (mkIf cfg.readarr.enable {readarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) - (mkIf cfg.prowlarr.enable {prowlarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) - (mkIf cfg.recyclarr.enable {recyclarr.serviceConfig.SupplementaryGroups = ["api-keys"];}) + (mkIf cfg.sonarr.enable {sonarr.serviceConfig.SupplementaryGroups = ["sonarr-api"];}) + (mkIf cfg.radarr.enable {radarr.serviceConfig.SupplementaryGroups = ["radarr-api"];}) + (mkIf cfg.lidarr.enable {lidarr.serviceConfig.SupplementaryGroups = ["lidarr-api"];}) + (mkIf cfg.readarr.enable {readarr.serviceConfig.SupplementaryGroups = ["readarr-api"];}) + (mkIf cfg.prowlarr.enable {prowlarr.serviceConfig.SupplementaryGroups = ["prowlarr-api"];}) + (mkIf cfg.recyclarr.enable { + recyclarr.serviceConfig.SupplementaryGroups = + (optional cfg.sonarr.enable "sonarr-api") ++ + (optional cfg.radarr.enable "radarr-api"); + }) # Create API key extractors for enabled services (mkIf cfg.sonarr.enable {"sonarr-api-key" = mkApiKeyExtractor "sonarr" cfg.sonarr;}) @@ -73,7 +88,7 @@ in { # Create the api-keys directory systemd.tmpfiles.rules = [ - "d ${cfg.stateDir}/api-keys 0750 root api-keys - -" + "d ${cfg.stateDir}/api-keys 0750 root root - -" ]; }; } From efae009f94229127bf61bc546caba0e178ad517f Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Mon, 31 Mar 2025 22:19:41 +0200 Subject: [PATCH 03/13] chore: formatting --- nixarr/lib/api-keys.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix index 4b6c9a9..d0194dd 100644 --- a/nixarr/lib/api-keys.nix +++ b/nixarr/lib/api-keys.nix @@ -73,9 +73,9 @@ in { (mkIf cfg.readarr.enable {readarr.serviceConfig.SupplementaryGroups = ["readarr-api"];}) (mkIf cfg.prowlarr.enable {prowlarr.serviceConfig.SupplementaryGroups = ["prowlarr-api"];}) (mkIf cfg.recyclarr.enable { - recyclarr.serviceConfig.SupplementaryGroups = - (optional cfg.sonarr.enable "sonarr-api") ++ - (optional cfg.radarr.enable "radarr-api"); + recyclarr.serviceConfig.SupplementaryGroups = + (optional cfg.sonarr.enable "sonarr-api") + ++ (optional cfg.radarr.enable "radarr-api"); }) # Create API key extractors for enabled services From 1b7dc69561dba8cafc8d35508c3287567cbc45c7 Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Tue, 1 Jul 2025 22:59:12 +0200 Subject: [PATCH 04/13] fix(monitoring): resolve rebase conflicts and improve wireguard exporter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix API key service user reference (torrenter -> transmission) - Remove duplicate vpnNamespaces.wg definition - Add proper enable option to wireguard exporter for consistency - Make wireguard exporter port mappings conditional πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- nixarr/lib/api-keys.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix index d0194dd..1c95e04 100644 --- a/nixarr/lib/api-keys.nix +++ b/nixarr/lib/api-keys.nix @@ -58,7 +58,7 @@ in { users.users = mkMerge [ # Static users (mkIf cfg.transmission.enable { - torrenter.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; + transmission.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; }) (mkIf cfg.transmission.privateTrackers.cross-seed.enable { cross-seed.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; From c2675d09499f63ceffff5dd9c4484ac162b116c2 Mon Sep 17 00:00:00 2001 From: Edward Pierzchalski Date: Sun, 12 Oct 2025 19:56:01 +1100 Subject: [PATCH 05/13] sonarr: add `port` option This lets cross-service integrations avoid hard-coding `8989` everywhere (e.g. [here](https://github.com/cramt/nixarr/blob/b8d54fe1e6049dc60a91b73a34cfb977731bea6c/nixarr/bazarr/default.nix#L133)) --- nixarr/sonarr/default.nix | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/nixarr/sonarr/default.nix b/nixarr/sonarr/default.nix index 422937c..18a56b3 100644 --- a/nixarr/sonarr/default.nix +++ b/nixarr/sonarr/default.nix @@ -22,6 +22,12 @@ in { package = mkPackageOption pkgs "sonarr" {}; + port = mkOption { + type = types.port; + default = defaultPort; + description = "Port for Sonarr to use."; + }; + stateDir = mkOption { type = types.path; default = "${nixarr.stateDir}/sonarr"; @@ -91,6 +97,7 @@ in { package = cfg.package; user = globals.sonarr.user; group = globals.sonarr.group; + settings.server.port = cfg.port; openFirewall = cfg.openFirewall; dataDir = cfg.stateDir; }; @@ -105,8 +112,8 @@ in { vpnNamespaces.wg = mkIf cfg.vpn.enable { portMappings = [ { - from = defaultPort; - to = defaultPort; + from = cfg.port; + to = cfg.port; } ]; }; @@ -118,17 +125,17 @@ in { recommendedOptimisation = true; recommendedGzipSettings = true; - virtualHosts."127.0.0.1:${builtins.toString defaultPort}" = { + virtualHosts."127.0.0.1:${builtins.toString cfg.port}" = { listen = [ { addr = "0.0.0.0"; - port = defaultPort; + port = cfg.port; } ]; locations."/" = { recommendedProxySettings = true; proxyWebsockets = true; - proxyPass = "http://192.168.15.1:${builtins.toString defaultPort}"; + proxyPass = "http://192.168.15.1:${builtins.toString cfg.port}"; }; }; }; From 796787f6b047e6b8ec4b867ab6249d8c59e14eb2 Mon Sep 17 00:00:00 2001 From: Edward Pierzchalski Date: Sun, 19 Oct 2025 15:18:41 +1100 Subject: [PATCH 06/13] api-keys: cleanup - Remove handling of dynamic users - Split out "which file to wait for" and "how to read that file", per service - Rely on `systemd.tmpfiles` to make dirs with the right permissions - Remove per-service group membership changes; those will be easier to reason about in each service's *.nix file --- nixarr/default.nix | 1 + nixarr/lib/api-keys.nix | 127 ++++++++++++++++++++++------------------ 2 files changed, 72 insertions(+), 56 deletions(-) diff --git a/nixarr/default.nix b/nixarr/default.nix index 07e5ce3..2729286 100644 --- a/nixarr/default.nix +++ b/nixarr/default.nix @@ -15,6 +15,7 @@ in { ./ddns ./jellyfin ./jellyseerr + ./lib/api-keys.nix ./lidarr ./nixarr-command ./openssh diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix index 1c95e04..96f0fab 100644 --- a/nixarr/lib/api-keys.nix +++ b/nixarr/lib/api-keys.nix @@ -7,8 +7,61 @@ with lib; let cfg = config.nixarr; + serviceCfgFile = { + bazarr = "${cfg.bazarr.stateDir}/config/config.yaml"; + jellyseerr = "${cfg.jellyseerr.stateDir}/settings.json"; + lidarr = "${cfg.lidarr.stateDir}/config.xml"; + prowlarr = "${cfg.prowlarr.stateDir}/config.xml"; + radarr = "${cfg.radarr.stateDir}/config.xml"; + readarr-audiobook = "${cfg.readarr-audiobook.stateDir}/config.xml"; + readarr = "${cfg.readarr.stateDir}/config.xml"; + sabnzbd = "${cfg.sabnzbd.stateDir}/sabnzbd.ini"; + sonarr = "${cfg.sonarr.stateDir}/config.xml"; + transmission = "${cfg.transmission.stateDir}/.config/transmission-daemon/settings.json"; + }; + + printServiceApiKey = let + yq = getExe' pkgs.yq "yq"; + xq = getExe' pkgs.yq "xq"; + grep = getExe pkgs.gnugrep; + sed = getExe pkgs.gnused; + in { + bazarr = pkgs.writeShellScript "print-bazarr-api-key" '' + ${yq} -r .auth.apiKey '${serviceCfgFile.bazarr}' + ''; + jellyseerr = pkgs.writeShellScript "print-jellyseerr-api-key" '' + ${yq} -r .main.apiKey '${serviceCfgFile.jellyseerr}' + ''; + lidarr = pkgs.writeShellScript "print-lidarr-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.lidarr}' + ''; + prowlarr = pkgs.writeShellScript "print-prowlarr-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.prowlarr}' + ''; + radarr = pkgs.writeShellScript "print-radarr-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.radarr}' + ''; + readarr-audiobook = pkgs.writeShellScript "print-readarr-audiobook-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.readarr-audiobook}' + ''; + readarr = pkgs.writeShellScript "print-readarr-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.readarr}' + ''; + sabnzbd = pkgs.writeShellScript "print-sabnzbd-api-key" '' + ${grep} api_key '${serviceCfgFile.sabnzbd}' | ${sed} 's/^api_key.*= *//g' + ''; + sonarr = pkgs.writeShellScript "print-sonarr-api-key" '' + ${xq} -r .Config.ApiKey '${serviceCfgFile.sonarr}' + ''; + transmission = pkgs.writeShellScript "print-transmission-api-key" '' + ${yq} -r .["rpc-password"] '${serviceCfgFile.transmission}' + ''; + }; + + servicesWithApiKeys = builtins.attrNames printServiceApiKey; + # Helper to create API key extraction for a service - mkApiKeyExtractor = serviceName: serviceConfig: { + mkApiKeyExtractor = serviceName: { description = "Extract ${serviceName} API key"; after = ["${serviceName}.service"]; requires = ["${serviceName}.service"]; @@ -16,79 +69,41 @@ with lib; let serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - # Use DynamicUser if the parent service does - DynamicUser = serviceConfig.serviceConfig.DynamicUser or false; - # Only set User if not using DynamicUser - ${ - if !(serviceConfig.serviceConfig.DynamicUser or false) - then "User" - else null - } = - serviceConfig.user or null; Group = "${serviceName}-api"; UMask = "0027"; # Results in 0640 permissions ExecStartPre = [ - "${pkgs.coreutils}/bin/mkdir -p ${cfg.stateDir}/api-keys" - "${pkgs.coreutils}/bin/chown root:${serviceName}-api ${cfg.stateDir}/api-keys" - "${pkgs.coreutils}/bin/chmod 750 ${cfg.stateDir}/api-keys" - # Wait for config file to exist - "${pkgs.bash}/bin/bash -c 'while [ ! -f ${serviceConfig.stateDir}/config.xml ]; do sleep 1; done'" + (pkgs.writeShellScript "wait-for-${serviceName}-config" '' + while [ ! -f '${serviceCfgFile.${serviceName}}' ]; do sleep 1; done + '') ]; ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" '' - ${pkgs.dasel}/bin/dasel -f "${serviceConfig.stateDir}/config.xml" \ - -s ".Config.ApiKey" | tr -d '\n\r' > "${cfg.stateDir}/api-keys/${serviceName}.key" - chown $USER:${serviceName}-api "${cfg.stateDir}/api-keys/${serviceName}.key" + ${printServiceApiKey.${serviceName}} > '${cfg.stateDir}/api-keys/${serviceName}.key' ''; }; }; in { config = mkIf cfg.enable { # Create per-service API key groups - users.groups = mkMerge [ - (mkIf cfg.sonarr.enable {sonarr-api = {};}) - (mkIf cfg.radarr.enable {radarr-api = {};}) - (mkIf cfg.lidarr.enable {lidarr-api = {};}) - (mkIf cfg.readarr.enable {readarr-api = {};}) - (mkIf cfg.prowlarr.enable {prowlarr-api = {};}) - ]; - - # Add services that need API keys to their respective groups - users.users = mkMerge [ - # Static users - (mkIf cfg.transmission.enable { - transmission.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; - }) - (mkIf cfg.transmission.privateTrackers.cross-seed.enable { - cross-seed.extraGroups = optional cfg.prowlarr.enable "prowlarr-api"; - }) - ]; - - # Add api groups to services with DynamicUser - systemd.services = mkMerge [ - (mkIf cfg.sonarr.enable {sonarr.serviceConfig.SupplementaryGroups = ["sonarr-api"];}) - (mkIf cfg.radarr.enable {radarr.serviceConfig.SupplementaryGroups = ["radarr-api"];}) - (mkIf cfg.lidarr.enable {lidarr.serviceConfig.SupplementaryGroups = ["lidarr-api"];}) - (mkIf cfg.readarr.enable {readarr.serviceConfig.SupplementaryGroups = ["readarr-api"];}) - (mkIf cfg.prowlarr.enable {prowlarr.serviceConfig.SupplementaryGroups = ["prowlarr-api"];}) - (mkIf cfg.recyclarr.enable { - recyclarr.serviceConfig.SupplementaryGroups = - (optional cfg.sonarr.enable "sonarr-api") - ++ (optional cfg.radarr.enable "radarr-api"); - }) + users.groups = mkMerge ( + builtins.map + (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api" = {};}) + servicesWithApiKeys + ); + systemd.services = mkMerge ( # Create API key extractors for enabled services - (mkIf cfg.sonarr.enable {"sonarr-api-key" = mkApiKeyExtractor "sonarr" cfg.sonarr;}) - (mkIf cfg.radarr.enable {"radarr-api-key" = mkApiKeyExtractor "radarr" cfg.radarr;}) - (mkIf cfg.lidarr.enable {"lidarr-api-key" = mkApiKeyExtractor "lidarr" cfg.lidarr;}) - (mkIf cfg.readarr.enable {"readarr-api-key" = mkApiKeyExtractor "readarr" cfg.readarr;}) - (mkIf cfg.prowlarr.enable {"prowlarr-api-key" = mkApiKeyExtractor "prowlarr" cfg.prowlarr;}) - ]; + builtins.map + (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api-key" = mkApiKeyExtractor serviceName;}) + servicesWithApiKeys + ); # Create the api-keys directory systemd.tmpfiles.rules = [ - "d ${cfg.stateDir}/api-keys 0750 root root - -" + # Needs to be world-executable for members of the `*-api` groups to access + # the files inside. + "d ${cfg.stateDir}/api-keys 0701 root root - -" ]; }; } From ff4980014d8d05130dca4058905878e642d3570a Mon Sep 17 00:00:00 2001 From: Edward Pierzchalski Date: Sun, 19 Oct 2025 20:46:44 +1100 Subject: [PATCH 07/13] recyclarr: use new api-keys services --- nixarr/recyclarr/default.nix | 61 +++++++++++++----------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/nixarr/recyclarr/default.nix b/nixarr/recyclarr/default.nix index 947d63c..d65da17 100644 --- a/nixarr/recyclarr/default.nix +++ b/nixarr/recyclarr/default.nix @@ -2,7 +2,6 @@ config, lib, pkgs, - inputs, ... }: with lib; let @@ -11,39 +10,6 @@ with lib; let nixarr = config.nixarr; format = pkgs.formats.yaml {}; - # Helper function to extract API keys - extractApiKeys = pkgs.writeShellApplication { - name = "extract-recyclarr-api-keys"; - runtimeInputs = with pkgs; [yq]; - text = '' - # Ensure state directory exists with proper permissions - mkdir -p "${cfg.stateDir}" - chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "${cfg.stateDir}" - chmod 755 "${cfg.stateDir}" - - ${optionalString nixarr.radarr.enable '' - # Extract Radarr API key - API_KEY_FILE="${cfg.stateDir}/radarr-api-key" - xq -r '.Config.ApiKey' "${nixarr.radarr.stateDir}/config.xml" > "$API_KEY_FILE" - chmod 400 "$API_KEY_FILE" - chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "$API_KEY_FILE" - echo "RADARR_API_KEY=$(tr -d '\n' < "$API_KEY_FILE")" >> "${cfg.stateDir}/env" - ''} - - ${optionalString nixarr.sonarr.enable '' - # Extract Sonarr API key - API_KEY_FILE="${cfg.stateDir}/sonarr-api-key" - xq -r '.Config.ApiKey' "${nixarr.sonarr.stateDir}/config.xml" > "$API_KEY_FILE" - chmod 400 "$API_KEY_FILE" - chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "$API_KEY_FILE" - echo "SONARR_API_KEY=$(tr -d '\n' < "$API_KEY_FILE")" >> "${cfg.stateDir}/env" - ''} - - chmod 400 "${cfg.stateDir}/env" - chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "${cfg.stateDir}/env" - ''; - }; - # Generate configuration file from Nix attribute set if provided generatedConfigFile = format.generate "recyclarr-config.yml" cfg.configuration; @@ -193,6 +159,9 @@ in { isSystemUser = true; group = globals.recyclarr.group; uid = globals.uids.${globals.recyclarr.user}; + extraGroups = + (optional nixarr.radarr.enable "radarr-api") + ++ (optional nixarr.sonarr.enable "sonarr-api"); }; }; @@ -207,16 +176,29 @@ in { requiredBy = ["recyclarr.service"]; before = ["recyclarr.service"]; requires = - (optionals nixarr.radarr.enable ["radarr.service"]) - ++ (optionals nixarr.sonarr.enable ["sonarr.service"]); + (optionals nixarr.radarr.enable ["radarr.service" "radarr-api-key.service"]) + ++ (optionals nixarr.sonarr.enable ["sonarr.service" "sonarr-api-key.service"]); after = - (optionals nixarr.radarr.enable ["radarr.service"]) - ++ (optionals nixarr.sonarr.enable ["sonarr.service"]); + (optionals nixarr.radarr.enable ["radarr.service" "radarr-api-key.service"]) + ++ (optionals nixarr.sonarr.enable ["sonarr.service" "sonarr-api-key.service"]); serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - ExecStart = "${extractApiKeys}/bin/extract-recyclarr-api-keys"; + UMask = "0077"; # Results in 0600 permissions + User = config.services.recyclarr.user; + ExecStart = pkgs.writeShellScript "recyclar-setup" '' + set -euo pipefail + echo -n > '${cfg.stateDir}/env' + ${optionalString nixarr.radarr.enable '' + printf RADARR_API_KEY= >> '${cfg.stateDir}/env' + cat '${nixarr.stateDir}/api-keys/radarr.key' >> '${cfg.stateDir}/env' + ''} + ${optionalString nixarr.sonarr.enable '' + printf SONARR_API_KEY= >> '${cfg.stateDir}/env' + cat '${nixarr.stateDir}/api-keys/sonarr.key' >> '${cfg.stateDir}/env' + ''} + ''; }; }; @@ -232,6 +214,7 @@ in { systemd.tmpfiles.rules = [ "d '${cfg.stateDir}' 0750 ${config.services.recyclarr.user} root - -" + "f '${cfg.stateDir}/env' 0600 ${config.services.recyclarr.user} ${config.services.recyclarr.group} - -" ]; }; } From 9a67194bdf06543ad16cd8a56dd4cd059952faed Mon Sep 17 00:00:00 2001 From: Edward Pierzchalski Date: Mon, 20 Oct 2025 16:37:12 +1100 Subject: [PATCH 08/13] prowlarr: use the NixOS module as much as possible This brings `prowlarr` in line with how `sonarr` and `radarr` are set up, which (among other things) lets users use `services.prowlarr.settings` to e.g. configure Postgres or URL base. --- nixarr/prowlarr/default.nix | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/nixarr/prowlarr/default.nix b/nixarr/prowlarr/default.nix index a9f4fa6..e76b828 100644 --- a/nixarr/prowlarr/default.nix +++ b/nixarr/prowlarr/default.nix @@ -84,19 +84,20 @@ in { "d '${cfg.stateDir}' 0700 ${globals.prowlarr.user} root - -" ]; - systemd.services.prowlarr = { - description = "prowlarr"; - after = ["network.target"]; - wantedBy = ["multi-user.target"]; - environment.PROWLARR__SERVER__PORT = builtins.toString cfg.port; + services.prowlarr = { + enable = cfg.enable; + package = cfg.package; + settings.server.port = cfg.port; + openFirewall = cfg.openFirewall; + }; - serviceConfig = { - Type = "simple"; - User = globals.prowlarr.user; - Group = globals.prowlarr.group; - ExecStart = "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}"; - Restart = "on-failure"; - }; + systemd.services.prowlarr.serviceConfig = { + # `User` and `Group` override `DynamicUser = true` from the NixOS Prowlarr + # module (because a user and group with those names exists). + User = globals.prowlarr.user; + Group = globals.prowlarr.group; + ExecStart = mkForce "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}"; + ReadWritePaths = [cfg.stateDir]; }; networking.firewall = mkIf cfg.openFirewall { From e74e0d83eea1ca5010b6a6a4895fdd5dd5149ef9 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 23 Oct 2025 14:31:53 +0200 Subject: [PATCH 09/13] fmt --- flake.nix | 5 ++- tests/vpn-confinement-test.nix | 60 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/flake.nix b/flake.nix index 1fa1ac3..cdbbdb4 100644 --- a/flake.nix +++ b/flake.nix @@ -29,7 +29,10 @@ forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f { - pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; }); in { nixosModules.default.imports = [./nixarr vpnconfinement.nixosModules.default]; diff --git a/tests/vpn-confinement-test.nix b/tests/vpn-confinement-test.nix index ce9c96d..3a3b7f6 100644 --- a/tests/vpn-confinement-test.nix +++ b/tests/vpn-confinement-test.nix @@ -1,38 +1,38 @@ /* - VPN Confinement Integration Test +VPN Confinement Integration Test - This test validates that Nixarr services are properly confined to a VPN namespace - and cannot leak traffic when the VPN connection fails. It uses a 3-VM topology - to simulate real-world network conditions. +This test validates that Nixarr services are properly confined to a VPN namespace +and cannot leak traffic when the VPN connection fails. It uses a 3-VM topology +to simulate real-world network conditions. - Network Topology: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” VLAN 2 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” VLAN 1 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚internetClientβ”‚ ◄──────────── β”‚ gateway β”‚ ◄──────────── β”‚ nixarrHost β”‚ - β”‚ 10.0.1.2 β”‚ β”‚ 10.0.1.1 β”‚ β”‚192.168.1.2 β”‚ - β”‚ fd00:2::2 β”‚ β”‚192.168.1.1 β”‚ β”‚ fd00:1::2 β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ fd00:2::1 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ fd00:1::1 β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - WireGuard tunnel β”‚ - 10.100.0.1 β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - fd00:100::1 VPN namespace - (10.100.0.2, fd00:100::2) +Network Topology: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” VLAN 2 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” VLAN 1 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚internetClientβ”‚ ◄──────────── β”‚ gateway β”‚ ◄──────────── β”‚ nixarrHost β”‚ +β”‚ 10.0.1.2 β”‚ β”‚ 10.0.1.1 β”‚ β”‚192.168.1.2 β”‚ +β”‚ fd00:2::2 β”‚ β”‚192.168.1.1 β”‚ β”‚ fd00:1::2 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ fd00:2::1 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ fd00:1::1 β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ + WireGuard tunnel β”‚ + 10.100.0.1 β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + fd00:100::1 VPN namespace + (10.100.0.2, fd00:100::2) - Test Coverage: - - VPN namespace isolation (transmission confined to wg namespace) - - IPv4 and IPv6 traffic routing through VPN tunnel - - Traffic leak prevention when VPN is down - - Port forwarding from external clients through gateway to VPN services - - DNS configuration in VPN namespace - - Service recovery after VPN reconnection +Test Coverage: +- VPN namespace isolation (transmission confined to wg namespace) +- IPv4 and IPv6 traffic routing through VPN tunnel +- Traffic leak prevention when VPN is down +- Port forwarding from external clients through gateway to VPN services +- DNS configuration in VPN namespace +- Service recovery after VPN reconnection - The test ensures that: - 1. All transmission traffic goes through the VPN tunnel - 2. Source IP is preserved (shows VPN client IP: 10.100.0.2/fd00:100::2) - 3. No traffic leaks to host network when VPN fails - 4. External port forwarding works correctly - 5. Both IPv4 and IPv6 work identically through the tunnel +The test ensures that: +1. All transmission traffic goes through the VPN tunnel +2. Source IP is preserved (shows VPN client IP: 10.100.0.2/fd00:100::2) +3. No traffic leaks to host network when VPN fails +4. External port forwarding works correctly +5. Both IPv4 and IPv6 work identically through the tunnel */ { pkgs, From e8312dfa783f3d5a67ea6e83782f53efaf1a5716 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 23 Oct 2025 15:13:10 +0200 Subject: [PATCH 10/13] test --- flake.lock | 20 ++++---- flake.nix | 2 +- nixarr/bazarr/default.nix | 1 - tests/simple-test.nix | 98 +++++++++++++++++++-------------------- 4 files changed, 58 insertions(+), 63 deletions(-) diff --git a/flake.lock b/flake.lock index 742fa94..67c36fb 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1750183894, - "narHash": "sha256-ZtOgEt70keBVB4YJc+z7m0h7J1BOlv/GjHE1YC6KxeA=", + "lastModified": 1761016216, + "narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f45e75fc63fc8a7ffc3da382b2f6b681c5b71875", + "rev": "481cf557888e05d3128a76f14c76397b7d7cc869", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixpkgs-unstable", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } @@ -25,11 +25,11 @@ }, "vpnconfinement": { "locked": { - "lastModified": 1749672087, - "narHash": "sha256-j8LG0s0QcvNkZZLcItl78lvTZemvsScir0dG3Ii4B1c=", + "lastModified": 1759956062, + "narHash": "sha256-NUZu0Rb0fwUjfdp51zMm0xM3lcK8Kw4c97LLog7+JjA=", "owner": "Maroka-chan", "repo": "VPN-Confinement", - "rev": "880b3bd2c864dce4f6afc79f6580ca699294c011", + "rev": "fabe7247b720b5eb4c3c053e24a2b3b70e64c52b", "type": "github" }, "original": { @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1750317638, - "narHash": "sha256-B4RWcXXOLO6gMeYyV+K4olu+kGGsYamKH+JAm0cIXqI=", + "lastModified": 1753958235, + "narHash": "sha256-Rd27XQJKv8Z4BCr3gdbaHFd0TmumiGxdjGRzsEf/mOg=", "owner": "rasmus-kirk", "repo": "website-builder", - "rev": "b54192000a00e865947f45bacf3184d56363ee38", + "rev": "00a14b7ae7baef2197978ba7c3fe72dfca7bc475", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cdbbdb4..bbb370c 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "The Nixarr Media Server Nixos Module"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; vpnconfinement.url = "github:Maroka-chan/VPN-Confinement"; diff --git a/nixarr/bazarr/default.nix b/nixarr/bazarr/default.nix index 4266060..8697aa2 100644 --- a/nixarr/bazarr/default.nix +++ b/nixarr/bazarr/default.nix @@ -98,7 +98,6 @@ in { --port ${toString cfg.port} \ --no-update True ''; - KillSignal = "SIGINT"; Restart = "on-failure"; KillSignal = "SIGINT"; SuccessExitStatus = "0 156"; diff --git a/tests/simple-test.nix b/tests/simple-test.nix index a402456..5fad4b0 100644 --- a/tests/simple-test.nix +++ b/tests/simple-test.nix @@ -25,7 +25,7 @@ pkgs.nixosTest { transmission = { enable = true; - privateTrackers.cross-seed.enable = true; + # privateTrackers.cross-seed.enable = true; }; autobrr.enable = true; @@ -37,51 +37,51 @@ pkgs.nixosTest { sabnzbd.enable = true; lidarr.enable = true; prowlarr.enable = true; - recyclarr = { - enable = true; - configuration = { - sonarr.series = { - base_url = "http://localhost:8989"; - api_key = "!env_var SONARR_API_KEY"; - quality_definition.type = "series"; - delete_old_custom_formats = true; - custom_formats = [ - { - trash_ids = [ - "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK - "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ - ]; - assign_scores_to = [ - { - name = "WEB-DL (1080p)"; - score = -10000; - } - ]; - } - ]; - }; - radarr.movies = { - base_url = "http://localhost:7878"; - api_key = "!env_var RADARR_API_KEY"; - quality_definition.type = "movie"; - delete_old_custom_formats = true; - custom_formats = [ - { - trash_ids = [ - "570bc9ebecd92723d2d21500f4be314c" # Remaster - "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster - ]; - assign_scores_to = [ - { - name = "HD Bluray + WEB"; - score = 25; - } - ]; - } - ]; - }; - }; - }; + # recyclarr = { + # enable = true; + # configuration = { + # sonarr.series = { + # base_url = "http://localhost:8989"; + # api_key = "!env_var SONARR_API_KEY"; + # quality_definition.type = "series"; + # delete_old_custom_formats = true; + # custom_formats = [ + # { + # trash_ids = [ + # "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK + # "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ + # ]; + # assign_scores_to = [ + # { + # name = "WEB-DL (1080p)"; + # score = -10000; + # } + # ]; + # } + # ]; + # }; + # radarr.movies = { + # base_url = "http://localhost:7878"; + # api_key = "!env_var RADARR_API_KEY"; + # quality_definition.type = "movie"; + # delete_old_custom_formats = true; + # custom_formats = [ + # { + # trash_ids = [ + # "570bc9ebecd92723d2d21500f4be314c" # Remaster + # "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster + # ]; + # assign_scores_to = [ + # { + # name = "HD Bluray + WEB"; + # score = 25; + # } + # ]; + # } + # ]; + # }; + # }; + # }; }; # Create a test user to verify mediaUsers functionality @@ -109,11 +109,7 @@ pkgs.nixosTest { machine.succeed("systemctl is-active sabnzbd") machine.succeed("systemctl is-active lidarr") machine.succeed("systemctl is-active prowlarr") - machine.succeed("systemctl is-active recyclarr") - - machine.succeed("nixarr list-api-keys") - machine.succeed("nixarr fix-permissions") - machine.succeed("nixarr wipe-uids-gids") + # machine.succeed("systemctl is-active recyclarr") print("\n=== Nixarr Simple Test Completed ===") ''; From 9f1b809818b1c072aa392aa8f05dd4a855a9606f Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Wed, 29 Oct 2025 09:02:42 +0100 Subject: [PATCH 11/13] Fixed cross-seed --- nixarr/transmission/cross-seed/default.nix | 20 ++++++++++++++++---- nixarr/transmission/default.nix | 11 +++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/nixarr/transmission/cross-seed/default.nix b/nixarr/transmission/cross-seed/default.nix index 7a26309..7ad604a 100644 --- a/nixarr/transmission/cross-seed/default.nix +++ b/nixarr/transmission/cross-seed/default.nix @@ -54,14 +54,14 @@ in { }; user = mkOption { - type = types.str; - default = "cross-seed"; + type = types.nullOr types.str; + default = null; description = "User account under which cross-seed runs."; }; group = mkOption { - type = types.str; - default = "cross-seed"; + type = types.nullOr types.str; + default = null; description = "Group under which cross-seed runs."; }; }; @@ -81,6 +81,18 @@ in { The settings.torrentDir option must be set if cross-seed is enabled. ''; } + { + assertion = cfg.enable -> cfg.user != null; + message = '' + The user option must be set if cross-seed is enabled. + ''; + } + { + assertion = cfg.enable -> cfg.group != null; + message = '' + The group option must be set if cross-seed is enabled. + ''; + } ]; systemd.tmpfiles.rules = diff --git a/nixarr/transmission/default.nix b/nixarr/transmission/default.nix index 3777f11..cc1dea4 100644 --- a/nixarr/transmission/default.nix +++ b/nixarr/transmission/default.nix @@ -287,7 +287,6 @@ in { users = { groups.${globals.transmission.group}.gid = globals.gids.${globals.transmission.group}; - groups.${globals.cross-seed.group}.gid = globals.gids.${globals.cross-seed.group}; users.${globals.transmission.user} = { isSystemUser = true; group = globals.transmission.group; @@ -296,10 +295,10 @@ in { }; systemd.tmpfiles.rules = [ - "d '${cfg.stateDir}' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -" + "d '${cfg.stateDir}' 0750 ${globals.transmission.user} ${globals.transmission.group} - -" # This is fixes a bug in nixpks (https://github.com/NixOS/nixpkgs/issues/291883) - "d '${cfg.stateDir}/.config' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -" - "d '${cfg.stateDir}/.config/transmission-daemon' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -" + "d '${cfg.stateDir}/.config' 0750 ${globals.transmission.user} ${globals.transmission.group} - -" + "d '${cfg.stateDir}/.config/transmission-daemon' 0750 ${globals.transmission.user} ${globals.transmission.group} - -" # Media Dirs "d '${nixarr.mediaDir}/torrents' 0755 ${globals.transmission.user} ${globals.transmission.group} - -" @@ -315,8 +314,8 @@ in { util-nixarr.services.cross-seed = mkIf cfg-cross-seed.enable { enable = true; dataDir = cfg-cross-seed.stateDir; - user = globals.cross-seed.user; - group = globals.cross-seed.group; + user = globals.transmission.user; + group = globals.transmission.group; settings = { torrentDir = "${cfg.stateDir}/.config/transmission-daemon/torrents"; From 9ae6966ded9a83623c588739d33d9ea93cadef95 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Wed, 29 Oct 2025 09:15:54 +0100 Subject: [PATCH 12/13] Fixed upnp --- tests/simple-test.nix | 16 ++++++++++++++-- util/globals/default.nix | 6 ++++-- util/upnp/default.nix | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/simple-test.nix b/tests/simple-test.nix index 5fad4b0..b701807 100644 --- a/tests/simple-test.nix +++ b/tests/simple-test.nix @@ -18,10 +18,22 @@ pkgs.nixosTest { nixarr = { enable = true; - jellyfin.enable = true; + jellyfin = { + enable = true; + expose.https = { + enable = true; + upnp.enable = true; + }; + }; + plex = { + enable = true; + expose.https = { + enable = true; + upnp.enable = true; + }; + }; jellyseerr.enable = true; audiobookshelf.enable = true; - plex.enable = true; transmission = { enable = true; diff --git a/util/globals/default.nix b/util/globals/default.nix index 628e27b..f5a5e3b 100644 --- a/util/globals/default.nix +++ b/util/globals/default.nix @@ -35,12 +35,14 @@ in { recyclarr = 269; sabnzbd = 38; transmission = 70; - cross-seed = 183; + # Removed 2025-10-29 + # cross-seed = 183; whisparr = 272; }; gids = { autobrr = 188; - cross-seed = 183; + # Removed 2025-10-29 + # cross-seed = 183; jellyseerr = 250; media = 169; prowlarr = 287; diff --git a/util/upnp/default.nix b/util/upnp/default.nix index d7992ab..3f56840 100644 --- a/util/upnp/default.nix +++ b/util/upnp/default.nix @@ -77,7 +77,7 @@ in { ); }; in - mkIf cfg.upnp.enable { + mkIf cfg.enable { enable = true; description = "Sets port on router"; script = "${upnp-ports}/bin/upnp-ports"; @@ -89,7 +89,7 @@ in { }; timers = { - upnpc = mkIf cfg.upnp.enable { + upnpc = mkIf cfg.enable { description = "Sets port on router"; wantedBy = ["timers.target"]; From 02ed738f53b80fe943af9da2a904af128ca2d79e Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Wed, 29 Oct 2025 09:24:23 +0100 Subject: [PATCH 13/13] Fixed test --- tests/simple-test.nix | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/simple-test.nix b/tests/simple-test.nix index b701807..54e7e7f 100644 --- a/tests/simple-test.nix +++ b/tests/simple-test.nix @@ -18,26 +18,14 @@ pkgs.nixosTest { nixarr = { enable = true; - jellyfin = { - enable = true; - expose.https = { - enable = true; - upnp.enable = true; - }; - }; - plex = { - enable = true; - expose.https = { - enable = true; - upnp.enable = true; - }; - }; + jellyfin.enable = true; + plex.enable = true; jellyseerr.enable = true; audiobookshelf.enable = true; transmission = { enable = true; - # privateTrackers.cross-seed.enable = true; + privateTrackers.cross-seed.enable = true; }; autobrr.enable = true;