diff --git a/integration-tests/apply/test-script.py b/integration-tests/apply/test-script.py index 24823d1..bdf3c5c 100644 --- a/integration-tests/apply/test-script.py +++ b/integration-tests/apply/test-script.py @@ -68,6 +68,15 @@ with subtest("Check that key files have correct permissions"): for path, permission in permissions.items(): node.succeed(f"if [[ \"{permission}\" != \"$(stat -c '%a %U %G' '{path}')\" ]]; then ls -lah '{path}'; exit 1; fi") +with subtest("Check that key services respond to key file changes"): + alpha.require_unit_state("key-text-key.service", "active") + + alpha.succeed("rm /run/keys/key-text") + alpha.wait_until_succeeds("systemctl --no-pager show key-text-key.service | grep ActiveState=inactive", timeout=10) + + alpha.succeed("touch /run/keys/key-text") + alpha.wait_for_unit("key-text-key.service") + with subtest("Check that we can correctly deploy to remaining nodes despite failures"): beta.systemctl("stop sshd") diff --git a/integration-tests/tools.nix b/integration-tests/tools.nix index 63eaafe..452c738 100644 --- a/integration-tests/tools.nix +++ b/integration-tests/tools.nix @@ -62,6 +62,7 @@ let environment.systemPackages = with pkgs; [ git # for git flake tests + inotifyTools # for key services build # HACK: copy stderr to both stdout and stderr # (the test framework only captures stdout, and only stderr appears on screen during the build) diff --git a/manual/src/features/keys.md b/manual/src/features/keys.md index 6efe4c8..be2b5eb 100644 --- a/manual/src/features/keys.md +++ b/manual/src/features/keys.md @@ -28,3 +28,8 @@ For example, to deploy DNS-01 credentials for use with `security.acme`: Take note that if you use the default path (`/run/keys`), the secret files are only stored in-memory and will not survive reboots. To upload your secrets without performing a full deployment, use `colmena upload-keys`. + +## Key Services + +For each secret file deployed using `deployment.keys`, a systemd service with the name of `${name}-key.service` is created (`acme-credentials.secret-key.service` for the example above). +This unit is only active when the corresponding file is present, allowing you to set up dependencies for services requiring secret files to function. diff --git a/src/nix/hive/eval.nix b/src/nix/hive/eval.nix index caf1cb1..1f33a24 100644 --- a/src/nix/hive/eval.nix +++ b/src/nix/hive/eval.nix @@ -467,6 +467,47 @@ let in { system.activationScripts.colmena-chown-keys = lib.mkIf (length commands != 0) script; }; + + # Create "${name}-key" services for NixOps compatibility + # + # This is built as part of the system profile. + # We must be careful not to access `text` / `keyCommand` / `keyFile` here + # + # Sadly, path units don't automatically deactivate the bound units when + # the key files are deleted, so we use inotifywait in the services' scripts. + # + # + keyServiceModule = { pkgs, lib, config, ... }: { + systemd.paths = lib.mapAttrs' (name: val: { + name = "${name}-key"; + value = { + wantedBy = [ "paths.target" ]; + pathConfig = { + PathExists = val.path; + }; + }; + }) config.deployment.keys; + + systemd.services = lib.mapAttrs' (name: val: { + name = "${name}-key"; + value = { + bindsTo = [ "${name}-key.path" ]; + serviceConfig = { + Restart = "on-failure"; + }; + path = [ pkgs.inotifyTools ]; + script = '' + if [[ ! -e "${val.path}" ]]; then + >&2 echo "${val.path} does not exist" + exit 0 + fi + + inotifywait -qq -e delete_self "${val.path}" + >&2 echo "${val.path} disappeared" + ''; + }; + }) config.deployment.keys; + }; in evalConfig { inherit (npkgs) system; @@ -474,6 +515,7 @@ let assertionModule nixpkgsModule keyChownModule + keyServiceModule deploymentOptions hive.defaults ] ++ configs;