From 10416ed23dc59b79498ffa318cba35e8891fb44d Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Tue, 21 Jul 2026 00:50:32 +0200 Subject: [PATCH] deploy: auto-fill password prompts from Proton Pass Every deploy stopped at a password prompt. pass-cli is installed, so read the passwords from the HomeLab vault instead, per command: switch/boot/test darman@ -> nixos-rebuild's sudo prompt kexec/install root@ -> the target's ssh password nixos-rebuild prompts via getpass(), which reads /dev/tty and ignores a piped stdin, so that one runs under setsid: no controlling terminal means getpass falls back to stdin. kexec wraps the master ssh in `sshpass -e` (scp rides the control socket) and pins password auth so a key can't fall through into a second prompt; install uses nixos-anywhere's own --env-password. Missing pass-cli, a logged-out session, or an absent item all yield an empty string and the original interactive prompt -- nothing becomes mandatory. Passwords never reach a command line. --- scripts/deploy | 96 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/scripts/deploy b/scripts/deploy index d891b27..98aad7b 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -19,6 +19,13 @@ # SSH host key must be at ~/.config/homelab//ssh_host_ed25519_key. # # Runs from a non-NixOS host too (nixos-rebuild / nixos-anywhere via `nix run`). +# +# Password prompts are auto-filled from the "HomeLab" Proton Pass vault when +# `pass-cli` is installed and logged in; otherwise every command prompts exactly +# as before. Items: +# darman@ darman's sudo password (switch/boot/test) +# root@ root's ssh password (kexec/install) +# Override with HOMELAB_PASS_ITEM / HOMELAB_PASS_ROOT_ITEM / HOMELAB_PASS_VAULT. set -euo pipefail # Locate the repo root (flake dir) regardless of where this script lives on disk. @@ -29,6 +36,26 @@ export PATH="/nix/var/nix/profiles/default/bin:$PATH" die() { echo "error: $*" >&2; exit 1; } +# The password field of a Proton Pass item ("--field password" prints the bare +# value, one line), or empty if pass-cli is missing / logged out / has no such +# item — every caller then falls back to the normal interactive prompt. +proton_pass_password() { + local title="$1" + command -v pass-cli >/dev/null 2>&1 || return 0 + pass-cli item view \ + --vault-name "${HOMELAB_PASS_VAULT:-HomeLab}" \ + --item-title "$title" \ + --field password --output human 2>/dev/null | head -1 +} + +# Path to an sshpass binary (system one, else built from nixpkgs). Empty if +# neither is available. +sshpass_bin() { + command -v sshpass 2>/dev/null && return 0 + nix build --no-link --print-out-paths nixpkgs#sshpass 2>/dev/null \ + | sed 's|$|/bin/sshpass|' +} + cmd="${1:-}"; [ -n "$cmd" ] || die "usage: ./deploy ..." case "$cmd" in @@ -49,10 +76,32 @@ case "$cmd" in o=(-o ControlMaster=auto -o "ControlPath=$cm" -o ControlPersist=300 \ -o StrictHostKeyChecking=accept-new) - echo ">> connecting to root@$host (enter the root password once)" - ssh "${o[@]}" "root@$host" 'mkdir -p /tmp/bin' + # Root's password from Proton Pass, fed to ssh/scp via sshpass -e. Only the + # first (master) connection authenticates; the rest ride the control socket. + # SSHPASS is exported into the sshpass child only, never onto a command line. + sp=() + root_pw="$(proton_pass_password "${HOMELAB_PASS_ROOT_ITEM:-root@$host}" || true)" + if [ -n "$root_pw" ]; then + sshpass="$(sshpass_bin || true)" + if [ -n "$sshpass" ]; then + sp=(env "SSHPASS=$root_pw" "$sshpass" -e) + # sshpass drives the password prompt; don't let a key/agent short-circuit + # into an interactive one for a host that only accepts passwords. + o+=(-o PreferredAuthentications=password -o PubkeyAuthentication=no) + else + echo ">> sshpass unavailable — falling back to the interactive prompt" >&2 + fi + fi + + if [ ${#sp[@]} -gt 0 ]; then + echo ">> connecting to root@$host (password from Proton Pass)" + else + echo ">> connecting to root@$host (enter the root password once)" + fi + "${sp[@]}" ssh "${o[@]}" "root@$host" 'mkdir -p /tmp/bin' scp "${o[@]}" "$cpio" "root@$host:/tmp/bin/cpio" scp "${o[@]}" "$bbox" "root@$host:/tmp/bin/gzip" # busybox as gzip (argv0) + unset root_pw echo ">> streaming installer + kexec-ing. SSH drops as the box jumps into the" echo " RAM installer. Disks are untouched." @@ -79,11 +128,22 @@ case "$cmd" in install -Dm644 "$hostkey.pub" "$stage/etc/ssh/ssh_host_ed25519_key.pub" echo ">> nixos-anywhere .#$config onto root@$host (OS disk WILL be wiped)" - nix run github:nix-community/nixos-anywhere -- \ - --flake ".#$config" \ - --extra-files "$stage" \ - --generate-hardware-config nixos-generate-config "./hosts/$config/hardware-configuration.nix" \ - --target-host "root@$host" + anywhere=(--flake ".#$config" + --extra-files "$stage" + --generate-hardware-config nixos-generate-config "./hosts/$config/hardware-configuration.nix" + --target-host "root@$host") + + # nixos-anywhere's --env-password reads root's ssh password from $SSHPASS + # (it ships its own sshpass), so a vault hit skips the ssh-copy-id prompt. + root_pw="$(proton_pass_password "${HOMELAB_PASS_ROOT_ITEM:-root@$host}" || true)" + if [ -n "$root_pw" ]; then + echo ">> root ssh password from Proton Pass" + env "SSHPASS=$root_pw" nix run github:nix-community/nixos-anywhere -- \ + --env-password "${anywhere[@]}" + else + nix run github:nix-community/nixos-anywhere -- "${anywhere[@]}" + fi + unset root_pw ;; switch|boot|test) @@ -95,10 +155,24 @@ case "$cmd" in # security.sudo.wheelNeedsPassword = true, and --use-remote-sudo only # prefixes with sudo without ever prompting. Asks for darman's password # (the darman_password hash in each host's sops file). - nix run nixpkgs#nixos-rebuild -- "$cmd" \ - --flake ".#$config" \ - --target-host "darman@$host" \ - --ask-sudo-password + rebuild=(nix run nixpkgs#nixos-rebuild -- "$cmd" + --flake ".#$config" + --target-host "darman@$host" + --ask-sudo-password) + + item="${HOMELAB_PASS_ITEM:-darman@$config}" + pw="$(proton_pass_password "$item" || true)" + if [ -n "$pw" ] && command -v setsid >/dev/null 2>&1; then + # nixos-rebuild prompts with getpass(), which reads /dev/tty and ignores a + # piped stdin. setsid drops the controlling terminal, so getpass falls back + # to stdin and takes the vault password (it warns about echo — harmless, + # nothing is echoed since the password never reaches the terminal). + echo ">> sudo password from Proton Pass ($item)" + printf '%s\n' "$pw" | setsid -w "${rebuild[@]}" + else + "${rebuild[@]}" + fi + unset pw ;; image|flash)