diff --git a/README.md b/README.md index 983d75f..ee56fd6 100644 --- a/README.md +++ b/README.md @@ -562,6 +562,20 @@ the service needs a token and a SSH private key to function. +## Restarting/reloading systemd units on secret change + +**With NixOS 22.05**, it is possible to restart or reload units when a secret changes or is newly initialized. + +This behavior can be configured per-secret: +```nix +{ + sops.secrets."home-assistant-secrets.yaml" = { + restartUnits = [ "home-assistant.service" ]; + # there is also `reloadUnits` which acts like a `reloadTrigger` in a NixOS systemd service + }; +} +``` + ## Symlinks to other directories Some services might expect files in certain locations. diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 5c0b694..d566a54 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -92,6 +92,15 @@ let This works the same way as . ''; }; + reloadUnits = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when this secret changes. + This works the same way as . + ''; + }; neededForUsers = mkOption { type = types.bool; default = false; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index f727a0f..03ee6f9 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -36,6 +36,7 @@ type secret struct { Format FormatType `json:"format"` Mode string `json:"mode"` RestartUnits []string `json:"restartUnits"` + ReloadUnits []string `json:"reloadUnits"` value []byte mode os.FileMode owner int @@ -679,6 +680,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret) error { var restart []string + var reload []string newSecrets := make(map[string]bool) modifiedSecrets := make(map[string]bool) @@ -702,6 +704,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if os.IsNotExist(err) { // File did not exist before restart = append(restart, secret.RestartUnits...) + reload = append(reload, secret.ReloadUnits...) newSecrets[secret.Name] = true continue } @@ -716,6 +719,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if !bytes.Equal(oldData, newData) { restart = append(restart, secret.RestartUnits...) + reload = append(reload, secret.ReloadUnits...) modifiedSecrets[secret.Name] = true } } @@ -744,6 +748,9 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if err := writeLines(restart, dryPrefix+"-restart-list"); err != nil { return err } + if err := writeLines(reload, dryPrefix+"-reload-list"); err != nil { + return err + } // Do not output changes if not requested if !logcfg.SecretChanges { diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 2e673d1..095d576 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -107,6 +107,7 @@ func testGPG(t *testing.T) { Path: path.Join(testdir.path, "test-target"), Mode: "0400", RestartUnits: []string{"affected-service"}, + ReloadUnits: []string{"affected-reload-service"}, } var jsonSecret, binarySecret secret @@ -205,6 +206,7 @@ func testSSHKey(t *testing.T) { Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, + ReloadUnits: []string{"affected-reload-service"}, } m := manifest{ @@ -237,6 +239,7 @@ func TestAge(t *testing.T) { Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, + ReloadUnits: []string{"affected-reload-service"}, } m := manifest{ @@ -269,6 +272,7 @@ func TestAgeWithSSH(t *testing.T) { Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, + ReloadUnits: []string{"affected-reload-service"}, } m := manifest{ @@ -302,6 +306,7 @@ func TestValidateManifest(t *testing.T) { Path: path.Join(testdir.path, "test-target"), Mode: "0400", RestartUnits: []string{}, + ReloadUnits: []string{}, } m := manifest{ diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 27dd5d5..174ac0d 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -204,109 +204,119 @@ inherit (pkgs) system; }; -} // pkgs.lib.optionalAttrs (pkgs.lib.versionAtLeast (pkgs.lib.versions.majorMinor pkgs.lib.version) "21.11") { - # This feature got reverted in nixpkgs... - #restart-and-reload = makeTest { - # name = "sops-restart-and-reload"; - # machine = { pkgs, lib, config, ... }: { - # imports = [ - # ../../modules/sops - # ]; +} // pkgs.lib.optionalAttrs (pkgs.lib.versionAtLeast (pkgs.lib.versions.majorMinor pkgs.lib.version) "22.05") { + restart-and-reload = makeTest { + name = "sops-restart-and-reload"; + machine = { pkgs, lib, config, ... }: { + imports = [ + ../../modules/sops + ]; - # sops = { - # age.keyFile = ./test-assets/age-keys.txt; - # defaultSopsFile = ./test-assets/secrets.yaml; - # secrets.test_key = { - # restartUnits = [ "restart-unit.service" "reload-unit.service" ]; - # }; - # }; + sops = { + age.keyFile = ./test-assets/age-keys.txt; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key = { + restartUnits = [ "restart-unit.service" "reload-unit.service" ]; + reloadUnits = [ "reload-trigger.service" ]; + }; + }; - # systemd.services."restart-unit" = { - # description = "Restart unit"; - # # not started on boot - # serviceConfig = { - # ExecStart = "/bin/sh -c 'echo ok > /restarted'"; - # }; - # }; - # systemd.services."reload-unit" = { - # description = "Restart unit"; - # wantedBy = [ "multi-user.target" ]; - # reloadIfChanged = true; - # serviceConfig = { - # Type = "oneshot"; - # RemainAfterExit = true; - # ExecStart = "/bin/sh -c true"; - # ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; - # }; - # }; - # }; - # testScript = '' - # machine.wait_for_unit("multi-user.target") - # machine.fail("test -f /restarted") - # machine.fail("test -f /reloaded") + 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 = '' + 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 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") + # 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") + # 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") + # 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") + # Ensure something happened + machine.succeed("test -f /restarted") + machine.succeed("test -f /reloaded") - # with subtest("change detection"): - # machine.succeed("rm /run/secrets/test_key") - # out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - # if "adding secret" not in out: - # raise Exception("Addition detection does not work") + with subtest("change detection"): + machine.succeed("rm /run/secrets/test_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + if "adding secret" not in out: + raise Exception("Addition detection does not work") - # machine.succeed(": > /run/secrets/test_key") - # out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - # if "modifying secret" not in out: - # raise Exception("Modification detection does not work") + machine.succeed(": > /run/secrets/test_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + if "modifying secret" not in out: + raise Exception("Modification detection does not work") - # machine.succeed(": > /run/secrets/another_key") - # out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - # if "removing secret" not in out: - # raise Exception("Removal detection does not work") + machine.succeed(": > /run/secrets/another_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + if "removing secret" not in out: + raise Exception("Removal detection does not work") - # with subtest("dry activation"): - # machine.succeed("rm /run/secrets/test_key") - # machine.succeed(": > /run/secrets/another_key") - # out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") - # if "would add secret" not in out: - # raise Exception("Dry addition detection does not work") - # if "would remove secret" not in out: - # raise Exception("Dry removal detection does not work") + with subtest("dry activation"): + machine.succeed("rm /run/secrets/test_key") + machine.succeed(": > /run/secrets/another_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") + if "would add secret" not in out: + raise Exception("Dry addition detection does not work") + if "would remove secret" not in out: + raise Exception("Dry removal detection does not work") - # machine.fail("test -f /run/secrets/test_key") - # machine.succeed("test -f /run/secrets/another_key") + machine.fail("test -f /run/secrets/test_key") + machine.succeed("test -f /run/secrets/another_key") - # machine.succeed("/run/current-system/bin/switch-to-configuration test") - # machine.succeed("test -f /run/secrets/test_key") - # machine.succeed("rm /restarted /reloaded") - # machine.fail("test -f /run/secrets/another_key") + machine.succeed("/run/current-system/bin/switch-to-configuration test") + machine.succeed("test -f /run/secrets/test_key") + machine.succeed("rm /restarted /reloaded") + machine.fail("test -f /run/secrets/another_key") - # machine.succeed(": > /run/secrets/test_key") - # out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") - # if "would modify secret" not in out: - # raise Exception("Dry modification detection does not work") - # machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]") + machine.succeed(": > /run/secrets/test_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") + if "would modify secret" not in out: + raise Exception("Dry modification detection does not work") + 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 - # ''; - #} { - # inherit pkgs; - # inherit (pkgs) system; - #}; + machine.fail("test -f /restarted") # not done in dry mode + machine.fail("test -f /reloaded") # not done in dry mode + ''; + } { + inherit pkgs; + inherit (pkgs) system; + }; }