diff --git a/common/deterministic-passwords.nix b/common/deterministic-passwords.nix index 2d0413d..74d2e24 100644 --- a/common/deterministic-passwords.nix +++ b/common/deterministic-passwords.nix @@ -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, ...}: with lib; with builtins; with import ; { -options = { - environment.deterministic-passwords = mkOption { +options = with types; { + 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: { options = { enabled = mkEnableOption "password writer for ${name}" // {default = true;}; - destination = mkOption { + namespace = mkOption { 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 { @@ -33,11 +83,46 @@ options = { description = "Target file mode (octal)"; }; - action = mkOption { - default = []; - type = listOf str; - description = "Action to perform on the remote host when the secret changes"; + destination = mkOption { + type = str; + description = ''Where to save the secret''; }; + + 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 = let shq = escapeShellArg; - makeUpdateScript = n: {enabled, destination, group, user, mode, action}@opts: '' - ( - umask 0777 - ACTION=${shq (join-string "\n" action)} - ${pkgs.util-linux}/bin/uuidgen -s -n $(cat /etc/cascade/host-secret.uuid) -N ${shq n} > ${shq destination} - chown ${shq user}:${shq group} ${shq destination} - chmod ${shq mode} ${shq destination} - ${pkgs.bash}/bin/bash -c "$ACTION" + ns = {namespace,...}@opts: if namespace == "" then "host:${config.networking.hostName}" else namespace; + makeUpdateScript = n: {enabled, writer, destination, group, user, mode, action, ...}@opts: '' + ( set -e; export PATH="$PATH:/run/current-system/sw/sbin:/run/current-system/sw/bin" + # mkdir here instead of in a global activation script for simplicity. + mkdir -p ${shq config.environment.deterministic-passwords.namespace-folder} + + if [ -e /etc/cascade/deterministic-passwords/namespaces/${ns opts} ];then + 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" ]; 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"; + requiredBy = before; + before = before; 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 [ - (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 { - systemd.paths = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}" (secretToPath n v)) config.environment.deterministic-passwords; - systemd.services = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}" (secretToService n v)) config.environment.deterministic-passwords; - system.activationScripts = mapAttrs' (n: v: nameValuePair "deterministic-password-${n}-refresh" (secretToActivationScript n v)) config.environment.deterministic-passwords; - environment.deterministic-passwords = mkDefault {}; + systemd.targets."deterministic-passwords".wants = [ "paths.target" ]; + systemd.paths = ( + (mapAttrs' (n: v: nameValuePair "deterministic-password-setup-${n}" (secretToPath n v)) config.environment.deterministic-passwords.secrets) + // + (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 + ''; }; }