From 98bccf64d1bf097393be7072c8a510942e8daedf Mon Sep 17 00:00:00 2001 From: Darren Rambaud <225436867+debtquity@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:43:23 -0500 Subject: [PATCH] nixos/kavita: improve credential management * allow specifying arbitrary number of secrets to replace in `appsettings.json` * update option documentation * move `services.kavita.tokenKeyFile` to `services.kavita.settings.TokenKey` to sync with updated credential management * fix nixosTests.kavita Resolves: #558959 --- nixos/modules/services/web-apps/kavita.nix | 77 +++++++++++++++++----- nixos/tests/kavita.nix | 4 +- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/nixos/modules/services/web-apps/kavita.nix b/nixos/modules/services/web-apps/kavita.nix index 21f98f47ae21..409a02e8b79b 100644 --- a/nixos/modules/services/web-apps/kavita.nix +++ b/nixos/modules/services/web-apps/kavita.nix @@ -8,9 +8,7 @@ let cfg = config.services.kavita; settingsFormat = pkgs.formats.json { }; - appsettings = settingsFormat.generate "appsettings.json" ( - { TokenKey = "@TOKEN@"; } // cfg.settings - ); + appsettings = settingsFormat.generate "appsettings.json" cfg.settings; in { imports = [ @@ -26,6 +24,10 @@ in ) ) (lib.mkRenamedOptionModule [ "services" "kavita" "port" ] [ "services" "kavita" "settings" "Port" ]) + (lib.mkRenamedOptionModule + [ "services" "kavita" "tokenKeyFile" ] + [ "services" "kavita" "settings" "TokenKey" ] + ) ]; options.services.kavita = { @@ -45,18 +47,20 @@ in description = "The directory where Kavita stores its state."; }; - tokenKeyFile = lib.mkOption { - type = lib.types.path; - description = '' - A file containing the TokenKey, a secret with at 512+ bits. - It can be generated with `head -c 64 /dev/urandom | base64 --wrap=0`. - ''; - }; - settings = lib.mkOption { default = { }; description = '' Kavita configuration options, as configured in {file}`appsettings.json`. + + Any configuration option that contains sensitive data should use a placeholder + value (ie, `oidc_client_secret`) enclosed with an `@` character: + + `services.kavita.settings.OpenIdConnectSettings.Secret = "@oidc_client_secret@";` + + Then, use the placeholder as unique key and hydrate the secret using your + preferred secrets management (ie, `sops-nix`): + + `services.kavita.credentials."oidc_client_secret" = config.sops.secrets."client_secret".path;` ''; type = lib.types.submodule { freeformType = settingsFormat.type; @@ -75,8 +79,46 @@ in IP Addresses to bind to. The default is to bind to all IPv4 and IPv6 addresses. ''; }; + + TokenKey = lib.mkOption { + default = "@token_key@"; + type = lib.types.str; + description = '' + A secret with at least 512 bits. It may be generated with `head -c 64 /dev/urandom | base64 --wrap=0`. + + Suggest using a placeholder value (eg, `token_key`) enclosed by `@` character (eg, `@tokey_key@`) to avoid writing + sensitive data to world readable nix store. Placeholder value will be defined as key in `services.kavita.credentials` + and the associated path containing the secret will be replaced at service startup. + ''; + }; }; }; + example = { + IpAddresses = "::2,127.0.0.2"; + Port = 5001; + TokenKey = "@token_key@"; + OpenIdConnectSettings = { + Enabled = true; + Authority = "https://idp.example.com"; + ClientId = "kavita"; + Secret = "@oidc_client_secret@"; + }; + }; + }; + + credentials = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + token_key = "/run/secrets/kavita/token_key"; + oidc_client_secret = "/run/secrets/kavita/client_secret"; + }; + description = '' + Secrets that will be passed to systemd service and subsequently all placeholders in {file}`appsettings.json` will be replaced + prior to service startup. + + It is not necessary to enclose the keys/placeholder values with `@` characters here. + ''; }; }; @@ -87,13 +129,16 @@ in after = [ "network.target" ]; preStart = '' install -m600 ${appsettings} ${lib.escapeShellArg cfg.dataDir}/config/appsettings.json - ${pkgs.replace-secret}/bin/replace-secret '@TOKEN@' \ - ''${CREDENTIALS_DIRECTORY}/token \ - '${cfg.dataDir}/config/appsettings.json' - ''; + '' + + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (key: val: '' + ${lib.getExe pkgs.replace-secret} '@${key}@' \ + ''${CREDENTIALS_DIRECTORY}/${key} \ + ${cfg.dataDir}/config/appsettings.json'') cfg.credentials + ); serviceConfig = { WorkingDirectory = cfg.dataDir; - LoadCredential = [ "token:${cfg.tokenKeyFile}" ]; + LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials; ExecStart = lib.getExe cfg.package; Restart = "always"; User = cfg.user; diff --git a/nixos/tests/kavita.nix b/nixos/tests/kavita.nix index bfb2f32a65eb..9f38ddacae9a 100644 --- a/nixos/tests/kavita.nix +++ b/nixos/tests/kavita.nix @@ -16,7 +16,7 @@ in kavita = { services.kavita = { enable = true; - tokenKeyFile = builtins.toFile "kavita.key" key; + settings.TokenKey = key; }; }; has_template = @@ -27,7 +27,7 @@ in { services.kavita = { enable = true; - tokenKeyFile = builtins.toFile "kavita.key" key; + settings.TokenKey = key; }; systemd.tmpfiles.rules = [ "d ${emailTemplDir} 0750 ${u} ${u} - -"