passwords: Refactor
This commit is contained in:
parent
81b1307609
commit
7b9c4fb004
|
@ -1,7 +1,53 @@
|
||||||
{ 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}";
|
||||||
|
passwordStorePath = password: "${passwordStoreDir}/${passwordHash password}";
|
||||||
|
describePassword = password: password.path
|
||||||
|
+ (if password.selector != null then " -> ${password.selector}" else "")
|
||||||
|
+ (if password.transform != null then " | (${password.transform})" else "");
|
||||||
|
passwordFiles = builtins.attrValues config.vivarium.passwordFiles;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
system = {
|
||||||
|
activationScripts.secrets = {
|
||||||
|
deps = [ "users" "groups" ];
|
||||||
|
supportsDryActivation = false; # TODO
|
||||||
|
text =
|
||||||
|
let
|
||||||
|
readPassword = password: "cat ${lib.strings.escapeShellArg (passwordStorePath password)}";
|
||||||
|
# Using awk's ENVIRON so it should resist any input?
|
||||||
|
pipeSubstitute = k: v: ''K=${lib.strings.escapeShellArg k} V="$(${v})" awk '{ gsub (ENVIRON["K"], ENVIRON["V"]); print }' '';
|
||||||
|
subsitutePassword = variable: password: pipeSubstitute variable (readPassword password);
|
||||||
|
subsitutePasswordFile = passwordFile: lib.mapAttrsToList subsitutePassword passwordFile.passwords;
|
||||||
|
renderPasswordFile = passwordFile: "${lib.strings.concatStringsSep "| " (subsitutePasswordFile passwordFile)} < ${passwordFile.template}";
|
||||||
|
installPasswordFile = passwordFile: ''
|
||||||
|
install -C -o ${passwordFile.owner} -g ${passwordFile.group} -m ${passwordFile.mode} -d ${builtins.dirOf passwordFile.path}
|
||||||
|
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
|
if password.selector == null
|
||||||
then " | head -n1"
|
then " | head -n1"
|
||||||
else
|
else
|
||||||
|
@ -9,61 +55,56 @@ let
|
||||||
then ""
|
then ""
|
||||||
else " | tail -n +2 | yq -r '.${password.selector}'")
|
else " | tail -n +2 | yq -r '.${password.selector}'")
|
||||||
);
|
);
|
||||||
readPasswordFile = password: (readPassword password) + (lib.strings.optionalString (password.transform != null) "| ${password.transform}");
|
readPassword = password: (readPasswordClear password) + (lib.strings.optionalString (password.transform != null) " | ${password.transform}");
|
||||||
passwords = builtins.attrValues config.vivarium.passwords;
|
isGeneratedPassword = password: ''test -f "$PASSWORD_STORE_DIR"/${lib.strings.escapeShellArg password.path}.gpg'';
|
||||||
passwordFiles = builtins.attrValues config.vivarium.passwordFiles;
|
testCanGenerate = password: lib.asserts.assertMsg (builtins.elem password.selector [ "@" null ]) "Unimplemented: generator + selector ${describePassword password}";
|
||||||
generatePassword = password: ''
|
generatePassword = password: assert testCanGenerate password; ''${password.generator} | pass insert -m ${lib.strings.escapeShellArg password.path}'';
|
||||||
if [ ! -f "$PASSWORD_STORE_DIR/${password.path}.gpg" ]
|
raiseCantGenerate = password: ''echo "Error: no generator" ; exit 1'';
|
||||||
|
ensurePassword = password: ''
|
||||||
|
if ! ${isGeneratedPassword password}
|
||||||
then
|
then
|
||||||
${password.generator} | pass insert -m "${password.path}"
|
echo Generating ${lib.strings.escapeShellArg (describePassword password)}
|
||||||
|
${(if password.generator != null then generatePassword else raiseCantGenerate) password}
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
ensurePassword = password:
|
writePasswordStore = password: ''
|
||||||
(lib.optionalString (password.generator != null) (generatePassword password)) + ''
|
# ${describePassword password}
|
||||||
${readPassword password} > /dev/null
|
${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
|
||||||
'';
|
'';
|
||||||
pipeSubstitute = k: v: " | K='${k}' V=\"$(${v})\" awk '{ gsub (ENVIRON[\"K\"], ENVIRON[\"V\"]); print }'";
|
# 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.)
|
||||||
renderPasswordFile = passwordFile: "echo ${lib.strings.escapeShellArg passwordFile.text} ${lib.strings.concatStrings (map
|
allFilenames = builtins.map (password: "${passwordStoreDir}/${passwordHash password}") passwords;
|
||||||
(password: pipeSubstitute password.variable (readPasswordFile password))
|
in
|
||||||
(lib.attrsets.attrValues passwordFile.passwords))}";
|
''
|
||||||
installPasswordFile = passwordFile: ''
|
ln -s ${lib.getExe (pkgs.writeShellApplication {
|
||||||
sudo mkdir -p "${builtins.dirOf passwordFile.path}"
|
name = "update-password-store";
|
||||||
${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
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
system.extraSystemBuilderCmds = ''
|
|
||||||
ln -s ${pkgs.writeShellApplication {
|
|
||||||
name = "generate-passwords";
|
|
||||||
text = ''
|
text = ''
|
||||||
test -d "$PASSWORD_STORE_DIR"
|
test -d "$PASSWORD_STORE_DIR"
|
||||||
'' + lib.strings.concatLines (builtins.map ensurePassword passwords);
|
sudo install -C -o root -g root -m u=rwx -d "${passwordStoreDir}"
|
||||||
}}/bin/generate-passwords $out/bin/generate-passwords
|
|
||||||
ln -s ${pkgs.writeShellApplication {
|
${lib.strings.concatLines (builtins.map writePasswordStore passwords)}
|
||||||
name = "install-passwords";
|
|
||||||
text = lib.strings.concatStrings (builtins.map installPasswordFile passwordFiles);
|
comm -23 <(sudo find ${passwordStoreDir} -type f | sort) <(echo ${lib.strings.escapeShellArg (lib.strings.concatLines allFilenames)} | sort) | while read -r file
|
||||||
}}/bin/install-passwords $out/bin/install-passwords
|
do
|
||||||
ln -s ${pkgs.writeShellApplication {
|
echo Removing "$file" from password store
|
||||||
name = "fix-permissions-passwords";
|
sudo rm "$file"
|
||||||
text = lib.strings.concatStrings (builtins.map fixPermissionsPasswordFile passwordFiles);
|
done
|
||||||
}}/bin/fix-permissions-passwords $out/bin/fix-permissions-passwords
|
|
||||||
'';
|
'';
|
||||||
|
})} $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,7 +112,12 @@ in
|
||||||
vivarium =
|
vivarium =
|
||||||
let
|
let
|
||||||
defaultvar = "@PASSWORD@";
|
defaultvar = "@PASSWORD@";
|
||||||
passwordTypeCommon = {
|
passwordSubmodule = lib.types.submodule ({ ... }: {
|
||||||
|
options = {
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Path to the password store entry";
|
||||||
|
};
|
||||||
selector = lib.mkOption {
|
selector = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
@ -82,39 +128,18 @@ in
|
||||||
default = "${lib.getExe pkgs.pwgen} -s 32";
|
default = "${lib.getExe pkgs.pwgen} -s 32";
|
||||||
description = "Command to generate the password. Won't work when selector is set to read metadata.";
|
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;
|
|
||||||
default = name;
|
|
||||||
description = "String in the template that will be substituted by the actual password";
|
|
||||||
};
|
|
||||||
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`.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue