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" ''