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@<config>  -> nixos-rebuild's sudo prompt
  kexec/install     root@<host>      -> 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.
This commit is contained in:
2026-07-21 00:50:32 +02:00
parent 0995a5fe2f
commit 10416ed23d
+85 -11
View File
@@ -19,6 +19,13 @@
# SSH host key must be at ~/.config/homelab/<config>/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@<config> darman's sudo password (switch/boot/test)
# root@<host> 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 <kexec|install|switch|boot|test> ..."
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)