diff --git a/nixarr/default.nix b/nixarr/default.nix index 1af3aab..c2cab36 100644 --- a/nixarr/default.nix +++ b/nixarr/default.nix @@ -15,6 +15,7 @@ in { ./ddns ./jellyfin ./jellyseerr + ./lib/api-keys.nix ./komga ./lidarr ./nixarr-command diff --git a/nixarr/lib/api-keys.nix b/nixarr/lib/api-keys.nix new file mode 100644 index 0000000..96f0fab --- /dev/null +++ b/nixarr/lib/api-keys.nix @@ -0,0 +1,109 @@ +{ + config, + lib, + pkgs, + ... +}: +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: { + description = "Extract ${serviceName} API key"; + after = ["${serviceName}.service"]; + requires = ["${serviceName}.service"]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + Group = "${serviceName}-api"; + UMask = "0027"; # Results in 0640 permissions + + ExecStartPre = [ + (pkgs.writeShellScript "wait-for-${serviceName}-config" '' + while [ ! -f '${serviceCfgFile.${serviceName}}' ]; do sleep 1; done + '') + ]; + + ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" '' + ${printServiceApiKey.${serviceName}} > '${cfg.stateDir}/api-keys/${serviceName}.key' + ''; + }; + }; +in { + config = mkIf cfg.enable { + # Create per-service API key groups + users.groups = mkMerge ( + builtins.map + (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api" = {};}) + servicesWithApiKeys + ); + + systemd.services = mkMerge ( + # Create API key extractors for enabled services + builtins.map + (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api-key" = mkApiKeyExtractor serviceName;}) + servicesWithApiKeys + ); + + # Create the api-keys directory + systemd.tmpfiles.rules = [ + # Needs to be world-executable for members of the `*-api` groups to access + # the files inside. + "d ${cfg.stateDir}/api-keys 0701 root root - -" + ]; + }; +} 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} - -" ]; }; }