From 7f621dfcca48d81e558e78fb790e42f222b9fdcf Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Sat, 25 Jul 2026 15:49:19 +1000 Subject: [PATCH] fix(push-host-keys): prompt sudo password once, pass via sudo -S Instead of ssh -t (requires PTY on both sides), prompt for the sudo password once at startup and pipe it to each remote invocation via sudo -S. This works from any context -- interactive terminal, background agent, or script -- with no PTY needed on either end. Also accepts SUDO_PASS from the environment for non-interactive callers. Co-Authored-By: Claude Sonnet 4.6 --- scripts/secrets/push-host-keys.sh | 41 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/scripts/secrets/push-host-keys.sh b/scripts/secrets/push-host-keys.sh index 09b3a87..38a4cde 100755 --- a/scripts/secrets/push-host-keys.sh +++ b/scripts/secrets/push-host-keys.sh @@ -19,8 +19,9 @@ # already has the current .sops.yaml/secrets/*.yaml. # # SSH: connects as SSH_USER@ (default: nixos, the user with the -# admin authorized key), then installs files via sudo. You will be prompted -# for the sudo password once per host. +# admin authorized key), then installs files via sudo -S (reads the sudo +# password from stdin). The password is prompted once at startup and reused +# for every host -- no PTY or terminal required on the remote side. # Hosts are reached at their bare hostname (relies on LAN DNS/mDNS). set -euo pipefail @@ -37,6 +38,7 @@ SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5) dry_run=0 skip_git_check=0 +sudo_password="" usage() { cat <] [--dry-run] [--skip-git-check] Environment: SSH_USER SSH username (default: nixos). + SUDO_PASS Sudo password (skips the interactive prompt; useful + when calling from another script). EOF } +# Prompt for the sudo password once; store it for all _do_push calls. +# Accepts SUDO_PASS from the environment to allow non-interactive callers. +prompt_sudo_password() { + [[ "$dry_run" -eq 1 ]] && return + if [[ -n "${SUDO_PASS:-}" ]]; then + sudo_password="$SUDO_PASS" + return + fi + read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password < /dev/tty + echo >&2 +} + locally_managed_hosts() { for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do [[ -e "$f" ]] || continue @@ -135,7 +151,7 @@ _do_push() { if [[ "$dry_run" -eq 1 ]]; then echo " [dry-run] would scp host-keys/${target}_ssh_host_ed25519_key{,.pub} to /tmp/" - echo " [dry-run] would: sudo install -m 0600/0644 to /etc/ssh/ and rm /tmp copies" + echo " [dry-run] would: sudo -S install -m 0600/0644 to /etc/ssh/ and rm /tmp copies" return fi @@ -145,15 +161,15 @@ _do_push() { scp -o StrictHostKeyChecking=no \ "$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub" - # Install with correct permissions via sudo. Commands are passed as an - # argument string (not heredoc) so stdin stays free and -t can allocate - # a PTY for the sudo password prompt. - echo " (sudo password may be required)" - ssh -t -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \ - "sudo install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key && - sudo install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub && - sudo rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub && - echo ' [ok] host key installed'" + # Install via sudo -S: the password is piped via herestring so no PTY is + # needed on either side. -p '' suppresses sudo's own prompt string. + ssh -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \ + "sudo -S -p '' bash -c ' + install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key + install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub + rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub + echo \" [ok] host key installed\" + '" <<< "$sudo_password" # Drop the stale known_hosts entry for this host (public key just changed) ssh-keygen -R "$hostname" 2>/dev/null || true @@ -278,6 +294,7 @@ fi nix_extra_opts ensure_remote_current +prompt_sudo_password if [[ "$mode" == "single" ]]; then push_target "$target_arg"