diff --git a/README.md b/README.md index 50456cc..0c8f2a4 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,14 @@ files: `systemd-vaultd.service`, `systemd-vaultd.socket`: make install ``` +## Known limitations + +systemd's LoadCredential option will not update credentials if a service is +reloaded. However systemd-vaultd called `systemd-vaultd-update-secrets` comes +with a helper program that can write secrets from the json file generated by +systemd-vaultd to a directory readable by the service. Checkout +`systemd-vaultd/nix/checks/systemd-vaultd-test.nix` for more details. + ## License Copyright (c) 2022 [Jörg Thalheim](https://github.com/mic92) and contributors. diff --git a/cmd/systemd-vaultd-update-secrets/main.go b/cmd/systemd-vaultd-update-secrets/main.go new file mode 100644 index 0000000..22ff597 --- /dev/null +++ b/cmd/systemd-vaultd-update-secrets/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path" + "syscall" + "time" +) + +const ( + systemdVaultdir = "/run/systemd-vaultd/secrets" +) + +func updateSecrets(credentialsDirectory, target string) error { + // get systemd service name from credentials directory + serviceName := path.Base(credentialsDirectory) + stat, err := os.Stat(credentialsDirectory) + if err != nil { + return fmt.Errorf("failed to stat %s: %w", credentialsDirectory, err) + } + // inherit the owner and group of the credentials directory + uid := stat.Sys().(*syscall.Stat_t).Uid + gid := stat.Sys().(*syscall.Stat_t).Gid + + jsonPath := path.Join(credentialsDirectory, fmt.Sprintf("%s.json", serviceName)) + var content []byte + for i := 0; i < 10; i++ { + content, err = os.ReadFile(jsonPath) + if err != nil { + if os.IsNotExist(err) { + // wait for the file to be created + fmt.Printf("waiting for %s to be created", jsonPath) + time.Sleep(1 * time.Second) + continue + } + return fmt.Errorf("failed to read vault json file %s: %w", serviceName, err) + } + break + } + var data map[string]interface{} + if err := json.Unmarshal(content, &data); err != nil { + return fmt.Errorf("failed to unmarshal json from %s: %w", jsonPath, err) + } + for key, value := range data { + targetPath := path.Join(target, key) + err := os.MkdirAll(path.Dir(targetPath), 0o700) + os.Chown(path.Dir(targetPath), int(uid), int(gid)) + + if err != nil { + return fmt.Errorf("failed to create directory %s: %w", path.Dir(targetPath), err) + } + os.WriteFile(targetPath, []byte(value.(string)), 0o400) + os.Chown(targetPath, int(uid), int(gid)) + } + + return nil +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: systemd-vaultd-update-secrets ") + os.Exit(1) + } + credentialsDirectory := os.Getenv("CREDENTIALS_DIRECTORY") + if credentialsDirectory == "" { + fmt.Println("CREDENTIALS_DIRECTORY environment variable must be set") + os.Exit(1) + } + + target := os.Args[1] + if err := updateSecrets(credentialsDirectory, target); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/flake.lock b/flake.lock index 79c2230..990817a 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1678379998, - "narHash": "sha256-TZdfNqftHhDuIFwBcN9MUThx5sQXCTeZk9je5byPKRw=", + "lastModified": 1680392223, + "narHash": "sha256-n3g7QFr85lDODKt250rkZj2IFS3i4/8HBU2yKHO3tqw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "c13d60b89adea3dc20704c045ec4d50dd964d447", + "rev": "dcc36e45d054d7bb554c9cdab69093debd91a0b5", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1679249765, - "narHash": "sha256-3sMdeiLrJQNvbVXzUuYUy/Va1Mpv733gyBruOypvmRo=", + "lastModified": 1680450296, + "narHash": "sha256-4SJqREZkmyQufQcudS+j0WqsHeRDE6jyFw6l6QcrMrE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "49966f2c7139521b1c84af4c76c72725e1eafd2e", + "rev": "cd9eead62d1cb6dd692cd87c209e0bc48f733669", "type": "github" }, "original": { @@ -50,11 +50,11 @@ ] }, "locked": { - "lastModified": 1678901796, - "narHash": "sha256-9myDjq948gHbiv16HnFQZaswQEpNodE/CuGCfDNnv/g=", + "lastModified": 1680218481, + "narHash": "sha256-VhkSVeKXbZtdaT41Kn3QuFE7OnXHM4UbMlqBez+tRL0=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "0f560a84215e79facd2833b20bfdc2033266f126", + "rev": "a082287718105c284475df18b882a76312dea0d0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f4b5a46..c50ab55 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,6 @@ , ... }: { packages.default = pkgs.callPackage ./default.nix { }; - packages.systemd = pkgs.callPackage ./nix/pkgs/systemd.nix { }; devShells.default = pkgs.mkShellNoCC { buildInputs = with pkgs; [ python3.pkgs.pytest @@ -35,7 +34,6 @@ go just config.treefmt.build.wrapper - config.packages.systemd ]; }; diff --git a/nix/checks/systemd-vaultd-test.nix b/nix/checks/systemd-vaultd-test.nix index ce8a050..b7dafe3 100644 --- a/nix/checks/systemd-vaultd-test.nix +++ b/nix/checks/systemd-vaultd-test.nix @@ -2,7 +2,6 @@ name = "systemd-vaultd"; nodes.server = { config - , pkgs , ... }: { imports = [ @@ -10,6 +9,9 @@ ../modules/systemd-vaultd.nix ./dev-vault-server.nix ]; + # speed up tests + virtualisation.cores = 4; + virtualisation.memorySize = 1024; systemd.services.service1 = { wantedBy = [ "multi-user.target" ]; @@ -33,17 +35,33 @@ }; }; + users.users.service2 = { + isSystemUser = true; + group = "service2"; + uid = 1000; + }; + users.groups.service2.gid = 1000; + systemd.services.service2 = { wantedBy = [ "multi-user.target" ]; + preStart = '' + cp -r $CREDENTIALS_DIRECTORY /run/service2/secrets + ''; script = '' set -x while true; do - cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2 + cat /run/service2/secrets/secret >&2 || : + cat /run/service2/secrets/secret > /tmp/service2 || : sleep 0.1 done ''; - serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true"; - serviceConfig.LoadCredential = [ "secret:/run/systemd-vaultd/sock" ]; + serviceConfig = { + ExecReload = "+${config.services.systemd-vaultd.package}/bin/systemd-vaultd-update-secrets /run/service2/secrets"; + User = "service2"; + Group = "service2"; + LoadCredential = [ "secret:/run/systemd-vaultd/sock" ]; + RuntimeDirectory = "service2"; + }; vault = { template = '' {{ with secret "secret/blocking-secret" }}{{ scratch.MapSet "secrets" "secret" .Data.data.foo }}{{ end }} @@ -77,32 +95,37 @@ machine.wait_for_open_port(8200) machine.wait_for_unit("setup-vault-agent-approle.service") - out = machine.wait_until_succeeds("cat /tmp/service1") - print(out) - assert out == "bar", f"{out} != bar" + out = machine.wait_until_succeeds("grep -q bar /tmp/service1") - out = machine.succeed("cat /tmp/service1-env") - print(out) - assert out == "bar", f"{out} != bar" + out = machine.succeed("grep -q bar /tmp/service1-env") - out = machine.succeed("systemctl status service2") + out = machine.succeed("systemctl status service2 || :") print(out) assert "(sd-mkdcreds)" in out, "service2 should be still blocked" machine.succeed("vault kv put secret/blocking-secret foo=bar") - out = machine.wait_until_succeeds("cat /tmp/service2") - print(out) - assert out == "bar", f"{out} != bar" + machine.wait_until_succeeds("grep -q bar /tmp/service2 >&2") - machine.succeed("vault kv put secret/blocking-secret foo=reload") + machine.succeed("umount /run/credentials/service2.service") machine.succeed("rm /run/systemd-vaultd/secrets/service2.service.json") + + machine.succeed("vault kv put secret/blocking-secret foo=reload") + machine.succeed("systemctl restart vault-agent-default") machine.wait_until_succeeds("cat /run/systemd-vaultd/secrets/service2.service.json >&2") - machine.succeed("systemctl reload service2") + machine.succeed("systemctl restart service2") machine.succeed("rm /tmp/service2") - out = machine.wait_until_succeeds("cat /tmp/service2") - print(out) - assert out == "reload", f"{out} != reload" + machine.wait_until_succeeds("grep -q reload /tmp/service2 >&2") + + # get uid and gid + out = machine.succeed("stat -c %u /run/service2/secrets/secret").strip() + assert out == "1000", "service2 should have access to secret file with uid 1000, got " + out + out = machine.succeed("stat -c %g /run/service2/secrets/secret").strip() + assert out == "1000", "service2 should have access to secret file with gid 1000, got " + out + + # get permissions in octal + out = machine.succeed("stat -c %a /run/service2/secrets/secret").strip() + assert out == "400", "service2 should have access to secret file with permissions 0400, got " + out ''; } diff --git a/nix/checks/unittests.nix b/nix/checks/unittests.nix index eeb14f8..199e246 100644 --- a/nix/checks/unittests.nix +++ b/nix/checks/unittests.nix @@ -3,11 +3,10 @@ , pkgs , lib , coreutils -, +, systemd }: let systemd-vaultd = pkgs.callPackage ../../default.nix { }; - systemd = pkgs.callPackage ../pkgs/systemd.nix { }; in writeShellScript "unittests" '' set -eu -o pipefail diff --git a/nix/modules/systemd-vaultd.nix b/nix/modules/systemd-vaultd.nix index fdc5589..aa8e7ef 100644 --- a/nix/modules/systemd-vaultd.nix +++ b/nix/modules/systemd-vaultd.nix @@ -1,4 +1,6 @@ { pkgs +, lib +, config , ... }: let @@ -8,27 +10,39 @@ in imports = [ ./vault-secrets.nix ]; + options = { + services.systemd-vaultd = { + package = lib.mkOption { + type = lib.types.package; + default = systemd-vaultd; + defaultText = "pkgs.systemd-vaultd"; + description = '' + The package to use for systemd-vaultd + ''; + }; + }; + }; - systemd.package = pkgs.callPackage ../pkgs/systemd.nix { }; - - systemd.sockets.systemd-vaultd = { - description = "systemd-vaultd socket"; - wantedBy = [ "sockets.target" ]; + config = { + systemd.sockets.systemd-vaultd = { + description = "systemd-vaultd socket"; + wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = "/run/systemd-vaultd/sock"; - SocketUser = "root"; - SocketMode = "0600"; + socketConfig = { + ListenStream = "/run/systemd-vaultd/sock"; + SocketUser = "root"; + SocketMode = "0600"; + }; }; - }; - systemd.services.systemd-vaultd = { - description = "systemd-vaultd daemon"; - requires = [ "systemd-vaultd.socket" ]; - after = [ "systemd-vaultd.socket" ]; - # Restarting can break services waiting for secrets - stopIfChanged = false; - serviceConfig = { - ExecStart = "${systemd-vaultd}/bin/systemd-vaultd"; + systemd.services.systemd-vaultd = { + description = "systemd-vaultd daemon"; + requires = [ "systemd-vaultd.socket" ]; + after = [ "systemd-vaultd.socket" ]; + # Restarting can break services waiting for secrets + stopIfChanged = false; + serviceConfig = { + ExecStart = "${config.services.systemd-vaultd.package}/bin/systemd-vaultd"; + }; }; }; } diff --git a/nix/pkgs/systemd.nix b/nix/pkgs/systemd.nix deleted file mode 100644 index 8a402fb..0000000 --- a/nix/pkgs/systemd.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ systemd -, fetchpatch -, -}: -systemd.overrideAttrs (old: { - patches = - old.patches - ++ [ - (fetchpatch { - url = "https://github.com/Mic92/systemd/commit/93a2921a81cab3be9b7eacab6b0095c96a0ae9e2.patch"; - sha256 = "sha256-7WlhMLE7sfD3Cxn6n6R1sUNzUOvas7XMyabi3bsq7jM="; - }) - ]; -})