246 lines
8.8 KiB
Nix
246 lines
8.8 KiB
Nix
# 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 <cascade/functions>;
|
|
|
|
{
|
|
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
|
|
'';
|
|
};
|
|
|
|
}
|