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;
+ };
}