diff --git a/os/password/default.nix b/os/password/default.nix index 67406a7..be7665d 100644 --- a/os/password/default.nix +++ b/os/password/default.nix @@ -1,69 +1,110 @@ { 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; + passwordStoreDir = "/etc/passwords"; + 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; - 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 - ''; + 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 + 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 = let - passwordTypes = lib.lists.flatten (map (f: builtins.attrValues f.passwords) passwordFiles); - password = passwordType: { - ${passwordType.path} = { - inherit (passwordType) selector generator; - }; - }; - passwords = map password passwordTypes; + passwordsList = lib.lists.flatten (map (f: builtins.attrValues f.passwords) passwordFiles); + passwordsAttrs = map (password: { ${passwordHash password} = password; }) passwordsList; in - lib.attrsets.mergeAttrsList passwords; + lib.attrsets.mergeAttrsList passwordsAttrs; }; options = { # 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 = 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 { + passwordSubmodule = lib.types.submodule ({ ... }: { + options = { + path = lib.mkOption { type = lib.types.str; - default = name; - description = "String in the template that will be substituted by the actual password"; + description = "Path to the password store entry"; + }; + 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 { 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"; - }; - }; - })); + type = lib.types.attrsOf passwordSubmodule; }; passwordFiles = lib.mkOption { @@ -146,13 +171,18 @@ in default = defaultvar; 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 { 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`"; }; 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`."; }; }; diff --git a/os/rebuild.sh b/os/rebuild.sh index d0cee42..05ab8cd 100644 --- a/os/rebuild.sh +++ b/os/rebuild.sh @@ -14,18 +14,18 @@ fi tmpdir="$(mktemp -d)" # sudo so the eval cache is shared with nixos-rebuild 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" # Show diff -nvd diff /nix/var/nix/profiles/system "$toplevel" +nvd diff "$(readlink -f /nix/var/nix/profiles/system)" "$toplevel" # Figure out specialisation specialisationArgs=() -currentSystem="$(readlink /run/current-system)" +currentSystem="$(readlink -f /run/current-system)" while read -r specialisation do - if [ "$(readlink "/nix/var/nix/profiles/system/specialisation/$specialisation")" = "$currentSystem" ] + if [ "$(readlink -f "/nix/var/nix/profiles/system/specialisation/$specialisation")" = "$currentSystem" ] then specialisationArgs=("--specialisation" "$specialisation") fi @@ -40,14 +40,8 @@ then fi if [ "$verb" = "test" ] || [ "$verb" = "switch" ] || [ "$confirm" = "y" ] 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" + "$toplevel/bin/update-password-store" 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 # Set as boot diff --git a/os/remote-builds/default.nix b/os/remote-builds/default.nix index 8d2611b..869c80d 100644 --- a/os/remote-builds/default.nix +++ b/os/remote-builds/default.nix @@ -28,7 +28,7 @@ let in { config = { - system.activationScripts.diff = { + system.activationScripts.remote = { supportsDryActivation = true; text = '' mkdir -p /root/.ssh