Merge pull request #13 from numtide/ci

add support for environment variables
main
Jörg Thalheim 2 years ago committed by GitHub
commit c19c8c1b24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -28,7 +28,6 @@
checks = let checks = let
nixosTests = pkgs.callPackages ./nix/checks/nixos-test.nix { nixosTests = pkgs.callPackages ./nix/checks/nixos-test.nix {
makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix");
inherit (self.nixosModules) vaultAgent systemdVaultd;
}; };
in { in {
treefmt = pkgs.callPackage ./nix/checks/treefmt.nix {}; treefmt = pkgs.callPackage ./nix/checks/treefmt.nix {};

@ -79,7 +79,22 @@ func parseCredentialsAddr(addr string) (*string, *string, error) {
return &fields[2], &fields[3], nil 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 shouldClose := true
defer func() { defer func() {
if shouldClose { if shouldClose {
@ -87,41 +102,69 @@ func (s *server) serveConnection(conn *net.UnixConn) {
} }
}() }()
addr := conn.RemoteAddr().String() log.Printf("Systemd requested environment file for %s from %s", secret, unit)
unit, secret, err := parseCredentialsAddr(addr) secretPath := filepath.Join(s.SecretDir, secret)
if err != nil {
log.Printf("Received connection but remote unix address seems to be not from systemd: %v", err) 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 return
} }
log.Printf("Systemd requested secret for %s/%s", *unit, *secret) defer f.Close()
secretName := *unit + ".json" 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) secretPath := filepath.Join(s.SecretDir, secretName)
secretMap, err := parseServiceSecrets(secretPath) secretMap, err := parseServiceSecrets(secretPath)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
log.Printf("Block start until %s appears", secretPath) if s.queueInotifyRequest(conn, secretName, secret) == nil {
shouldClose = false 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
} }
s.inotifyRequests <- inotifyRequest{filename: secretName, key: *secret, conn: conn}
return return
} else if err != nil { } 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 return
} }
val, ok := secretMap[*secret] val, ok := secretMap[secret]
if ok { if ok {
if _, err = io.WriteString(conn, fmt.Sprint(val)); err != nil { if _, err = io.WriteString(conn, fmt.Sprint(val)); err != nil {
log.Printf("Failed to send secret: %v", err) log.Printf("Failed to send secret: %v", err)
} }
} else { } 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)
} }
} }

@ -1,8 +1,6 @@
{ {
makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>, makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>,
pkgs ? (import <nixpkgs> {}), pkgs ? (import <nixpkgs> {}),
vaultAgent ? ../modules/vault-agent.nix,
systemdVaultd ? ../modules/systemd-vaultd.nix,
}: let }: let
makeTest' = args: makeTest' = args:
makeTest args { makeTest args {
@ -10,161 +8,8 @@
inherit (pkgs) system; inherit (pkgs) system;
}; };
in { in {
vault-agent = makeTest' { vault-agent = makeTest' (import ./vault-agent-test.nix);
name = "vault-agent"; systemd-vaultd = makeTest' (import ./systemd-vaultd-test.nix);
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"
'';
};
unittests = makeTest' { unittests = makeTest' {
name = "unittests"; name = "unittests";
nodes.server = {}; nodes.server = {};

@ -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"
'';
}

@ -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"
'';
}

@ -29,7 +29,7 @@
services = config.systemd.services; services = config.systemd.services;
getTemplate = serviceName: vaultConfig: getSecretTemplate = serviceName: vaultConfig:
{ {
contents = vaultConfig.template; contents = vaultConfig.template;
destination = "/run/systemd-vaultd/secrets/${serviceName}.service.json"; destination = "/run/systemd-vaultd/secrets/${serviceName}.service.json";
@ -43,15 +43,35 @@
} ${lib.escapeShellArg "${serviceName}.service"}"; } ${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: vaultTemplates = config:
lib.mapAttrsToList (lib.mapAttrsToList
(serviceName: service: (serviceName: service:
getTemplate serviceName services.${serviceName}.vault) getSecretTemplate serviceName services.${serviceName}.vault)
(lib.filterAttrs (n: v: v.vault.secrets != {} && v.vault.agent == config._module.args.name) services); (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 { in {
options = { options = {
systemd.services = lib.mkOption { 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 = { options.vault = {
changeAction = lib.mkOption { changeAction = lib.mkOption {
description = '' description = ''
@ -66,9 +86,17 @@ in {
}; };
template = lib.mkOption { 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 = '' 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 { secrets = lib.mkOption {
type = lib.types.attrsOf (secretType config._module.args.name); type = lib.types.attrsOf (secretType serviceName);
default = {}; default = {};
description = "List of secrets to load from vault agent template"; description = "List of secrets to load from vault agent template";
example = { 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 <<EOF
[Unit]
Before=${service}.service
BindsTo=${service}.service
StopPropagatedFrom=${service}.service
[Service]
Type=oneshot
ExecStart=${pkgs.coreutils}/bin/true
RemainAfterExit=true
LoadCredential=${service}.service.EnvironmentFile:/run/systemd-vaultd/sock
[Install]
WantedBy=${service}.service
EOF
'')
servicesWithEnv)))
];
};
} }

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
) )
func parseServiceSecrets(path string) (map[string]interface{}, error) { func parseServiceSecrets(path string) (map[string]interface{}, error) {
@ -18,3 +19,7 @@ func parseServiceSecrets(path string) (map[string]interface{}, error) {
} }
return data, nil return data, nil
} }
func isEnvironmentFile(secret string) bool {
return strings.HasSuffix(secret, ".service.EnvironmentFile")
}

@ -119,10 +119,23 @@ func (s *server) watch(inotifyFd int) {
continue continue
} }
delete(connsForPath, fname) delete(connsForPath, fname)
secretMap, err := parseServiceSecrets(filepath.Join(s.SecretDir, fname))
if err != nil { var secretMap map[string]interface{}
log.Printf("Failed to process service file: %v", err) var err error
continue
if isEnvironmentFile(fname) {
content, err := os.ReadFile(filepath.Join(s.SecretDir, fname))
if err != nil {
log.Printf("Failed to process service file: %v", err)
continue
}
secretMap = map[string]interface{}{fname: string(content)}
} else {
secretMap, err = parseServiceSecrets(filepath.Join(s.SecretDir, fname))
if err != nil {
log.Printf("Failed to process service file: %v", err)
continue
}
} }
for _, conn := range conns { for _, conn := range conns {
@ -131,7 +144,7 @@ func (s *server) watch(inotifyFd int) {
if err == nil { if err == nil {
val, ok := secretMap[conn.key] val, ok := secretMap[conn.key]
if !ok { if !ok {
log.Printf("Secret map % has no value for key %s", fname, conn.key) log.Printf("Secret map %s has no value for key %s", fname, conn.key)
continue continue
} }
_, err = io.WriteString(conn.connection, fmt.Sprint(val)) _, err = io.WriteString(conn.connection, fmt.Sprint(val))

Loading…
Cancel
Save