enhance deterministic password support

This commit is contained in:
James Andariese 2022-10-07 21:10:15 -05:00
parent 464d7b2497
commit fc8e9dde60

View File

@ -1,18 +1,68 @@
# generate passwords using hashes and a root key
#
# uses util-linux uuidgen as a KDF with custom namespaces to generate passwords across a network.
#
#
{pkgs, lib, config, ...}: {pkgs, lib, config, ...}:
with lib; with lib;
with builtins; with builtins;
with import <cascade/functions>; with import <cascade/functions>;
{ {
options = { options = with types; {
environment.deterministic-passwords = mkOption { environment.deterministic-passwords.namespaces = mkOption {
type = listOf str;
default = [];
description = ''
Namespaces to be made available on the host.
For maximum security, this should _not_ contain anything unused on the host.
Any namespaces used by the Nix-based deterministic passwords module will be
included automatically. This can usually be left as default.
'';
};
system.build.deterministic-passwords.generateNamespaces = mkOption {
type = str;
description = ''
command which expects a $ROOT variable and which produce namespace UUID secrets.
defaults to an automatically generated version.
'';
};
environment.deterministic-passwords.namespace-folder = mkOption {
type = str;
default = "/etc/cascade/deterministic-passwords/namespaces";
description = ''
Folder where namespace secrets are held.
'';
};
environment.deterministic-passwords.secrets = mkOption {
default = {};
description = ''
Secrets to be placed on disk.
'';
type = with types; attrsOf (submodule ({config, name, ...}@args: { type = with types; attrsOf (submodule ({config, name, ...}@args: {
options = { options = {
enabled = mkEnableOption "password writer for ${name}" // {default = true;}; enabled = mkEnableOption "password writer for ${name}" // {default = true;};
destination = mkOption { namespace = mkOption {
type = str; type = str;
description = ''Where to save the secret''; description = ''
Namespace in which to create password.
This is a namespace-prefixed, colon-delimited, hostname-compatible
name which usually will represent the scope of members which should
know the secret. For hosts, the namespace prefix is "host:". I.e.,
this will be "host:chimecho" for secrets which should only be known
to the host named "chimecho". It might instead be "service:vault"
for secrets which should be known to a cluster serving vault, such
as the database password.
Defaults to the hostname if the string is empty or not set.
'';
default = "";
}; };
group = mkOption { group = mkOption {
@ -33,11 +83,46 @@ options = {
description = "Target file mode (octal)"; description = "Target file mode (octal)";
}; };
action = mkOption { destination = mkOption {
default = []; type = str;
type = listOf str; description = ''Where to save the secret'';
description = "Action to perform on the remote host when the secret changes";
}; };
writer = mkOption {
default = ''
echo "$secret" > "$destination"
'';
type = str;
#merge = xl: join-string "\n" (map (cmd: "bash -c ${shq cmd}") xl);
description = ''
Actions to perform on the remote host to create the new secret on disk.
$secret is the secret.
$destination is the path to the destination file.
'';
};
before = mkOption {
default = [];
type = listOf str;
description = ''
systemd units depending on this password being in place should be
listed here.
this translates directly to systemd.services."deterministic-passwords-''${NAME}".before
systemd.services."deterministic-passwords-''${NAME}".requiredBy
'';
};
action = mkOption {
default = "";
type = str;
#merge = xl: join-string "\n" (map (cmd: "bash -c ${shq cmd}") xl);
description = ''
actions to perform after the secret has been written. these actions MAY occur after the
units in "before" are allowed to start.
'';
};
}; };
})); }));
}; };
@ -45,39 +130,83 @@ options = {
config = config =
let shq = escapeShellArg; let shq = escapeShellArg;
makeUpdateScript = n: {enabled, destination, group, user, mode, action}@opts: '' ns = {namespace,...}@opts: if namespace == "" then "host:${config.networking.hostName}" else namespace;
( makeUpdateScript = n: {enabled, writer, destination, group, user, mode, action, ...}@opts: ''
umask 0777 ( set -e; export PATH="$PATH:/run/current-system/sw/sbin:/run/current-system/sw/bin"
ACTION=${shq (join-string "\n" action)} # mkdir here instead of in a global activation script for simplicity.
${pkgs.util-linux}/bin/uuidgen -s -n $(cat /etc/cascade/host-secret.uuid) -N ${shq n} > ${shq destination} mkdir -p ${shq config.environment.deterministic-passwords.namespace-folder}
chown ${shq user}:${shq group} ${shq destination}
chmod ${shq mode} ${shq destination} if [ -e /etc/cascade/deterministic-passwords/namespaces/${ns opts} ];then
${pkgs.bash}/bin/bash -c "$ACTION" umask 0277
mkdir -p "$(dirname ${shq destination})"
(
export secret="$( ${pkgs.util-linux}/bin/uuidgen -s -n $(cat /etc/cascade/deterministic-passwords/namespaces/${ns opts}) -N ${shq n} )"
export destination=${shq destination}
bash -c ${shq writer}
)
chown ${shq user}:${shq group} ${shq destination}
chmod ${shq mode} ${shq destination}
fi
) )
''; '';
secretToPath = n: {enabled, destination, group, user, mode, action}@opts: mkIf (enabled) { secretToPath = n: {enabled, destination, group, user, mode, action, ...}@opts: mkIf (enabled) {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
pathConfig = { pathConfig = {
PathChanged = "/etc/cascade/host-secret.uuid"; PathChanged = "/etc/cascade/deterministic-passwords/namespaces/${ns opts}";
}; };
after = [ "network-online.target" ];
}; };
secretToService = n: {enabled, destination, group, user, mode, action}@opts: mkIf (enabled) { secretToService = n: {enabled, before, ...}@opts: mkIf (enabled) {
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
requiredBy = before;
before = before;
script = makeUpdateScript n opts; script = makeUpdateScript n opts;
}; };
secretToActivationScript = n: {enabled, destination, group, user, mode, action}@opts: { secretToAfterPath = n: {enabled, ...}@opts: mkIf (enabled) {
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathChanged = "/etc/cascade/deterministic-passwords/namespaces/${ns opts}";
};
};
secretToAfterService = n: {action, before, enabled, ...}@opts: mkIf (enabled) {
serviceConfig.Type = "oneshot";
after = [ "deterministic-password-setup-${n}.service" ];
script = ''
set -e; export PATH="$PATH:/run/current-system/sw/sbin:/run/current-system/sw/bin"
${action}
'';
};
secretToActivationScript = n: {enabled, destination, ...}@opts: {
text = mkMerge [ text = mkMerge [
(mkIf enabled (makeUpdateScript n opts)) (mkIf (!enabled) ''rm -f ${shq destination}'')
(mkIf (!enabled) ''rm -f ${shq destination}'')
]; ];
}; };
secretToGenerateNamespace = n: opts: let namespace = ns opts; in ''
printf 'echo %q > %q\n' "$(${pkgs.util-linux}/bin/uuidgen -s -n "$UUID" -N ${shq namespace})" ${shq config.environment.deterministic-passwords.namespace-folder}/${shq namespace}
'';
in in
{ {
systemd.paths = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}" (secretToPath n v)) config.environment.deterministic-passwords; systemd.targets."deterministic-passwords".wants = [ "paths.target" ];
systemd.services = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}" (secretToService n v)) config.environment.deterministic-passwords; systemd.paths = (
system.activationScripts = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}-refresh" (secretToActivationScript n v)) config.environment.deterministic-passwords; (mapAttrs' (n: v: nameValuePair "deterministic-password-setup-${n}" (secretToPath n v)) config.environment.deterministic-passwords.secrets)
environment.deterministic-passwords = mkDefault {}; //
(mapAttrs' (n: v: nameValuePair "deterministic-password-after-${n}" (secretToAfterPath n v)) config.environment.deterministic-passwords.secrets)
);
systemd.services = (
(mapAttrs' (n: v: nameValuePair "deterministic-password-setup-${n}" (secretToService n v)) config.environment.deterministic-passwords.secrets)
//
(mapAttrs' (n: v: nameValuePair "deterministic-password-after-${n}" (secretToAfterService n v)) config.environment.deterministic-passwords.secrets)
);
system.build.deterministic-passwords.generateNamespaces = ''
if [ -z "$UUID" ];then
1>&2 echo "the root UUID must be in the UUID environment variable"
exit 1
fi
printf '(set -e; X=%q; mkdir -p "$X"; rm -f "$X"/*:*; )\n' ${shq config.environment.deterministic-passwords.namespace-folder}
(
${ join-string "\n" (mapAttrsToList (n: v: (secretToGenerateNamespace n v)) config.environment.deterministic-passwords.secrets) }
true ; ) | ${pkgs.coreutils}/bin/sort | ${pkgs.coreutils}/bin/uniq
'';
}; };
} }