Allow setting OS-level password

Bit ugly as it is, but we're slowly iterating towards a secret manager
I'm happy with.
This commit is contained in:
Geoffrey Frogeye 2024-06-18 22:56:44 +02:00
parent 5462fa43fa
commit a39118d439
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
3 changed files with 171 additions and 0 deletions

View file

@ -12,6 +12,7 @@
disko.nixosModules.disko disko.nixosModules.disko
./gaming ./gaming
./geoffrey.nix ./geoffrey.nix
./password
./printing ./printing
./remote-builds ./remote-builds
./style ./style

163
os/password/default.nix Normal file
View file

@ -0,0 +1,163 @@
{ pkgs, lib, config, ... }:
let
pass_subdir = "";
readPassword = password: "pass show '${pass_subdir}/${password.path}'" + (
if password.selector == null
then " | head -n1"
else
(if password.selector == "@"
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;
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
{
config = {
system.extraSystemBuilderCmds = ''
ln -s ${pkgs.writeShellApplication {
name = "generate-passwords";
text = ''
test -d "$PASSWORD_STORE_DIR"
'' + lib.strings.concatLines (builtins.map ensurePassword passwords);
}}/bin/generate-passwords $out/bin/generate-passwords
ln -s ${pkgs.writeShellApplication {
name = "install-passwords";
text = lib.strings.concatStrings (builtins.map installPasswordFile passwordFiles);
}}/bin/install-passwords $out/bin/install-passwords
ln -s ${pkgs.writeShellApplication {
name = "fix-permissions-passwords";
text = lib.strings.concatStrings (builtins.map fixPermissionsPasswordFile passwordFiles);
}}/bin/fix-permissions-passwords $out/bin/fix-permissions-passwords
'';
vivarium.passwords =
let
passwordTypes = lib.lists.flatten (map (f: builtins.attrValues f.passwords) passwordFiles);
password = passwordType: {
${passwordType.path} = {
inherit (passwordType) selector generator;
};
};
passwords = map password passwordTypes;
in
lib.attrsets.mergeAttrsList passwords;
};
options = {
# Using vivarium because that's where it's from, and we don't want it in home manager's frogeye
# TODO Make this cleaner, merge the two, somehow
vivarium =
let
defaultvar = "@PASSWORD@";
passwordTypeCommon = {
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.";
};
};
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 {
type = lib.types.nullOr lib.types.str;
default = null;
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
{
passwords = lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = passwordTypeCommon // {
path = lib.mkOption {
type = lib.types.str;
default = name;
description = "Path to the password store entry";
};
};
}));
};
passwordFiles =
lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.submodule ({ name, config, ... }: {
options = {
path = lib.mkOption {
type = lib.types.str;
default = name;
description = "Where to place the file.";
};
mode = lib.mkOption {
type = lib.types.str;
default = "0400";
description = "Unix permission";
};
owner = lib.mkOption {
type = lib.types.str;
default = "root";
description = "Owner of the secret file";
};
group = lib.mkOption {
type = lib.types.str;
default = "root";
description = "Group of the secret file";
};
text = lib.mkOption {
type = lib.types.str;
default = defaultvar;
description = "Content of the template used to make the file. Exclusive with `template`.";
};
passwords = lib.mkOption {
default = lib.optionalAttrs (config.password != null) { ${defaultvar} = config.password; };
type = lib.types.attrsOf (lib.types.submodule passwordTypeFile);
description = "Paths to passwords that will substitute the variables in the template. Exclusive with `password`";
};
password = lib.mkOption {
type = lib.types.submodule ({ ... }@args: passwordTypeFile (args // { name = defaultvar; }));
description = "Path to password that will substitute '@PASSWORD@' in the template. Exclusive with `passwords`.";
};
};
}));
};
};
};
}

View file

@ -39,7 +39,14 @@ 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/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