diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c33d7a1cbbb16..f1533c873c501 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -2034,6 +2034,7 @@ ./tasks/stratis.nix ./tasks/swraid.nix ./tasks/trackpoint.nix + ./testing/hardcoded-secret.nix ./testing/service-runner.nix ./virtualisation/amazon-options.nix ./virtualisation/appvm.nix diff --git a/nixos/modules/services/web-apps/stash.nix b/nixos/modules/services/web-apps/stash.nix index e4e1c65d09327..7726c2e71d04c 100644 --- a/nixos/modules/services/web-apps/stash.nix +++ b/nixos/modules/services/web-apps/stash.nix @@ -366,6 +366,69 @@ let done ''; }; + + secretOptionType = + let + contractSecretsType = types.submodule { + options = { + input = mkOption { + description = "Input of the contract for file secrets."; + default = { }; + type = types.submodule { + options = { + mode = mkOption { + description = '' + Mode the secret file must have. + ''; + type = types.str; + default = "0400"; + readOnly = true; + }; + + owner = mkOption { + description = '' + Linux user that must own the secret file. + ''; + type = types.str; + default = cfg.user; + readOnly = true; + }; + + group = mkOption { + description = '' + Linux group that must own the secret file. + ''; + type = types.str; + default = cfg.group; + readOnly = true; + }; + }; + }; + }; + + output = mkOption { + description = "Output of the contract for file secrets."; + type = types.submodule { + options = { + path = mkOption { + type = types.str; + description = '' + Path to the file containing the secret generated out of band. + + This path will exist after deploying to a target host, + it is not available through the nix store. + ''; + }; + }; + }; + }; + }; + }; + in + types.oneOf [ + types.path + contractSecretsType + ]; in { meta = { @@ -418,7 +481,7 @@ in }; passwordFile = mkOption { - type = types.nullOr types.path; + type = types.nullOr secretOptionType; default = null; example = "/path/to/password/file"; description = '' @@ -431,12 +494,12 @@ in ''; }; - jwtSecretKeyFile = mkOption { - type = types.path; + jwtSecretKey = mkOption { + type = secretOptionType; description = "Path to file containing a secret used to sign JWT tokens."; }; - sessionStoreKeyFile = mkOption { - type = types.path; + sessionStoreKey = mkOption { + type = secretOptionType; description = "Path to file containing a secret for session store."; }; @@ -514,9 +577,15 @@ in install -d ${cfg.settings.generated} if [[ -z "${toString cfg.mutableSettings}" || ! -f ${cfg.dataDir}/config.yml ]]; then env \ - password=$(< ${cfg.passwordFile}) \ - jwtSecretKeyFile=$(< ${cfg.jwtSecretKeyFile}) \ - sessionStoreKeyFile=$(< ${cfg.sessionStoreKeyFile}) \ + password=$(< ${ + if lib.isPath cfg.passwordFile then cfg.passwordFile else cfg.passwordFile.output.path + }) \ + jwtSecretKeyFile=$(< ${ + if lib.isPath cfg.jwtSecretKey then cfg.jwtSecretKey else cfg.jwtSecretKey.output.path + }) \ + sessionStoreKeyFile=$(< ${ + if lib.isPath cfg.sessionStoreKey then cfg.sessionStoreKey else cfg.sessionStoreKey.output.path + }) \ ${lib.getExe pkgs.yq-go} ' .jwt_secret_key = strenv(jwtSecretKeyFile) | .session_store_key = strenv(sessionStoreKeyFile) | diff --git a/nixos/modules/testing/hardcoded-secret.nix b/nixos/modules/testing/hardcoded-secret.nix new file mode 100644 index 0000000000000..f553576868aa8 --- /dev/null +++ b/nixos/modules/testing/hardcoded-secret.nix @@ -0,0 +1,127 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.testing.hardcoded-secret; + + inherit (lib) mapAttrs' mkOption nameValuePair; + inherit (lib.types) + attrsOf + str + submodule + ; + inherit (pkgs) writeText; +in +{ + options.testing.hardcoded-secret = mkOption { + default = { }; + description = '' + Hardcoded file secrets. These should only be used in tests. + + They aim to replace the usage of pkgs.writeText in NixOS VM tests + as those make the file world readable + while this module set runtime permissions on the file. + This makes the tests more accurate, ensuring the permissions + set by the contract consumer are correct. + ''; + example = lib.literalExpression '' + { + mySecret = { + input = { + user = "me"; + mode = "0400"; + }; + content = "My Secret"; + }; + } + ''; + type = attrsOf ( + submodule ( + { name, ... }: + { + options = { + input = mkOption { + description = "Input of the contract for file secrets."; + type = lib.types.submodule { + options = { + mode = mkOption { + description = '' + Mode the secret file must have. + ''; + type = str; + default = "0400"; + }; + + owner = mkOption { + description = '' + Linux user that must own the secret file. + ''; + type = str; + default = "root"; + }; + + group = mkOption { + description = '' + Linux group that must own the secret file. + ''; + type = str; + default = "root"; + }; + }; + }; + }; + + output = mkOption { + description = "Output of the contract for file secrets."; + default = { }; + type = lib.types.submodule { + options = { + path = mkOption { + type = str; + description = '' + Path to the file containing the secret generated out of band. + + This path will exist after deploying to a target host, + it is not available through the nix store. + ''; + default = "/run/hardcodedsecrets/${name}"; + }; + }; + }; + }; + + content = mkOption { + type = str; + description = '' + Content of the secret as a string. + + This will be stored in the nix store and should only be used for testing or maybe in dev. + ''; + }; + }; + } + ) + ); + }; + + config = { + system.activationScripts = mapAttrs' ( + n: cfg': + let + source = writeText "hardcodedsecret_${n}_content" cfg'.content; + + inherit (cfg') input output; + in + nameValuePair "hardcodedsecret_${n}" '' + mkdir -p "$(dirname "${output.path}")" + touch "${output.path}" + chmod ${input.mode} "${output.path}" + chown ${input.owner}:${input.group} "${output.path}" + cp ${source} "${output.path}" + '' + ) cfg; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 44add4090727d..b97c21fc387cb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -420,6 +420,9 @@ in containers-restart_networking = runTest ./containers-restart_networking.nix; containers-tmpfs = runTest ./containers-tmpfs.nix; containers-unified-hierarchy = runTest ./containers-unified-hierarchy.nix; + contracts = import ./contracts { + inherit runTest; + }; convos = runTest ./convos.nix; coredns = runTest ./coredns.nix; corerad = runTest ./corerad.nix; diff --git a/nixos/tests/contracts/default.nix b/nixos/tests/contracts/default.nix new file mode 100644 index 0000000000000..548829012fc85 --- /dev/null +++ b/nixos/tests/contracts/default.nix @@ -0,0 +1,4 @@ +{ runTest }: +{ + filesecrets-hardcoded-secret = runTest ./filesecrets/hardcoded-secret.nix; +} diff --git a/nixos/tests/contracts/filesecrets/hardcoded-secret.nix b/nixos/tests/contracts/filesecrets/hardcoded-secret.nix new file mode 100644 index 0000000000000..e90cadb767bdd --- /dev/null +++ b/nixos/tests/contracts/filesecrets/hardcoded-secret.nix @@ -0,0 +1,29 @@ +args@{ + lib, + config, + pkgs, + ... +}: +let + test = import ./test.nix args; +in +test { + name = "contracts-secrets-hardcoded-secret"; + providerRoot = [ + "testing" + "hardcoded-secret" + "mysecret" + ]; + extraModules = [ + ../../../modules/testing/hardcoded-secret.nix + ( + { config, ... }: + { + testing.hardcoded-secret.mysecret.content = config.test.content; + } + ) + ]; +} +// { + meta.maintainers = [ lib.maintainers.ibizaman ]; +} diff --git a/nixos/tests/contracts/filesecrets/test.nix b/nixos/tests/contracts/filesecrets/test.nix new file mode 100644 index 0000000000000..00d2bdf251df8 --- /dev/null +++ b/nixos/tests/contracts/filesecrets/test.nix @@ -0,0 +1,89 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (lib) getAttrFromPath setAttrByPath; + inherit (lib) mkOption types; +in +{ + name, + providerRoot, + extraModules ? [ ], +}: +{ + name = "contracts_filesecrets_${name}"; + + nodes.machine = + { config, ... }: + { + imports = extraModules; + + options.test = { + owner = mkOption { + type = types.str; + default = "root"; + }; + + group = mkOption { + type = types.str; + default = "root"; + }; + + mode = mkOption { + type = types.str; + default = "0400"; + }; + + content = mkOption { + type = types.str; + default = "a super secret secret!"; + }; + }; + + config = lib.mkMerge [ + (setAttrByPath providerRoot { + input = { + inherit (config.test) owner group mode; + }; + }) + (lib.mkIf (config.test.owner != "root") { + users.users.${config.test.owner}.isNormalUser = true; + }) + (lib.mkIf (config.test.group != "root") { + users.groups.${config.test.group} = { }; + }) + ]; + }; + + testScript = + { nodes, ... }: + let + cfg = nodes.machine; + inherit (getAttrFromPath providerRoot nodes.machine) output; + in + '' + owner = machine.succeed("stat -c '%U' ${output.path}").strip() + print(f"Got owner {owner}") + if owner != "${cfg.test.owner}": + raise Exception(f"Owner should be '${cfg.test.owner}' but got '{owner}'") + + group = machine.succeed("stat -c '%G' ${output.path}").strip() + print(f"Got group {group}") + if group != "${cfg.test.group}": + raise Exception(f"Group should be '${cfg.test.group}' but got '{group}'") + + mode = str(int(machine.succeed("stat -c '%a' ${output.path}").strip())) + print(f"Got mode {mode}") + wantedMode = str(int("${cfg.test.mode}")) + if mode != wantedMode: + raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") + + content = machine.succeed("cat ${output.path}").strip() + print(f"Got content {content}") + if content != "${cfg.test.content}": + raise Exception(f"Content should be '${cfg.test.content}' but got '{content}'") + ''; +} diff --git a/nixos/tests/stash.nix b/nixos/tests/stash.nix index 838a5e8a43c6d..119072363ccc7 100644 --- a/nixos/tests/stash.nix +++ b/nixos/tests/stash.nix @@ -9,61 +9,75 @@ import ./make-test-python.nix ( name = "stash"; meta.maintainers = pkgs.stash.meta.maintainers; - nodes.machine = { - services.stash = { - inherit dataDir; - enable = true; + nodes.machine = + { config, ... }: + { + services.stash = { + inherit dataDir; + enable = true; - username = "test"; - passwordFile = pkgs.writeText "stash-password" "MyPassword"; + username = "test"; + passwordFile.output = config.testing.hardcoded-secret."stash-password".output; - jwtSecretKeyFile = pkgs.writeText "jwt_secret_key" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - sessionStoreKeyFile = pkgs.writeText "session_store_key" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + jwtSecretKey.output.path = config.testing.hardcoded-secret."jwt_secret_key".output.path; + sessionStoreKey.output.path = config.testing.hardcoded-secret."session_store_key".output.path; - plugins = - let - src = pkgs.fetchFromGitHub { - owner = "stashapp"; - repo = "CommunityScripts"; - rev = "9b6fac4934c2fac2ef0859ea68ebee5111fc5be5"; - hash = "sha256-PO3J15vaA7SD4r/LyHlXjnpaeYAN9Q++O94bIWdz7OA="; - }; - in - [ - (pkgs.runCommand "stashNotes" { inherit src; } '' - mkdir -p $out/plugins - cp -r $src/plugins/stashNotes $out/plugins/stashNotes - '') - (pkgs.runCommand "Theme-Plex" { inherit src; } '' - mkdir -p $out/plugins - cp -r $src/themes/Theme-Plex $out/plugins/Theme-Plex - '') - ]; + plugins = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScripts"; + rev = "9b6fac4934c2fac2ef0859ea68ebee5111fc5be5"; + hash = "sha256-PO3J15vaA7SD4r/LyHlXjnpaeYAN9Q++O94bIWdz7OA="; + }; + in + [ + (pkgs.runCommand "stashNotes" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/plugins/stashNotes $out/plugins/stashNotes + '') + (pkgs.runCommand "Theme-Plex" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/themes/Theme-Plex $out/plugins/Theme-Plex + '') + ]; - mutableScrapers = true; - scrapers = - let - src = pkgs.fetchFromGitHub { - owner = "stashapp"; - repo = "CommunityScrapers"; - rev = "2ece82d17ddb0952c16842b0775274bcda598d81"; - hash = "sha256-AEmnvM8Nikhue9LNF9dkbleYgabCvjKHtzFpMse4otM="; - }; - in - [ - (pkgs.runCommand "FTV" { inherit src; } '' - mkdir -p $out/scrapers/FTV - cp -r $src/scrapers/FTV.yml $out/scrapers/FTV - '') - ]; + mutableScrapers = true; + scrapers = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScrapers"; + rev = "2ece82d17ddb0952c16842b0775274bcda598d81"; + hash = "sha256-AEmnvM8Nikhue9LNF9dkbleYgabCvjKHtzFpMse4otM="; + }; + in + [ + (pkgs.runCommand "FTV" { inherit src; } '' + mkdir -p $out/scrapers/FTV + cp -r $src/scrapers/FTV.yml $out/scrapers/FTV + '') + ]; - settings = { - inherit host port; + settings = { + inherit host port; - stash = [ { path = "/srv"; } ]; + stash = [ { path = "/srv"; } ]; + }; + }; + testing.hardcoded-secret."stash-password" = { + input = config.services.stash.passwordFile.input; + content = "MyPassword"; + }; + testing.hardcoded-secret."jwt_secret_key" = { + input = config.services.stash.jwtSecretKey.input; + content = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + }; + testing.hardcoded-secret."session_store_key" = { + input = config.services.stash.sessionStoreKey.input; + content = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; }; }; - }; testScript = '' machine.wait_for_unit("stash.service")