ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/

add key services (#305)

Signed-off-by: marshmallow <github@althaea.zone>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

marshmallow
autofix-ci[bot]
and committed by
GitHub
adc77d25 a6adc4a9

+154 -37
+1
CHANGELOG.md
··· 11 11 12 12 - `--ssh-accept-host` was added. 13 13 - `--on -` will now read additional apply targets from stdin. 14 + - `{key.name}-key.{path,service}` systemd units where added. 14 15 - `--path` now supports flakerefs (`github:foo/bar`, `git+file:///...`, 15 16 `gitlab:foo/bar`, etc). 16 17 - `--flake` is now an alias for `--path`.
+11
doc/guides/keys.md
··· 167 167 168 168 You can access the full absolute path of any key with 169 169 `config.deployment.keys.<name>.path` (auto-generated and read-only). 170 + 171 + Keys also have a `config.deployment.keys.<name>.service` property 172 + (auto-generated and read-only), which represent systemd services that you can 173 + `require`, telling systemd there is a hard-dependency on that key for the 174 + service to run. 175 + 170 176 Here's an example with the Tailscale service: 171 177 172 178 ```nix:line-numbers [hive.nix] ··· 186 192 deployment.keys."tailscale.key" = { 187 193 keyCommand = ["gpg" "--decrypt" "${./secrets/tailscale.key.gpg}"]; 188 194 }; 195 + 196 + # The service will not start unless the key exists. 197 + systemd.services.tailscaled-autoconnect.requires = [ 198 + config.deployment.keys."tailscale.key".service 199 + ]; 189 200 }; 190 201 } 191 202 ```
+1 -1
doc/options.nix
··· 7 7 let 8 8 eval = lib.evalModules { 9 9 modules = [ 10 - ../runtime/module.nix 10 + ../runtime/module/options.nix 11 11 { 12 12 options._module.args = lib.mkOption { 13 13 internal = true;
+1 -1
doc/package.nix
··· 13 13 let 14 14 eval = lib.evalModules { 15 15 modules = [ 16 - ../runtime/module.nix 16 + ../runtime/module/options.nix 17 17 { 18 18 options._module.args = lib.mkOption { 19 19 internal = true;
+1 -1
flake.nix
··· 41 41 systems = import systems; 42 42 43 43 flake = { 44 - nixosModules.default = import ./runtime/module.nix; 44 + nixosModules.default = import ./runtime/module; 45 45 makeHive = import ./runtime/makeHive.nix; 46 46 hydraJobs = 47 47 let
+1 -1
runtime/evaluate.nix
··· 6 6 nixosConfigurations ? { }, 7 7 }: 8 8 let 9 - module = import ./module.nix; 9 + module = import ./module; 10 10 11 11 mergedHive = { 12 12 meta = { };
+12 -22
runtime/module.nix runtime/module/options.nix
··· 4 4 { 5 5 lib, 6 6 name, 7 - config, 8 7 ... 9 8 }: 10 9 let ··· 115 114 path = lib.mkOption { 116 115 internal = true; 117 116 type = types.path; 118 - default = "${config.destDir}/${config.name}"; 117 + default = 118 + if lib.hasSuffix "/" config.destDir then 119 + "${config.destDir}${config.name}" 120 + else 121 + "${config.destDir}/${config.name}"; 122 + description = "Path that the key is deployed to."; 123 + }; 124 + service = lib.mkOption { 125 + internal = true; 126 + type = types.str; 127 + default = "${config.name}-key.service"; 128 + description = "Name of the systemd service that represents this key."; 119 129 }; 120 130 group = lib.mkOption { 121 131 type = types.str; ··· 181 191 destDir = "/etc/arbs/"; 182 192 }; 183 193 }; 184 - }; 185 - }; 186 - 187 - config = { 188 - deployment = { 189 - _keys = lib.mapAttrsToList ( 190 - _: value: 191 - value 192 - // { 193 - source = { 194 - # Attach type to internally tag serde enum 195 - t = builtins.replaceStrings [ "path" "string" "list" ] [ "Path" "String" "Command" ] ( 196 - builtins.typeOf value.source 197 - ); 198 - c = value.source; 199 - }; 200 - } 201 - ) config.deployment.keys; 202 - 203 - _hostPlatform = config.nixpkgs.hostPlatform.system; 204 194 }; 205 195 }; 206 196 }
+79
runtime/module/config.nix
··· 1 + # SPDX-License-Identifier: AGPL-3.0-or-later 2 + # Copyright 2024-2025 wire Contributors 3 + 4 + { 5 + pkgs, 6 + lib, 7 + config, 8 + ... 9 + }: 10 + { 11 + config = { 12 + systemd = { 13 + paths = lib.mapAttrs' ( 14 + _name: value: 15 + lib.nameValuePair "${value.name}-key" { 16 + description = "Monitor changes to ${value.path}. You should Require ${value.service} instead of this."; 17 + pathConfig = { 18 + PathExists = value.path; 19 + PathChanged = value.path; 20 + Unit = "${value.name}-key.service"; 21 + }; 22 + } 23 + ) config.deployment.keys; 24 + 25 + services = lib.mapAttrs' ( 26 + _name: value: 27 + lib.nameValuePair "${value.name}-key" { 28 + description = "Service that requires ${value.path}"; 29 + path = [ 30 + pkgs.inotify-tools 31 + pkgs.coreutils 32 + ]; 33 + script = '' 34 + MSG="Key ${value.path} exists." 35 + systemd-notify --ready --status="$MSG" 36 + 37 + echo "waiting to fail if the key is removed..." 38 + 39 + while inotifywait -e delete_self "${value.path}"; do 40 + MSG="Key ${value.path} no longer exists." 41 + 42 + systemd-notify --status="$MSG" 43 + echo $MSG 44 + 45 + exit 1 46 + done 47 + ''; 48 + unitConfig = { 49 + ConditionPathExists = value.path; 50 + }; 51 + serviceConfig = { 52 + Type = "simple"; 53 + Restart = "no"; 54 + NotifyAccess = "all"; 55 + RemainAfterExit = "yes"; 56 + }; 57 + } 58 + ) config.deployment.keys; 59 + }; 60 + 61 + deployment = { 62 + _keys = lib.mapAttrsToList ( 63 + _: value: 64 + value 65 + // { 66 + source = { 67 + # Attach type to internally tag serde enum 68 + t = builtins.replaceStrings [ "path" "string" "list" ] [ "Path" "String" "Command" ] ( 69 + builtins.typeOf value.source 70 + ); 71 + c = value.source; 72 + }; 73 + } 74 + ) config.deployment.keys; 75 + 76 + _hostPlatform = config.nixpkgs.hostPlatform.system; 77 + }; 78 + }; 79 + }
+6
runtime/module/default.nix
··· 1 + { 2 + imports = [ 3 + ./options.nix 4 + ./config.nix 5 + ]; 6 + }
+25 -8
tests/nix/suite/test_keys/default.nix
··· 14 14 deployer_so = collect_store_objects(deployer) 15 15 receiver_so = collect_store_objects(receiver) 16 16 17 - # build all nodes without any keys 17 + # build receiver with no keys 18 18 deployer.succeed(f"wire apply --no-progress --on receiver --path {TEST_DIR}/hive.nix --no-keys --ssh-accept-host -vvv >&2") 19 19 20 20 receiver.wait_for_unit("sshd.service") 21 21 22 22 # --no-keys should never push a key 23 - receiver.fail("test -f /run/keys/source_string") 24 - deployer.fail("test -f /run/keys/source_string") 23 + receiver.fail("test -f /run/keys/source_string_name") 24 + deployer.fail("test -f /run/keys/source_string_name") 25 + 26 + # key services are created 27 + receiver.succeed("systemctl cat source_string_name-key.service") 28 + 29 + _, is_failed = receiver.execute("systemctl is-failed source_string_name-key.service") 30 + assert is_failed == "inactive\n", f"source_string_name-key.service must be inactive before key exists ({is_failed})" 25 31 26 32 def test_keys(target, target_object, non_interactive): 27 33 if non_interactive: ··· 30 36 deployer.succeed(f"wire apply keys --on {target} --no-progress --path {TEST_DIR}/hive.nix --ssh-accept-host -vvv >&2") 31 37 32 38 keys = [ 33 - ("/run/keys/source_string", "hello_world_source", "root root 600"), 34 - ("/etc/keys/file", "hello_world_file", "root root 644"), 35 - ("/home/owner/some/deep/path/command", "hello_world_command", "owner owner 644"), 36 - ("/run/keys/environment", "string_from_environment", "root root 600"), 39 + ("/run/keys/source_string_name", "hello_world_source", "root root 600", "source_string_name"), 40 + ("/etc/keys/file", "hello_world_file", "root root 644", "file"), 41 + ("/home/owner/some/deep/path/command", "hello_world_command", "owner owner 644", "command"), 42 + ("/run/keys/environment", "string_from_environment", "root root 600", "environment"), 37 43 ] 38 44 39 - for path, value, permissions in keys: 45 + for path, value, permissions, name in keys: 40 46 # test existence & value 41 47 source_string = target_object.succeed(f"cat {path}") 42 48 assert value in source_string, f"{path} has correct contents ({target})" ··· 47 53 def perform_routine(target, target_object, non_interactive): 48 54 test_keys(target, target_object, non_interactive) 49 55 56 + # only check systemd units on receiver since deployer applys are one time only 57 + if target == "receiver": 58 + target_object.succeed("systemctl start source_string_name-key.path") 59 + target_object.succeed("systemctl start command-key.path") 60 + target_object.wait_for_unit("source_string_name-key.service") 61 + target_object.wait_for_unit("command-key.service") 62 + 50 63 # Mess with the keys to make sure that every push refreshes the permissions 51 64 target_object.succeed("echo 'incorrect_value' > /run/keys/source_string") 52 65 target_object.succeed("chown 600 /etc/keys/file") 53 66 # Test having a key that doesn't exist mixed with keys that do 54 67 target_object.succeed("rm /home/owner/some/deep/path/command") 68 + 69 + if target == "receiver": 70 + _, is_failed = target_object.execute("systemctl is-active command-key.service") 71 + assert is_failed == "failed\n", f"command-key.service is failed after deletion ({is_failed})" 55 72 56 73 # Test keys twice to ensure the operation is idempotent, 57 74 # especially around directory creation.
+2
tests/nix/suite/test_keys/hive.nix
··· 9 9 defaults = { 10 10 deployment.keys = { 11 11 source_string = { 12 + # key with different name to attr name 13 + name = "source_string_name"; 12 14 source = '' 13 15 hello_world_source 14 16 '';
+14 -3
wire/lib/src/test_support.rs
··· 2 2 // Copyright 2024-2025 wire Contributors 3 3 4 4 use std::{ 5 - fs, io, 5 + fs::{self, create_dir}, 6 + io, 6 7 path::Path, 7 8 process::Command, 8 9 sync::{Arc, Mutex}, ··· 26 27 27 28 let root = path.parent().unwrap().parent().unwrap().parent().unwrap(); 28 29 30 + create_dir(tmp_dir.as_ref().join("module/"))?; 31 + 29 32 fs::copy( 30 33 root.join(Path::new("runtime/evaluate.nix")), 31 34 tmp_dir.as_ref().join("evaluate.nix"), 32 35 )?; 33 36 fs::copy( 34 - root.join(Path::new("runtime/module.nix")), 35 - tmp_dir.as_ref().join("module.nix"), 37 + root.join(Path::new("runtime/module/config.nix")), 38 + tmp_dir.as_ref().join("module/config.nix"), 39 + )?; 40 + fs::copy( 41 + root.join(Path::new("runtime/module/options.nix")), 42 + tmp_dir.as_ref().join("module/options.nix"), 43 + )?; 44 + fs::copy( 45 + root.join(Path::new("runtime/module/default.nix")), 46 + tmp_dir.as_ref().join("module/default.nix"), 36 47 )?; 37 48 fs::copy( 38 49 root.join(Path::new("runtime/makeHive.nix")),