Compare commits

...
Author SHA1 Message Date
beatzaplentyandClaude Sonnet 4.6 b65d78a515 fix(push-host-keys): detect non-interactive stdin, direct to SUDO_PASS
read exits non-zero when stdin is not a terminal (set -e killed the
script silently). Catch that and emit a clear error pointing to the
SUDO_PASS environment variable rather than crashing with no output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 15:55:06 +10:00
beatzaplentyandClaude Sonnet 4.6 fa53d37360 fix(push-host-keys): remove /dev/tty probe, plain read is sufficient
/dev/tty exists as a device node even without a controlling terminal,
so -r/-w tests pass but opening it fails. Plain 'read -r -s' from stdin
is enough: works interactively from a real terminal, and from a non-tty
context the caller should set SUDO_PASS in the environment instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 15:52:09 +10:00
beatzaplentyandClaude Sonnet 4.6 cbb86704a3 fix(push-host-keys): fall back to stdin when /dev/tty unavailable
Environments without a controlling terminal (containers, CI agents)
don't have /dev/tty. Try it first for the sudo password prompt, fall
back to plain stdin so the script works in both contexts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 15:49:56 +10:00
beatzaplentyandClaude Sonnet 4.6 7f621dfcca 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 <noreply@anthropic.com>
2026-07-25 15:49:19 +10:00
beatzaplentyandClaude Sonnet 4.6 c3eb2c9e9f fix(push-host-keys): fix sudo PTY allocation failure
ssh -t won't allocate a PTY when its own stdin is redirected (by a
heredoc). Replaced the heredoc-fed 'sudo bash -s' with commands passed
as an argument string so stdin stays free and -t can properly allocate
a PTY for the sudo password prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 15:34:47 +10:00
+36 -12
View File
@@ -19,8 +19,9 @@
# already has the current .sops.yaml/secrets/*.yaml.
#
# SSH: connects as SSH_USER@<hostname> (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 <<EOF
@@ -51,9 +53,30 @@ Usage: $0 [--all | <target>] [--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 exits non-zero when stdin is not a terminal (e.g. CI, background
# agents). Catch that and give a clear message rather than a silent exit.
if ! read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password; then
echo >&2
echo "ERROR: stdin is not a terminal -- cannot prompt for sudo password." >&2
echo " Set SUDO_PASS=<password> in the environment and re-run." >&2
exit 1
fi
echo >&2
}
locally_managed_hosts() {
for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do
[[ -e "$f" ]] || continue
@@ -135,7 +158,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 +168,15 @@ _do_push() {
scp -o StrictHostKeyChecking=no \
"$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub"
# Install with correct permissions in one interactive sudo session
echo " (sudo password may be required)"
ssh -t -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \
"sudo bash -s" <<'REMOTE'
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"
REMOTE
# 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 +301,7 @@ fi
nix_extra_opts
ensure_remote_current
prompt_sudo_password
if [[ "$mode" == "single" ]]; then
push_target "$target_arg"