# 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 = 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;}; namespace = mkOption { type = str; 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 { type = str; description = "Group that will own the secret."; default = "root"; }; user = mkOption { type = str; description = "User who will own the secret."; default = "root"; }; mode = mkOption { default = "0400"; type = str; description = "Target file mode (octal)"; }; 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. ''; }; }; })); }; }; config = let shq = escapeShellArg; 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) { wantedBy = [ "multi-user.target" ]; pathConfig = { PathChanged = "/etc/cascade/deterministic-passwords/namespaces/${ns opts}"; }; }; secretToService = n: {enabled, before, ...}@opts: mkIf (enabled) { serviceConfig.Type = "oneshot"; requiredBy = before; before = before; script = makeUpdateScript n 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) ''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.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 ''; }; }