passwords: Refactor

This commit is contained in:
Geoffrey Frogeye 2024-06-26 02:16:50 +02:00
parent 81b1307609
commit 7b9c4fb004
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
3 changed files with 127 additions and 103 deletions

View file

@ -1,69 +1,110 @@
{ pkgs, lib, config, ... }: { pkgs, lib, config, ... }:
let let
pass_subdir = ""; passwordStoreDir = "/etc/passwords";
readPassword = password: "pass show '${pass_subdir}/${password.path}'" + ( passwordHash = password: builtins.hashString "sha256" "${password.path}\n${builtins.toString password.selector}\n${builtins.toString password.transform}";
if password.selector == null passwordStorePath = password: "${passwordStoreDir}/${passwordHash password}";
then " | head -n1" describePassword = password: password.path
else + (if password.selector != null then " -> ${password.selector}" else "")
(if password.selector == "@" + (if password.transform != null then " | (${password.transform})" else "");
then ""
else " | tail -n +2 | yq -r '.${password.selector}'")
);
readPasswordFile = password: (readPassword password) + (lib.strings.optionalString (password.transform != null) "| ${password.transform}");
passwords = builtins.attrValues config.vivarium.passwords;
passwordFiles = builtins.attrValues config.vivarium.passwordFiles; passwordFiles = builtins.attrValues config.vivarium.passwordFiles;
generatePassword = password: ''
if [ ! -f "$PASSWORD_STORE_DIR/${password.path}.gpg" ]
then
${password.generator} | pass insert -m "${password.path}"
fi
'';
ensurePassword = password:
(lib.optionalString (password.generator != null) (generatePassword password)) + ''
${readPassword password} > /dev/null
'';
pipeSubstitute = k: v: " | K='${k}' V=\"$(${v})\" awk '{ gsub (ENVIRON[\"K\"], ENVIRON[\"V\"]); print }'";
renderPasswordFile = passwordFile: "echo ${lib.strings.escapeShellArg passwordFile.text} ${lib.strings.concatStrings (map
(password: pipeSubstitute password.variable (readPasswordFile password))
(lib.attrsets.attrValues passwordFile.passwords))}";
installPasswordFile = passwordFile: ''
sudo mkdir -p "${builtins.dirOf passwordFile.path}"
${renderPasswordFile passwordFile} | sudo tee ${passwordFile.path} > /dev/null
'';
fixPermissionsPasswordFile = passwordFile: ''
sudo chown ${passwordFile.owner}:${passwordFile.group} ${passwordFile.path}
sudo chmod ${passwordFile.mode} ${passwordFile.path}
'';
in in
{ {
config = { config = {
system.extraSystemBuilderCmds = '' system = {
ln -s ${pkgs.writeShellApplication { activationScripts.secrets = {
name = "generate-passwords"; deps = [ "users" "groups" ];
text = '' supportsDryActivation = false; # TODO
test -d "$PASSWORD_STORE_DIR" text =
'' + lib.strings.concatLines (builtins.map ensurePassword passwords); let
}}/bin/generate-passwords $out/bin/generate-passwords readPassword = password: "cat ${lib.strings.escapeShellArg (passwordStorePath password)}";
ln -s ${pkgs.writeShellApplication { # Using awk's ENVIRON so it should resist any input?
name = "install-passwords"; pipeSubstitute = k: v: ''K=${lib.strings.escapeShellArg k} V="$(${v})" awk '{ gsub (ENVIRON["K"], ENVIRON["V"]); print }' '';
text = lib.strings.concatStrings (builtins.map installPasswordFile passwordFiles); subsitutePassword = variable: password: pipeSubstitute variable (readPassword password);
}}/bin/install-passwords $out/bin/install-passwords subsitutePasswordFile = passwordFile: lib.mapAttrsToList subsitutePassword passwordFile.passwords;
ln -s ${pkgs.writeShellApplication { renderPasswordFile = passwordFile: "${lib.strings.concatStringsSep "| " (subsitutePasswordFile passwordFile)} < ${passwordFile.template}";
name = "fix-permissions-passwords"; installPasswordFile = passwordFile: ''
text = lib.strings.concatStrings (builtins.map fixPermissionsPasswordFile passwordFiles); install -C -o ${passwordFile.owner} -g ${passwordFile.group} -m ${passwordFile.mode} -d ${builtins.dirOf passwordFile.path}
}}/bin/fix-permissions-passwords $out/bin/fix-permissions-passwords temp="$(mktemp)"
''; trap 'rm "$temp"' ERR
${renderPasswordFile passwordFile} > "$temp"
install -C -o ${passwordFile.owner} -g ${passwordFile.group} -m ${passwordFile.mode} -T "$temp" ${passwordFile.path}
rm "$temp"
trap - ERR
'';
installPasswordFileApp = passwordFile: pkgs.writeShellApplication {
name = builtins.replaceStrings ["/"] ["_"] passwordFile.path;
runtimeInputs = with pkgs; [ gawk ];
text = installPasswordFile passwordFile;
};
installPasswordFileSandboxed = passwordFile:
''${lib.getExe (installPasswordFileApp passwordFile)} || echo Failed to install ${lib.strings.escapeShellArg passwordFile.path}'';
in
''
echo Installing secrets...
${lib.strings.concatLines (builtins.map installPasswordFileSandboxed passwordFiles)}
'';
};
extraSystemBuilderCmds =
let
passwords = builtins.attrValues config.vivarium.passwords;
readPasswordClear = password: "pass show ${lib.strings.escapeShellArg password.path}" + (
if password.selector == null
then " | head -n1"
else
(if password.selector == "@"
then ""
else " | tail -n +2 | yq -r '.${password.selector}'")
);
readPassword = password: (readPasswordClear password) + (lib.strings.optionalString (password.transform != null) " | ${password.transform}");
isGeneratedPassword = password: ''test -f "$PASSWORD_STORE_DIR"/${lib.strings.escapeShellArg password.path}.gpg'';
testCanGenerate = password: lib.asserts.assertMsg (builtins.elem password.selector [ "@" null ]) "Unimplemented: generator + selector ${describePassword password}";
generatePassword = password: assert testCanGenerate password; ''${password.generator} | pass insert -m ${lib.strings.escapeShellArg password.path}'';
raiseCantGenerate = password: ''echo "Error: no generator" ; exit 1'';
ensurePassword = password: ''
if ! ${isGeneratedPassword password}
then
echo Generating ${lib.strings.escapeShellArg (describePassword password)}
${(if password.generator != null then generatePassword else raiseCantGenerate) password}
fi
'';
writePasswordStore = password: ''
# ${describePassword password}
${ensurePassword password}
echo Updating ${lib.strings.escapeShellArg (describePassword password)} in password store
temp="$(mktemp)"
trap 'rm "$temp"' ERR
${readPassword password} > "$temp"
sudo install -C -o root -g root -m u=rw -T "$temp" ${lib.strings.escapeShellArg (passwordStorePath password)}
rm "$temp"
trap - ERR
'';
# TODO Only read password if timestamp didn't change from git repository (and alert if in future, exists on fs but not in git, etc.)
allFilenames = builtins.map (password: "${passwordStoreDir}/${passwordHash password}") passwords;
in
''
ln -s ${lib.getExe (pkgs.writeShellApplication {
name = "update-password-store";
text = ''
test -d "$PASSWORD_STORE_DIR"
sudo install -C -o root -g root -m u=rwx -d "${passwordStoreDir}"
${lib.strings.concatLines (builtins.map writePasswordStore passwords)}
comm -23 <(sudo find ${passwordStoreDir} -type f | sort) <(echo ${lib.strings.escapeShellArg (lib.strings.concatLines allFilenames)} | sort) | while read -r file
do
echo Removing "$file" from password store
sudo rm "$file"
done
'';
})} $out/bin/
'';
};
vivarium.passwords = vivarium.passwords =
let let
passwordTypes = lib.lists.flatten (map (f: builtins.attrValues f.passwords) passwordFiles); passwordsList = lib.lists.flatten (map (f: builtins.attrValues f.passwords) passwordFiles);
password = passwordType: { passwordsAttrs = map (password: { ${passwordHash password} = password; }) passwordsList;
${passwordType.path} = {
inherit (passwordType) selector generator;
};
};
passwords = map password passwordTypes;
in in
lib.attrsets.mergeAttrsList passwords; lib.attrsets.mergeAttrsList passwordsAttrs;
}; };
options = { options = {
# Using vivarium because that's where it's from, and we don't want it in home manager's frogeye # Using vivarium because that's where it's from, and we don't want it in home manager's frogeye
@ -71,50 +112,34 @@ in
vivarium = vivarium =
let let
defaultvar = "@PASSWORD@"; defaultvar = "@PASSWORD@";
passwordTypeCommon = { passwordSubmodule = lib.types.submodule ({ ... }: {
selector = lib.mkOption { options = {
type = lib.types.nullOr lib.types.str; path = lib.mkOption {
default = null;
description = "If unset, selects the first line. If '@', select everything. If any other value, will parse the password metadata as YML and use selector (yq).";
};
generator = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "${lib.getExe pkgs.pwgen} -s 32";
description = "Command to generate the password. Won't work when selector is set to read metadata.";
};
};
hostConfig = config;
passwordTypeFile = { name, config, ... }: {
options = passwordTypeCommon // {
variable = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = name; description = "Path to the password store entry";
description = "String in the template that will be substituted by the actual password"; };
selector = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "If unset, selects the first line. If '@', select everything. If any other value, will parse the password metadata as YML and use selector (yq).";
};
generator = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "${lib.getExe pkgs.pwgen} -s 32";
description = "Command to generate the password. Won't work when selector is set to read metadata.";
}; };
transform = lib.mkOption { transform = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
description = "Shell command to transform the password with before substitution"; description = "Shell command to transform the password with before substitution";
}; };
path = lib.mkOption {
type = lib.types.str;
description = "Path to the password store entry";
};
}; };
}; });
in in
{ {
passwords = lib.mkOption { passwords = lib.mkOption {
default = { }; default = { };
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { type = lib.types.attrsOf passwordSubmodule;
options = passwordTypeCommon // {
path = lib.mkOption {
type = lib.types.str;
default = name;
description = "Path to the password store entry";
};
};
}));
}; };
passwordFiles = passwordFiles =
lib.mkOption { lib.mkOption {
@ -146,13 +171,18 @@ in
default = defaultvar; default = defaultvar;
description = "Content of the template used to make the file. Exclusive with `template`."; description = "Content of the template used to make the file. Exclusive with `template`.";
}; };
template = lib.mkOption {
type = lib.types.path;
default = pkgs.writeText "password-template" config.text;
description = "Content of the template used to make the file. Exclusive with `text`.";
};
passwords = lib.mkOption { passwords = lib.mkOption {
default = lib.optionalAttrs (config.password != null) { ${defaultvar} = config.password; }; default = lib.optionalAttrs (config.password != null) { ${defaultvar} = config.password; };
type = lib.types.attrsOf (lib.types.submodule passwordTypeFile); type = lib.types.attrsOf passwordSubmodule;
description = "Paths to passwords that will substitute the variables in the template. Exclusive with `password`"; description = "Paths to passwords that will substitute the variables in the template. Exclusive with `password`";
}; };
password = lib.mkOption { password = lib.mkOption {
type = lib.types.submodule ({ ... }@args: passwordTypeFile (args // { name = defaultvar; })); type = passwordSubmodule;
description = "Path to password that will substitute '@PASSWORD@' in the template. Exclusive with `passwords`."; description = "Path to password that will substitute '@PASSWORD@' in the template. Exclusive with `passwords`.";
}; };
}; };

View file

@ -14,18 +14,18 @@ fi
tmpdir="$(mktemp -d)" tmpdir="$(mktemp -d)"
# sudo so the eval cache is shared with nixos-rebuild # sudo so the eval cache is shared with nixos-rebuild
sudo nom build "$self#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" -o "$tmpdir/toplevel" "$@" sudo nom build "$self#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" -o "$tmpdir/toplevel" "$@"
toplevel="$(readlink "$tmpdir/toplevel")" toplevel="$(readlink -f "$tmpdir/toplevel")"
rm -rf "$tmpdir" rm -rf "$tmpdir"
# Show diff # Show diff
nvd diff /nix/var/nix/profiles/system "$toplevel" nvd diff "$(readlink -f /nix/var/nix/profiles/system)" "$toplevel"
# Figure out specialisation # Figure out specialisation
specialisationArgs=() specialisationArgs=()
currentSystem="$(readlink /run/current-system)" currentSystem="$(readlink -f /run/current-system)"
while read -r specialisation while read -r specialisation
do do
if [ "$(readlink "/nix/var/nix/profiles/system/specialisation/$specialisation")" = "$currentSystem" ] if [ "$(readlink -f "/nix/var/nix/profiles/system/specialisation/$specialisation")" = "$currentSystem" ]
then then
specialisationArgs=("--specialisation" "$specialisation") specialisationArgs=("--specialisation" "$specialisation")
fi fi
@ -40,14 +40,8 @@ then
fi fi
if [ "$verb" = "test" ] || [ "$verb" = "switch" ] || [ "$confirm" = "y" ] if [ "$verb" = "test" ] || [ "$verb" = "switch" ] || [ "$confirm" = "y" ]
then then
# Generate passwords first. If there's a missing one that cannot be generated, we'll know before anything is written "$toplevel/bin/update-password-store"
"$toplevel/bin/generate-passwords"
# Install the passwords to their respective directories
"$toplevel/bin/install-passwords"
sudo nixos-rebuild --flake "$self#$HOSTNAME" test "${specialisationArgs[@]}" "$@" sudo nixos-rebuild --flake "$self#$HOSTNAME" test "${specialisationArgs[@]}" "$@"
# Fix passwords permission. After install, so it can use new users
"$toplevel/bin/fix-permissions-passwords"
# TODO Install passwords with correct permissions during activation
fi fi
# Set as boot # Set as boot

View file

@ -28,7 +28,7 @@ let
in in
{ {
config = { config = {
system.activationScripts.diff = { system.activationScripts.remote = {
supportsDryActivation = true; supportsDryActivation = true;
text = '' text = ''
mkdir -p /root/.ssh mkdir -p /root/.ssh