From 46c6aaeac7c80da741a61e16305237a27dda1b86 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 12 Jun 2026 18:28:28 +0200 Subject: [PATCH] nixos/netbox: improve secret management With this change secrets will be automatically generated if left at their individual option defaults. We now also allow configuration of multiple peppers, to allow proper rotation as intended by upstream. Finally, we now we prevent secrets from leaking into the store. --- nixos/modules/services/web-apps/netbox.nix | 112 +++++++++++++++++---- nixos/tests/web-apps/netbox-upgrade.nix | 6 -- nixos/tests/web-apps/netbox/default.nix | 7 -- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix index 65f761373cda..fdfe33e6f970 100644 --- a/nixos/modules/services/web-apps/netbox.nix +++ b/nixos/modules/services/web-apps/netbox.nix @@ -6,6 +6,13 @@ }: let + inherit (lib) + any + attrValues + mkChangedOptionModule + optionalString + ; + cfg = config.services.netbox; pythonFmt = pkgs.formats.pythonVars { }; staticDir = cfg.dataDir + "/static"; @@ -19,6 +26,8 @@ let settingsFile extraConfigFile ]; + secretKeyFile = + if cfg.secretKeyFile != null then cfg.secretKeyFile else "${cfg.dataDir}/secret.key"; pkg = (cfg.package.overrideAttrs (old: { @@ -51,6 +60,16 @@ let in { + imports = [ + (mkChangedOptionModule + [ "services" "netbox" "apiTokenPeppersFile" ] + [ "services" "netbox" "apiTokenPepperFiles" ] + (config: { + "1" = config.services.netbox.apiTokenPeppersFile; + }) + ) + ]; + options.services.netbox = { enable = lib.mkOption { type = lib.types.bool; @@ -159,18 +178,58 @@ in }; secretKeyFile = lib.mkOption { - type = lib.types.path; + type = + with lib.types; + nullOr (pathWith { + inStore = false; + }); + default = null; description = '' - Path to a file containing the secret key. + Path to a file containing the [secret key]. + + The secret key is used for hashing passwords and signing HTTP cookies. + It can be rotated without data loss; however all existing user sessions + will be invalidated. + + ::: {.note} + If unset, a random secret will be created automatically at + `/var/lib/netbox/secret.key`. + ::: + + [secret key]: https://netboxlabs.com/docs/netbox/configuration/required-parameters/#secret_key ''; }; - apiTokenPeppersFile = lib.mkOption { - type = with lib.types; nullOr path; + apiTokenPepperFiles = lib.mkOption { + type = + with lib.types; + attrsOf (pathWith { + inStore = false; + }); + default = { + "1" = "${cfg.dataDir}/pepper.1"; + }; + defaultText = lib.literalExpression '' + { + "1" = "''${config.services.netbox.dataDir}/pepper.1"; + } + ''; + example = { + "1" = "/run/keys/netbox-pepper-old"; + "2" = "/run/keys/netbox-pepper-current"; + }; description = '' - Path to a file containing the API_TOKEN_PEPPER that will be configured for the pepper_id with the id 1. - This is required for netbox >= 4.5. For generating this pepper, - [consider using `$INSTALL_ROOT/netbox/generate_secret_key.py`](https://netboxlabs.com/docs/netbox/configuration/required-parameters/#api_token_peppers) + Mapping of cryptographic pepper IDs to files containing the pepper values. + + Peppers provide an additional secret input in hashing operations. They + are required for v2 API tokens (NetBox 4.5+). + + ::: {.note} + By default a random pepper will be created automatically at + `/var/lib/netbox/pepper.1` and configured with pepper ID 1. + ::: + + [cryptographic peppers]: https://netboxlabs.com/docs/netbox/configuration/required-parameters/#api_token_peppers ''; }; @@ -226,6 +285,7 @@ in AUTH_LDAP_FIND_GROUP_PERMS = True ''; }; + keycloakClientSecret = lib.mkOption { type = with lib.types; nullOr path; default = null; @@ -291,15 +351,17 @@ in }; extraConfig = '' - with open("${cfg.secretKeyFile}", "r") as file: + with open("${secretKeyFile}", "r") as file: SECRET_KEY = file.readline() + + API_TOKEN_PEPPERS = { + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (id: file: '' + ${id}: open("${file}", "r").read().strip(), + '') cfg.apiTokenPepperFiles + )} + } '' - + (lib.optionalString (cfg.apiTokenPeppersFile != null) '' - with open("${cfg.apiTokenPeppersFile}", "r") as pepper_file: - API_TOKEN_PEPPERS = { - 1: pepper_file.readline(), - }; - '') + (lib.optionalString (cfg.keycloakClientSecret != null) '' with open("${cfg.keycloakClientSecret}", "r") as file: SOCIAL_AUTH_KEYCLOAK_SECRET = file.readline() @@ -356,6 +418,21 @@ in environment.PYTHONPATH = pkg.pythonPath; preStart = '' + # Generate random default secrets, if the user didn't supply any. + ${optionalString (cfg.secretKeyFile == null) '' + if [ ! -e "${secretKeyFile}" ]; then + ${pkg}/opt/netbox/netbox/generate_secret_key.py > "${secretKeyFile}" + fi + ''} + ${optionalString + (any (path: path == "${cfg.dataDir}/pepper.1") (attrValues cfg.apiTokenPepperFiles)) + '' + if [ ! -e "${cfg.dataDir}/pepper.1" ]; then + ${pkg}/opt/netbox/netbox/generate_secret_key.py > "${cfg.dataDir}/pepper.1" + fi + '' + } + # On the first run, or on upgrade / downgrade, run migrations and related. # This mostly correspond to upstream NetBox's 'upgrade.sh' script. versionFile="${cfg.dataDir}/version" @@ -463,12 +540,5 @@ in }; users.groups.netbox = { }; users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ]; - - assertions = [ - { - assertion = (lib.versionAtLeast cfg.package.version "4.5") -> (cfg.apiTokenPeppersFile != null); - message = "NetBox 4.5 or newer require setting `services.netbox.apiTokenPeppersFile`"; - } - ]; }; } diff --git a/nixos/tests/web-apps/netbox-upgrade.nix b/nixos/tests/web-apps/netbox-upgrade.nix index c965e0c9b0f3..389f60856c68 100644 --- a/nixos/tests/web-apps/netbox-upgrade.nix +++ b/nixos/tests/web-apps/netbox-upgrade.nix @@ -34,12 +34,6 @@ in # Pick the NetBox package from this config's "pkgs" argument, # so that `nixpkgs.config.permittedInsecurePackages` works package = pkgs.${oldNetbox}; - secretKeyFile = pkgs.writeText "secret" '' - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - ''; - apiTokenPeppersFile = pkgs.writeText "pepper" '' - kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_ - ''; }; services.nginx = { diff --git a/nixos/tests/web-apps/netbox/default.nix b/nixos/tests/web-apps/netbox/default.nix index 803cf5c25ed3..da9d2c9bfe0f 100644 --- a/nixos/tests/web-apps/netbox/default.nix +++ b/nixos/tests/web-apps/netbox/default.nix @@ -34,13 +34,6 @@ import ../../make-test-python.nix ( services.netbox = { enable = true; package = netbox; - secretKeyFile = pkgs.writeText "secret" '' - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 - ''; - # Value from https://netbox.readthedocs.io/en/feature/configuration/required-parameters/#api_token_peppers - apiTokenPeppersFile = pkgs.writeText "pepper" '' - kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_ - ''; enableLdap = true; ldapConfigPath = pkgs.writeText "ldap_config.py" ''