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.
This commit is contained in:
Martin Weinelt
2026-07-07 16:58:05 +02:00
parent d407951447
commit 46c6aaeac7
3 changed files with 91 additions and 34 deletions
+91 -21
View File
@@ -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`";
}
];
};
}
-6
View File
@@ -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 = {
-7
View File
@@ -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" ''