# 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''; }; renew = mkOption { type = nullOr str; description = '' a systemd time interval (systemd.time(7)) or null specifies the interval after which the secret will be renewed by recomputing it from its namespace, running the writer, and running the after action. this occurs after the first time the secret is written and on every boot. if set to null, the default, this will only take place when the namespace secret is refreshed. prod sniff test! the default is the _safest_, not the _best_. if the writer and action are cheap, consider putting this on a loop to ensure there is no issue caused by accidental usage of NFS or similar issues which can disrupt the async notification mechanisms in the kernel. ''; default = null; }; 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 ) ''; secretToTimer = n: {enabled, renew, ...}@opts: mkIf (enabled) { wantedBy = [ "multi-user.target" "timers.target" ]; timerConfig.OnUnitActiveSec = renew; timerConfig.OnStartupSec = "3s"; timerConfig.AccuracySec = mkIf (isSecondsTimeUnit renew) "1s"; partOf = [ "deterministic-password-setup-${n}.path" ]; }; 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, destination, ...}@opts: mkIf (enabled) { wantedBy = [ "multi-user.target" ]; pathConfig = { PathChanged = destination; }; }; 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}'') (mkIf (enabled) ''systemctl start deterministic-password-setup-${n}.service'') ]; }; 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.timers = ( (mapAttrs' (n: v: nameValuePair "deterministic-password-setup-${n}" (secretToTimer n v)) config.environment.deterministic-passwords.secrets) ); 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 ''; }; }