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
This commit is contained in:
Edward Pierzchalski
2025-10-19 15:18:41 +11:00
parent 1b7dc69561
commit 796787f6b0
2 changed files with 72 additions and 56 deletions
+1
View File
@@ -15,6 +15,7 @@ in {
./ddns ./ddns
./jellyfin ./jellyfin
./jellyseerr ./jellyseerr
./lib/api-keys.nix
./lidarr ./lidarr
./nixarr-command ./nixarr-command
./openssh ./openssh
+71 -56
View File
@@ -7,8 +7,61 @@
with lib; let with lib; let
cfg = config.nixarr; 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 # Helper to create API key extraction for a service
mkApiKeyExtractor = serviceName: serviceConfig: { mkApiKeyExtractor = serviceName: {
description = "Extract ${serviceName} API key"; description = "Extract ${serviceName} API key";
after = ["${serviceName}.service"]; after = ["${serviceName}.service"];
requires = ["${serviceName}.service"]; requires = ["${serviceName}.service"];
@@ -16,79 +69,41 @@ with lib; let
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; 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"; Group = "${serviceName}-api";
UMask = "0027"; # Results in 0640 permissions UMask = "0027"; # Results in 0640 permissions
ExecStartPre = [ ExecStartPre = [
"${pkgs.coreutils}/bin/mkdir -p ${cfg.stateDir}/api-keys" (pkgs.writeShellScript "wait-for-${serviceName}-config" ''
"${pkgs.coreutils}/bin/chown root:${serviceName}-api ${cfg.stateDir}/api-keys" while [ ! -f '${serviceCfgFile.${serviceName}}' ]; do sleep 1; done
"${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" '' ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" ''
${pkgs.dasel}/bin/dasel -f "${serviceConfig.stateDir}/config.xml" \ ${printServiceApiKey.${serviceName}} > '${cfg.stateDir}/api-keys/${serviceName}.key'
-s ".Config.ApiKey" | tr -d '\n\r' > "${cfg.stateDir}/api-keys/${serviceName}.key"
chown $USER:${serviceName}-api "${cfg.stateDir}/api-keys/${serviceName}.key"
''; '';
}; };
}; };
in { in {
config = mkIf cfg.enable { config = mkIf cfg.enable {
# Create per-service API key groups # Create per-service API key groups
users.groups = mkMerge [ users.groups = mkMerge (
(mkIf cfg.sonarr.enable {sonarr-api = {};}) builtins.map
(mkIf cfg.radarr.enable {radarr-api = {};}) (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api" = {};})
(mkIf cfg.lidarr.enable {lidarr-api = {};}) servicesWithApiKeys
(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");
})
systemd.services = mkMerge (
# Create API key extractors for enabled services # Create API key extractors for enabled services
(mkIf cfg.sonarr.enable {"sonarr-api-key" = mkApiKeyExtractor "sonarr" cfg.sonarr;}) builtins.map
(mkIf cfg.radarr.enable {"radarr-api-key" = mkApiKeyExtractor "radarr" cfg.radarr;}) (serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api-key" = mkApiKeyExtractor serviceName;})
(mkIf cfg.lidarr.enable {"lidarr-api-key" = mkApiKeyExtractor "lidarr" cfg.lidarr;}) servicesWithApiKeys
(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 # Create the api-keys directory
systemd.tmpfiles.rules = [ 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 - -"
]; ];
}; };
} }