mirror of
https://github.com/Mic92/sops-nix.git
synced 2025-03-16 05:28:15 +00:00
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.
531 lines
19 KiB
Nix
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
|
|
'';
|
|
});
|
|
}
|