From 7fa8deb568d26902b795915604b6b3724efe04ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 28 Oct 2022 16:47:57 +0200 Subject: [PATCH] add support for environment variables --- flake.nix | 1 - main.go | 85 +++++++++++---- nix/checks/nixos-test.nix | 159 +---------------------------- nix/checks/systemd-vaultd-test.nix | 116 +++++++++++++++++++++ nix/checks/vault-agent-test.nix | 48 +++++++++ nix/modules/vault-secrets.nix | 88 ++++++++++++++-- secrets.go | 5 + watcher.go | 23 ++++- 8 files changed, 331 insertions(+), 194 deletions(-) create mode 100644 nix/checks/systemd-vaultd-test.nix create mode 100644 nix/checks/vault-agent-test.nix diff --git a/flake.nix b/flake.nix index fb0940c..8ad1ad1 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,6 @@ checks = let nixosTests = pkgs.callPackages ./nix/checks/nixos-test.nix { makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); - inherit (self.nixosModules) vaultAgent systemdVaultd; }; in { treefmt = pkgs.callPackage ./nix/checks/treefmt.nix {}; diff --git a/main.go b/main.go index 7fc7a2e..20b23fc 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,22 @@ func parseCredentialsAddr(addr string) (*string, *string, error) { return &fields[2], &fields[3], nil } -func (s *server) serveConnection(conn *net.UnixConn) { +func (s *server) queueInotifyRequest(conn *net.UnixConn, filename string, key string) error { + log.Printf("Block start until %s appears", filename) + fd, err := connFd(conn) + if err != nil { + // connection was closed while we trying to wait + return err + } + if err := s.epollWatch(fd); err != nil { + log.Printf("Cannot get setup epoll for unix socket: %s", err) + return err + } + s.inotifyRequests <- inotifyRequest{filename: filename, key: key, conn: conn} + return nil +} + +func (s *server) serveServiceEnvironment(conn *net.UnixConn, unit string, secret string) { shouldClose := true defer func() { if shouldClose { @@ -87,41 +102,69 @@ func (s *server) serveConnection(conn *net.UnixConn) { } }() - addr := conn.RemoteAddr().String() - unit, secret, err := parseCredentialsAddr(addr) - if err != nil { - log.Printf("Received connection but remote unix address seems to be not from systemd: %v", err) + log.Printf("Systemd requested environment file for %s from %s", secret, unit) + secretPath := filepath.Join(s.SecretDir, secret) + + f, err := os.Open(secretPath) + if errors.Is(err, os.ErrNotExist) { + if s.queueInotifyRequest(conn, secret, secret) == nil { + shouldClose = false + } + return + } else if err != nil { + log.Printf("Cannot open environment file %s/%s: %v", unit, secret, err) return } - log.Printf("Systemd requested secret for %s/%s", *unit, *secret) - secretName := *unit + ".json" + defer f.Close() + if _, err = io.Copy(conn, f); err != nil { + log.Printf("Failed to send environment file: %v", err) + } +} + +func (s *server) serveServiceSecrets(conn *net.UnixConn, unit string, secret string) { + shouldClose := true + defer func() { + if shouldClose { + conn.Close() + } + }() + + log.Printf("Systemd requested secret for %s/%s", unit, secret) + secretName := unit + ".json" secretPath := filepath.Join(s.SecretDir, secretName) secretMap, err := parseServiceSecrets(secretPath) if errors.Is(err, os.ErrNotExist) { - log.Printf("Block start until %s appears", secretPath) - shouldClose = false - fd, err := connFd(conn) - if err != nil { - // connection was closed while we trying to wait - return - } - if err := s.epollWatch(fd); err != nil { - log.Printf("Cannot get setup epoll for unix socket: %s", err) - return + if s.queueInotifyRequest(conn, secretName, secret) == nil { + shouldClose = false } - s.inotifyRequests <- inotifyRequest{filename: secretName, key: *secret, conn: conn} return } else if err != nil { - log.Printf("Cannot process secret %s/%s: %v", *unit, *secret, err) + log.Printf("Cannot process secret %s/%s: %v", unit, secret, err) return } - val, ok := secretMap[*secret] + val, ok := secretMap[secret] if ok { if _, err = io.WriteString(conn, fmt.Sprint(val)); err != nil { log.Printf("Failed to send secret: %v", err) } } else { - log.Printf("Secret map at %s has no value for key %s", secretPath, *secret) + log.Printf("Secret map at %s has no value for key %s", secretPath, secret) + } +} + +func (s *server) serveConnection(conn *net.UnixConn) { + addr := conn.RemoteAddr().String() + unit, secret, err := parseCredentialsAddr(addr) + if err != nil { + conn.Close() + log.Printf("Received connection but remote unix address seems to be not from systemd: %v", err) + return + } + + if isEnvironmentFile(*secret) { + s.serveServiceEnvironment(conn, *unit, *secret) + } else { + s.serveServiceSecrets(conn, *unit, *secret) } } diff --git a/nix/checks/nixos-test.nix b/nix/checks/nixos-test.nix index 520f6fb..6471740 100644 --- a/nix/checks/nixos-test.nix +++ b/nix/checks/nixos-test.nix @@ -1,8 +1,6 @@ { makeTest ? import , pkgs ? (import {}), - vaultAgent ? ../modules/vault-agent.nix, - systemdVaultd ? ../modules/systemd-vaultd.nix, }: let makeTest' = args: makeTest args { @@ -10,161 +8,8 @@ inherit (pkgs) system; }; in { - vault-agent = makeTest' { - name = "vault-agent"; - nodes.server = { - config, - pkgs, - ... - }: { - imports = [ - vaultAgent - ./dev-vault-server.nix - ]; - - services.vault.agents.test.settings = { - vault = { - address = "http://localhost:8200"; - }; - template = { - contents = ''{{ with secret "secret/my-secret" }}{{ .Data.data.foo }}{{ end }}''; - destination = "/run/render.txt"; - }; - - auto_auth = { - method = [ - { - type = "approle"; - config = { - role_id_file_path = "/tmp/roleID"; - secret_id_file_path = "/tmp/secretID"; - remove_secret_id_file_after_reading = false; - }; - } - ]; - }; - }; - }; - testScript = '' - start_all() - machine.wait_for_unit("multi-user.target") - machine.wait_for_unit("vault.service") - machine.wait_for_open_port(8200) - machine.wait_for_unit("setup-vault-agent-approle.service") - - # It should be able to write our template - out = machine.wait_until_succeeds("cat /run/render.txt") - print(out) - assert out == "bar" - ''; - }; - systemd-vaultd = makeTest' { - name = "systemd-vaultd"; - nodes.server = { - config, - pkgs, - ... - }: { - imports = [ - vaultAgent - systemdVaultd - ./dev-vault-server.nix - ]; - - systemd.services.service1 = { - wantedBy = ["multi-user.target"]; - script = '' - cat $CREDENTIALS_DIRECTORY/foo > /tmp/service1 - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - vault = { - template = '' - {{ with secret "secret/my-secret" }}{{ .Data.data | toJSON }}{{ end }} - ''; - secrets.foo = {}; - }; - }; - - systemd.services.service2 = { - wantedBy = ["multi-user.target"]; - script = '' - cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2 - sleep infinity - ''; - reload = '' - cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2-reload - ''; - serviceConfig.LoadCredential = ["secret:/run/systemd-vaultd/sock"]; - vault = { - template = '' - {{ with secret "secret/blocking-secret" }}{{ scratch.MapSet "secrets" "secret" .Data.data.foo }}{{ end }} - {{ scratch.Get "secrets" | explodeMap | toJSON }} - ''; - secrets.secret = {}; - }; - }; - - systemd.package = pkgs.systemd.overrideAttrs (old: { - patches = - old.patches - ++ [ - (pkgs.fetchpatch { - url = "https://github.com/Mic92/systemd/commit/93a2921a81cab3be9b7eacab6b0095c96a0ae9e2.patch"; - sha256 = "sha256-7WlhMLE7sfD3Cxn6n6R1sUNzUOvas7XMyabi3bsq7jM="; - }) - ]; - }); - - services.vault.agents.default.settings = { - vault = { - address = "http://localhost:8200"; - }; - auto_auth = { - method = [ - { - type = "approle"; - config = { - role_id_file_path = "/tmp/roleID"; - secret_id_file_path = "/tmp/secretID"; - remove_secret_id_file_after_reading = false; - }; - } - ]; - }; - }; - }; - testScript = '' - start_all() - machine.wait_for_unit("vault.service") - machine.wait_for_open_port(8200) - machine.wait_for_unit("setup-vault-agent-approle.service") - machine.wait_for_unit("service1.service") - out = machine.succeed("cat /tmp/service1") - print(out) - assert out == "bar", f"{out} != bar" - - 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.succeed("vault kv put secret/blocking-secret foo=reload") - machine.succeed("rm /run/systemd-vaultd/secrets/service2.service.json") - 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") - out = machine.wait_until_succeeds("cat /tmp/service2-reload") - print(out) - assert out == "reload", f"{out} != reload" - ''; - }; + vault-agent = makeTest' (import ./vault-agent-test.nix); + systemd-vaultd = makeTest' (import ./systemd-vaultd-test.nix); unittests = makeTest' { name = "unittests"; nodes.server = {}; diff --git a/nix/checks/systemd-vaultd-test.nix b/nix/checks/systemd-vaultd-test.nix new file mode 100644 index 0000000..0f4f758 --- /dev/null +++ b/nix/checks/systemd-vaultd-test.nix @@ -0,0 +1,116 @@ +{ + name = "systemd-vaultd"; + nodes.server = { + config, + pkgs, + ... + }: { + imports = [ + ../modules/vault-agent.nix + ../modules/systemd-vaultd.nix + ./dev-vault-server.nix + ]; + + systemd.services.service1 = { + wantedBy = ["multi-user.target"]; + script = '' + cat $CREDENTIALS_DIRECTORY/foo > /tmp/service1 + echo -n "$SECRET_ENV" > /tmp/service1-env + ''; + #serviceConfig = { + # EnvironmentFile = [ "/run/systemd-vaultd/service1.service.EnvironmentFile" ]; + #}; + vault = { + template = '' + {{ with secret "secret/my-secret" }}{{ .Data.data | toJSON }}{{ end }} + ''; + secrets.foo = {}; + environmentTemplate = '' + {{ with secret "secret/my-secret" }} + SECRET_ENV={{ .Data.data.foo }} + {{ end }} + ''; + }; + }; + + systemd.services.service2 = { + wantedBy = ["multi-user.target"]; + script = '' + cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2 + sleep infinity + ''; + reload = '' + cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2-reload + ''; + serviceConfig.LoadCredential = ["secret:/run/systemd-vaultd/sock"]; + vault = { + template = '' + {{ with secret "secret/blocking-secret" }}{{ scratch.MapSet "secrets" "secret" .Data.data.foo }}{{ end }} + {{ scratch.Get "secrets" | explodeMap | toJSON }} + ''; + secrets.secret = {}; + }; + }; + + systemd.package = pkgs.systemd.overrideAttrs (old: { + patches = + old.patches + ++ [ + (pkgs.fetchpatch { + url = "https://github.com/Mic92/systemd/commit/93a2921a81cab3be9b7eacab6b0095c96a0ae9e2.patch"; + sha256 = "sha256-7WlhMLE7sfD3Cxn6n6R1sUNzUOvas7XMyabi3bsq7jM="; + }) + ]; + }); + + services.vault.agents.default.settings = { + vault = { + address = "http://localhost:8200"; + }; + auto_auth = { + method = [ + { + type = "approle"; + config = { + role_id_file_path = "/tmp/roleID"; + secret_id_file_path = "/tmp/secretID"; + remove_secret_id_file_after_reading = false; + }; + } + ]; + }; + }; + }; + testScript = '' + start_all() + machine.wait_for_unit("vault.service") + 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.succeed("cat /tmp/service1-env") + print(out) + assert out == "bar", f"{out} != bar" + + 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.succeed("vault kv put secret/blocking-secret foo=reload") + machine.succeed("rm /run/systemd-vaultd/secrets/service2.service.json") + 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") + out = machine.wait_until_succeeds("cat /tmp/service2-reload") + print(out) + assert out == "reload", f"{out} != reload" + ''; +} diff --git a/nix/checks/vault-agent-test.nix b/nix/checks/vault-agent-test.nix new file mode 100644 index 0000000..39fec55 --- /dev/null +++ b/nix/checks/vault-agent-test.nix @@ -0,0 +1,48 @@ +{ + name = "vault-agent"; + nodes.server = { + config, + pkgs, + ... + }: { + imports = [ + ./dev-vault-server.nix + ../modules/vault-agent.nix + ]; + + services.vault.agents.test.settings = { + vault = { + address = "http://localhost:8200"; + }; + template = { + contents = ''{{ with secret "secret/my-secret" }}{{ .Data.data.foo }}{{ end }}''; + destination = "/run/render.txt"; + }; + + auto_auth = { + method = [ + { + type = "approle"; + config = { + role_id_file_path = "/tmp/roleID"; + secret_id_file_path = "/tmp/secretID"; + remove_secret_id_file_after_reading = false; + }; + } + ]; + }; + }; + }; + testScript = '' + start_all() + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("vault.service") + machine.wait_for_open_port(8200) + machine.wait_for_unit("setup-vault-agent-approle.service") + + # It should be able to write our template + out = machine.wait_until_succeeds("cat /run/render.txt") + print(out) + assert out == "bar" + ''; +} diff --git a/nix/modules/vault-secrets.nix b/nix/modules/vault-secrets.nix index 88408b3..8b11fb2 100644 --- a/nix/modules/vault-secrets.nix +++ b/nix/modules/vault-secrets.nix @@ -29,7 +29,7 @@ services = config.systemd.services; - getTemplate = serviceName: vaultConfig: + getSecretTemplate = serviceName: vaultConfig: { contents = vaultConfig.template; destination = "/run/systemd-vaultd/secrets/${serviceName}.service.json"; @@ -43,15 +43,35 @@ } ${lib.escapeShellArg "${serviceName}.service"}"; }; + getEnvironmentTemplate = serviceName: vaultConfig: + { + contents = vaultConfig.environmentTemplate; + destination = "/run/systemd-vaultd/secrets/${serviceName}.service.EnvironmentFile"; + perms = "0400"; + } + // lib.optionalAttrs (vaultConfig.changeAction != null) { + command = "systemctl ${ + if vaultConfig.changeAction == "restart" + then "try-restart" + else "try-reload-or-restart" + } ${lib.escapeShellArg "${serviceName}.service"}"; + }; + vaultTemplates = config: - lib.mapAttrsToList - (serviceName: service: - getTemplate serviceName services.${serviceName}.vault) - (lib.filterAttrs (n: v: v.vault.secrets != {} && v.vault.agent == config._module.args.name) services); + (lib.mapAttrsToList + (serviceName: service: + getSecretTemplate serviceName services.${serviceName}.vault) + (lib.filterAttrs (n: v: v.vault.secrets != {} && v.vault.agent == config._module.args.name) services)) + ++ (lib.mapAttrsToList + (serviceName: service: + getEnvironmentTemplate serviceName services.${serviceName}.vault) + (lib.filterAttrs (n: v: v.vault.environmentTemplate != null && v.vault.agent == config._module.args.name) services)); in { options = { systemd.services = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({config, ...}: { + type = lib.types.attrsOf (lib.types.submodule ({config, ...}: let + serviceName = config._module.args.name; + in { options.vault = { changeAction = lib.mkOption { description = '' @@ -66,9 +86,17 @@ in { }; template = lib.mkOption { - type = lib.types.str; + type = lib.types.lines; + description = '' + The vault agent template to use for secrets + ''; + }; + + environmentTemplate = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; description = '' - The vault agent template to use for this secret + The vault agent template to use for environment file ''; }; @@ -81,7 +109,7 @@ in { }; secrets = lib.mkOption { - type = lib.types.attrsOf (secretType config._module.args.name); + type = lib.types.attrsOf (secretType serviceName); default = {}; description = "List of secrets to load from vault agent template"; example = { @@ -89,7 +117,17 @@ in { }; }; }; - config.serviceConfig.LoadCredential = lib.mapAttrsToList (_: config: "${config.name}:/run/systemd-vaultd/sock") config.vault.secrets; + config = let + mkIfHasEnv = lib.mkIf (config.vault.environmentTemplate != null); + in { + after = mkIfHasEnv ["${serviceName}-envfile.service"]; + bindsTo = mkIfHasEnv ["${serviceName}-envfile.service"]; + + serviceConfig = { + LoadCredential = lib.mapAttrsToList (_: config: "${config.name}:/run/systemd-vaultd/sock") config.vault.secrets; + EnvironmentFile = mkIfHasEnv ["/run/systemd-vaultd/secrets/${serviceName}.service.EnvironmentFile"]; + }; + }; })); }; @@ -99,4 +137,34 @@ in { })); }; }; + + config = { + # we cannot use `systemd.services` here since this would create infinite recursion + systemd.packages = let + servicesWithEnv = builtins.attrNames (lib.filterAttrs (n: v: v.vault.environmentTemplate != null) services); + in [ + (pkgs.runCommand "env-services" {} + ('' + mkdir -p $out/lib/systemd/system + '' + + (lib.concatMapStringsSep "\n" (service: '' + cat > $out/lib/systemd/system/${service}-envfile.service <