Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 77 additions & 8 deletions nixos/modules/services/web-apps/stash.nix
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,69 @@ let
done
'';
};

secretOptionType =
let
contractSecretsType = types.submodule {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from what i can tell the submodules look similar, with this one also providing default values.
would it cause recursion problems if such a module were just exposed in some mutually accessible location, be it at e.g. contracts.secrets?

options.contracts could be an option taking type attrsOf deferredModule or the like, so that on the config side we could then plug in your shared submodule for contract.secrets, then access that from both places that got it now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree but isn't this one more implementation competing with the other two big PRs? Not saying we shouldn't think about it but TBH I purposely kept that out of this PR to focus on the contract for secrets. Unless a good argument for, I'd rather not introduce this complexity in the PR.

My hope by doing it like this is to create a handful of contracts, use them in a handful of places and then we can think of an underlying implementation with concrete use cases.

Note also that on one side, we set default values on the consumer side of the contract and on the other on the provider side of the contract. So both sides are not exactly the same although the overall shape is.

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 = {
Expand Down Expand Up @@ -418,7 +481,7 @@ in
};

passwordFile = mkOption {
type = types.nullOr types.path;
type = types.nullOr secretOptionType;
default = null;
example = "/path/to/password/file";
description = ''
Expand All @@ -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.";
};

Expand Down Expand Up @@ -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
Comment thread
ibizaman marked this conversation as resolved.
}) \
${lib.getExe pkgs.yq-go} '
.jwt_secret_key = strenv(jwtSecretKeyFile) |
.session_store_key = strenv(sessionStoreKeyFile) |
Expand Down
127 changes: 127 additions & 0 deletions nixos/modules/testing/hardcoded-secret.nix
Original file line number Diff line number Diff line change
@@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice, i think may be common for modules to have options not specific to a given instance like the options.testing.hardcoded-secret.<name> here.

if we were to add those later tho, i would imagine that in the case of this current module one would then want to move the current options.testing.hardcoded-secret down.

i'm not sure nixos has a great way of moving options down tho (renamed options are expected to not simultaneously exist as the new of a new option), so in that sense, i think it could be helpful to complicate this module like that already, maybe like my earlier example there.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjust terminology here? (+ at stash)

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;
};
}
3 changes: 3 additions & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions nixos/tests/contracts/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{ runTest }:
{
filesecrets-hardcoded-secret = runTest ./filesecrets/hardcoded-secret.nix;
}
29 changes: 29 additions & 0 deletions nixos/tests/contracts/filesecrets/hardcoded-secret.nix
Original file line number Diff line number Diff line change
@@ -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 ];
}
89 changes: 89 additions & 0 deletions nixos/tests/contracts/filesecrets/test.nix
Original file line number Diff line number Diff line change
@@ -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}'")
'';
}
Loading
Loading