1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2025-03-16 05:28:15 +00:00
sops-nix/pkgs/sops-install-secrets/nixos-test.nix
Jeremy Fleischman c9f6b151cc fix: create template.path symlink
This fixes https://github.com/Mic92/sops-nix/issues/653.

Note: `main.go` has been slowly accumulating shared logic between vanilla
"secrets" and "templates". It feels to me like we could DRY up some of
the logic in here by creating some shared "interface" that they both
implement. I opted not to try to tackle that here, though.
2024-11-08 06:07:13 +00:00

531 lines
19 KiB
Nix

{ lib, testers }:
let
userPasswordTest = name: extraConfig: testers.runNixOSTest {
inherit name;
nodes.machine = { config, lib, ... }: {
imports = [
../../modules/sops
extraConfig
];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key.neededForUsers = true;
secrets."nested/test/file".owner = "example-user";
};
system.switch.enable = true;
users.users.example-user = lib.mkMerge [
(lib.mkIf (! config.systemd.sysusers.enable) {
isNormalUser = true;
hashedPasswordFile = config.sops.secrets.test_key.path;
})
(lib.mkIf config.systemd.sysusers.enable {
isSystemUser = true;
group = "users";
hashedPasswordFile = config.sops.secrets.test_key.path;
})
];
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
assert user == "example-user", f"Expected 'example-user', got '{user}'"
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
'' + lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
'';
};
in {
ssh-keys = testers.runNixOSTest {
name = "sops-ssh-keys";
nodes.server = { ... }: {
imports = [ ../../modules/sops ];
services.openssh.enable = true;
services.openssh.hostKeys = [{
type = "rsa";
bits = 4096;
path = ./test-assets/ssh-key;
}];
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key = { };
};
testScript = ''
start_all()
server.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
};
pruning = testers.runNixOSTest {
name = "sops-pruning";
nodes.machine = { lib, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
keepGenerations = lib.mkDefault 0;
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
specialisation.pruning.configuration.sops.keepGenerations = 10;
};
testScript = ''
# Force us to generation 100
machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric")
machine.succeed("ln -fsn /run/secrets.d/99 /run/secrets")
machine.succeed("/run/current-system/activate")
machine.succeed("test -d /run/secrets.d/100")
# Ensure nothing is pruned, these are just random numbers
machine.succeed("test -d /run/secrets.d/1")
machine.succeed("test -d /run/secrets.d/90")
machine.succeed("test -d /run/secrets.d/non-numeric")
machine.succeed("/run/current-system/specialisation/pruning/bin/switch-to-configuration test")
print(machine.succeed("ls -la /run/secrets.d/"))
# Ensure stuff was properly pruned.
# We are now at generation 101 so 92 must exist when we keep 10 generations
# and 91 must not.
machine.fail("test -d /run/secrets.d/91")
machine.succeed("test -d /run/secrets.d/92")
machine.succeed("test -d /run/secrets.d/non-numeric")
'';
};
age-keys = testers.runNixOSTest {
name = "sops-age-keys";
nodes.machine = { config, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets = {
test_key = { };
test_key_someuser_somegroup = {
uid = config.users.users."someuser".uid;
gid = config.users.groups."somegroup".gid;
key = "test_key";
};
test_key_someuser_root = {
uid = config.users.users."someuser".uid;
key = "test_key";
};
test_key_root_root = {
key = "test_key";
};
test_key_1001_1001 = {
uid = 1001;
gid = 1001;
key = "test_key";
};
};
};
users.users."someuser" = {
uid = 1000;
group = "somegroup";
isNormalUser = true;
};
users.groups."somegroup" = {
gid = 1000;
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
};
testScript = ''
start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
with subtest("test ownership"):
machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_somegroup) = '1000' ]")
machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_somegroup) = '1000' ]")
machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_somegroup) = 'someuser' ]")
machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_somegroup) = 'somegroup' ]")
machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_root) = '1000' ]")
machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_root) = '0' ]")
machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_root) = 'someuser' ]")
machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_root) = 'root' ]")
machine.succeed("[ $(stat -c%u /run/secrets/test_key_1001_1001) = '1001' ]")
machine.succeed("[ $(stat -c%g /run/secrets/test_key_1001_1001) = '1001' ]")
machine.succeed("[ $(stat -c%U /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]")
machine.succeed("[ $(stat -c%G /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]")
'';
};
age-ssh-keys = testers.runNixOSTest {
name = "sops-age-ssh-keys";
nodes.machine = {
imports = [ ../../modules/sops ];
services.openssh.enable = true;
services.openssh.hostKeys = [{
type = "ed25519";
path = ./test-assets/ssh-ed25519-key;
}];
sops = {
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
# Generate a key and append it to make sure it appending doesn't break anything
age = {
keyFile = "/tmp/testkey";
generateKey = true;
};
};
};
testScript = ''
start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
};
pgp-keys = testers.runNixOSTest {
name = "sops-pgp-keys";
nodes.server = { lib, config, ... }: {
imports = [ ../../modules/sops ];
users.users.someuser = {
isSystemUser = true;
group = "nogroup";
};
sops.gnupg.home = "/run/gpghome";
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key.owner = config.users.users.someuser.name;
sops.secrets."nested/test/file".owner = config.users.users.someuser.name;
sops.secrets.existing-file = {
key = "test_key";
path = "/run/existing-file";
};
# must run before sops
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
cp -r ${./test-assets/gnupghome} /run/gpghome
chmod -R 700 /run/gpghome
touch /run/existing-file
'';
# Useful for debugging
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
#environment.variables = {
# GNUPGHOME = "/run/gpghome";
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
# SOPSFILE = "${./test-assets/secrets.yaml}";
#};
};
testScript = ''
def assertEqual(exp: str, act: str) -> None:
if exp != act:
raise Exception(f"{exp!r} != {act!r}")
start_all()
value = server.succeed("cat /run/secrets/test_key")
assertEqual("test_value", value)
server.succeed("runuser -u someuser -- cat /run/secrets/test_key >&2")
value = server.succeed("cat /run/secrets/nested/test/file")
assertEqual(value, "another value")
target = server.succeed("readlink -f /run/existing-file")
assertEqual("/run/secrets.d/1/existing-file", target.strip())
'';
};
templates = testers.runNixOSTest {
name = "sops-templates";
nodes.machine = { config, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
sops.templates.test_template = {
content = ''
This line is not modified.
The next value will be replaced by ${config.sops.placeholder.test_key}
This line is also not modified.
'';
mode = "0400";
owner = "someuser";
group = "somegroup";
};
sops.templates.test_default = {
content = ''
Test value: ${config.sops.placeholder.test_key}
'';
path = "/etc/externally/linked";
};
users.groups.somegroup = {};
users.users.someuser = {
isSystemUser = true;
group = "somegroup";
};
};
testScript = ''
def assertEqual(exp: str, act: str) -> None:
if exp != act:
raise Exception(f"{exp!r} != {act!r}")
start_all()
machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_template) = 'someuser' ]")
machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_template) = 'somegroup' ]")
machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_default) = 'root' ]")
machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_default) = 'root' ]")
expected = """\
This line is not modified.
The next value will be replaced by test_value
This line is also not modified.
"""
rendered = machine.succeed("cat /run/secrets/rendered/test_template")
expected_default = """\
Test value: test_value
"""
rendered_default = machine.succeed("cat /run/secrets/rendered/test_default")
assertEqual(expected, rendered)
assertEqual(expected_default, rendered_default)
# Confirm that `test_default` was symlinked to the appropriate place.
realpath = machine.succeed("realpath /etc/externally/linked").strip()
assertEqual(realpath, "/run/secrets.d/1/rendered/test_default")
'';
};
restart-and-reload = testers.runNixOSTest {
name = "sops-restart-and-reload";
nodes.machine = {config, ...}: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = {
restartUnits = [ "restart-unit.service" "reload-unit.service" ];
reloadUnits = [ "reload-trigger.service" ];
};
templates.test_template.content = ''
this is a template with
a secret: ${config.sops.placeholder.test_key}
'';
};
system.switch.enable = true;
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
systemd.services."restart-unit" = {
description = "Restart unit";
# not started on boot
serviceConfig = { ExecStart = "/bin/sh -c 'echo ok > /restarted'"; };
};
systemd.services."reload-unit" = {
description = "Reload unit";
wantedBy = [ "multi-user.target" ];
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
};
systemd.services."reload-trigger" = {
description = "Reload trigger unit";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
};
};
testScript = ''
def assertOutput(output, *expected_lines):
expected_lines = list(expected_lines)
# Remove unrelated fluff that shows up in the output of `switch-to-configuration`.
prefix = "setting up /etc...\n"
if output.startswith(prefix):
output = output.removeprefix(prefix)
actual_lines = output.splitlines(keepends=False)
if actual_lines != expected_lines:
raise Exception(f"{actual_lines} != {expected_lines}")
machine.wait_for_unit("multi-user.target")
machine.fail("test -f /restarted")
machine.fail("test -f /reloaded")
# Nothing is to be restarted after boot
machine.fail("ls /run/nixos/*-list")
# Nothing happens when the secret is not changed
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.fail("test -f /restarted")
machine.fail("test -f /reloaded")
# Ensure the secret is changed
machine.succeed(": > /run/secrets/test_key")
# The secret is changed, now something should happen
machine.succeed("/run/current-system/bin/switch-to-configuration test")
# Ensure something happened
machine.succeed("test -f /restarted")
machine.succeed("test -f /reloaded")
with subtest("change detection"):
machine.succeed("rm /run/secrets/test_key")
machine.succeed("rm /run/secrets/rendered/test_template")
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
assertOutput(
out,
"adding secret: test_key",
"adding rendered secret: test_template",
)
machine.succeed(": > /run/secrets/test_key")
machine.succeed(": > /run/secrets/rendered/test_template")
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
assertOutput(
out,
"modifying secret: test_key",
"modifying rendered secret: test_template",
)
machine.succeed(": > /run/secrets/another_key")
machine.succeed(": > /run/secrets/rendered/another_template")
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
assertOutput(
out,
"removing secret: another_key",
"removing rendered secret: another_template",
)
with subtest("dry activation"):
machine.succeed("rm /run/secrets/test_key")
machine.succeed("rm /run/secrets/rendered/test_template")
machine.succeed(": > /run/secrets/another_key")
machine.succeed(": > /run/secrets/rendered/another_template")
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
assertOutput(
out,
"would add secret: test_key",
"would remove secret: another_key",
"would add rendered secret: test_template",
"would remove rendered secret: another_template",
)
# Verify that we did not actually activate the new configuration.
machine.fail("test -f /run/secrets/test_key")
machine.fail("test -f /run/secrets/rendered/test_template")
machine.succeed("test -f /run/secrets/another_key")
machine.succeed("test -f /run/secrets/rendered/another_template")
# Now actually activate and sanity check the resulting secrets.
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("test -f /run/secrets/test_key")
machine.succeed("test -f /run/secrets/rendered/test_template")
machine.fail("test -f /run/secrets/another_key")
machine.fail("test -f /run/secrets/rendered/another_template")
# Remove the restarted/reloaded indicators so we can confirm a
# dry-activate doesn't trigger systemd units.
machine.succeed("rm /restarted /reloaded")
machine.succeed(": > /run/secrets/test_key")
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
assertOutput(
out,
"would modify secret: test_key",
)
machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]")
machine.fail("test -f /restarted") # not done in dry mode
machine.fail("test -f /reloaded") # not done in dry mode
'';
};
user-passwords = userPasswordTest "sops-user-passwords" {
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
};
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") {
user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" ({ pkgs, ... }: {
systemd.sysusers.enable = true;
users.mutableUsers = true;
system.etc.overlay.enable = true;
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
# must run before sops sets up keys
systemd.services."sops-install-secrets-for-users".preStart = ''
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
});
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") {
user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" ({ pkgs, ... }: {
services.userborn.enable = true;
users.mutableUsers = false;
system.etc.overlay.enable = true;
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
# must run before sops sets up keys
systemd.services."sops-install-secrets-for-users".preStart = ''
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
});
}