#!/usr/bin/env bash # ssh-helper.sh — secure SSH command execution via ControlMaster. # # Architecture: # • Hosts configured in $LARRY_HOME/.ssh-hosts.tsv as # alias \t user@host \t port # • Passwords stored at $LARRY_HOME/.ssh-creds/, mode 0600. # The password file is the single point of truth — to rotate (daily-changing # passwords) just overwrite the file with the new one and re-run 'setup'. # • sshpass reads the password via -f (file), so it never lands in argv or # environment where Larry the LLM (or other processes via /proc) could see it. # • The first 'setup' call opens a long-lived SSH ControlMaster connection # (default ControlPersist=8h). Subsequent 'exec' calls multiplex through # the master socket and need no password. # • Larry's tool layer only sees: alias, command, command_output. # Never the password. Never the user@host (unless added to the alias list). # # Subcommands: # hosts list configured hosts # add add a host to the alias list # remove remove an alias (also clears cred + socket) # pass set/update the password (hidden interactive) # setup open ControlMaster (uses stored password ONCE) # close close ControlMaster # status [alias] show open masters / cred presence # exec run command via master (returns output) # help print this help set -u set -o pipefail LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" SSH_HOSTS_FILE="$LARRY_HOME/.ssh-hosts.tsv" SSH_CREDS_DIR="$LARRY_HOME/.ssh-creds" SSH_SOCKETS_DIR="$LARRY_HOME/.ssh-sockets" SSH_CONTROL_PERSIST="${LARRY_SSH_CONTROL_PERSIST:-8h}" die() { printf 'ssh-helper: %s\n' "$*" >&2; exit 1; } warn() { printf 'ssh-helper: warn: %s\n' "$*" >&2; } ok() { printf 'ssh-helper: %s\n' "$*"; } ensure_layout() { mkdir -p "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null chmod 700 "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null || true if [ ! -f "$SSH_HOSTS_FILE" ]; then umask 077 printf 'alias\taddr\tport\n' > "$SSH_HOSTS_FILE" chmod 600 "$SSH_HOSTS_FILE" fi } # read_host_addr ALIAS → echoes "ADDR\tPORT" or empty read_host_addr() { local alias="$1" [ -f "$SSH_HOSTS_FILE" ] || { printf ''; return 1; } awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $2 "\t" $3; exit }' < "$SSH_HOSTS_FILE" } require_sshpass() { command -v sshpass >/dev/null 2>&1 \ || die "sshpass not on PATH — install it (apt install sshpass / brew install sshpass) and retry" } cmd_help() { sed -n '4,30p' "$0" } cmd_hosts() { ensure_layout if [ "$(wc -l < "$SSH_HOSTS_FILE")" -le 1 ]; then echo "no hosts configured. Add with: ssh-helper.sh add " return 0 fi printf 'alias user@host port cred master\n' printf '%s\n' '───── ───────── ──── ──── ──────' awk -F'\t' 'NR>1' "$SSH_HOSTS_FILE" | while IFS=$'\t' read -r alias addr port; do local cred_state="–" [ -f "$SSH_CREDS_DIR/$alias" ] && cred_state="✓" local master_state="–" local sock="$SSH_SOCKETS_DIR/$alias.sock" if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then master_state="open" fi printf '%-20s%-52s%-6s%-6s%s\n' "$alias" "$addr" "${port:-22}" "$cred_state" "$master_state" done } cmd_add() { local alias="${1:-}" target="${2:-}" [ -n "$alias" ] && [ -n "$target" ] || die "usage: add " [[ "$target" =~ ^[^@[:space:]]+@[^:[:space:]]+(:[0-9]+)?$ ]] \ || die "target must look like user@host or user@host:port" local addr port if [[ "$target" == *:* ]]; then addr="${target%:*}" port="${target##*:}" else addr="$target" port="22" fi ensure_layout # Reject duplicates (use 'remove' first) if awk -F'\t' -v a="$alias" 'NR>1 && $1==a { found=1; exit } END { exit !found }' "$SSH_HOSTS_FILE"; then die "alias '$alias' already exists. Use 'remove $alias' first." fi umask 077 printf '%s\t%s\t%s\n' "$alias" "$addr" "$port" >> "$SSH_HOSTS_FILE" chmod 600 "$SSH_HOSTS_FILE" ok "added $alias → $addr (port $port). Next: ssh-helper.sh pass $alias" } cmd_remove() { local alias="${1:-}" [ -n "$alias" ] || die "usage: remove " ensure_layout local tmp; tmp=$(mktemp) awk -F'\t' -v a="$alias" 'NR==1 || $1!=a' "$SSH_HOSTS_FILE" > "$tmp" && mv "$tmp" "$SSH_HOSTS_FILE" chmod 600 "$SSH_HOSTS_FILE" # Close + clean master socket and cred cmd_close "$alias" 2>/dev/null || true rm -f "$SSH_CREDS_DIR/$alias" "$SSH_SOCKETS_DIR/$alias.sock" 2>/dev/null ok "removed $alias (cred + socket cleared)" } cmd_pass() { local alias="${1:-}" [ -n "$alias" ] || die "usage: pass " local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias (run 'add' first)" ensure_layout printf 'Password for %s (input is hidden; press Enter when done): ' "$alias" >&2 local pw="" stty -echo 2>/dev/null IFS= read -r pw /dev/null echo "" >&2 [ -n "$pw" ] || die "no password entered" umask 077 # NO trailing newline — sshpass -f expects raw password as full file content printf '%s' "$pw" > "$SSH_CREDS_DIR/$alias" chmod 600 "$SSH_CREDS_DIR/$alias" ok "password saved to $SSH_CREDS_DIR/$alias (mode 0600). Next: ssh-helper.sh setup $alias" } cmd_setup() { local alias="${1:-}" [ -n "$alias" ] || die "usage: setup " local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias" local addr port addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) ensure_layout local sock="$SSH_SOCKETS_DIR/$alias.sock" if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then ok "master already open for $alias ($addr:$port)" return 0 fi local credfile="$SSH_CREDS_DIR/$alias" [ -f "$credfile" ] || die "no password set for $alias — run 'pass $alias' first" require_sshpass ok "opening ssh master for $alias ($addr:$port) — ControlPersist=$SSH_CONTROL_PERSIST..." if sshpass -f "$credfile" ssh \ -o "ControlMaster=yes" \ -o "ControlPath=$sock" \ -o "ControlPersist=$SSH_CONTROL_PERSIST" \ -o "StrictHostKeyChecking=accept-new" \ -o "ConnectTimeout=10" \ -p "$port" \ -N -f \ "$addr" 2>/tmp/larry-ssh-setup.err; then if ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then ok "✓ master open: $alias → $addr:$port (socket: $sock)" rm -f /tmp/larry-ssh-setup.err return 0 fi fi printf 'ssh-helper: setup failed. sshpass/ssh stderr:\n' >&2 cat /tmp/larry-ssh-setup.err >&2 2>/dev/null rm -f /tmp/larry-ssh-setup.err return 1 } cmd_close() { local alias="${1:-}" [ -n "$alias" ] || die "usage: close " local addr_port; addr_port=$(read_host_addr "$alias") || addr_port="" local sock="$SSH_SOCKETS_DIR/$alias.sock" if [ -S "$sock" ] && [ -n "$addr_port" ]; then local addr port addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) ssh -S "$sock" -O exit -p "$port" "$addr" 2>/dev/null || true fi rm -f "$sock" ok "closed master for $alias" } cmd_status() { ensure_layout if [ -n "${1:-}" ]; then local alias="$1" local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias" local addr port addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) local sock="$SSH_SOCKETS_DIR/$alias.sock" printf 'alias: %s\naddr: %s\nport: %s\ncred: %s\nsocket: %s\nstatus: ' \ "$alias" "$addr" "$port" \ "$([ -f "$SSH_CREDS_DIR/$alias" ] && echo present || echo missing)" \ "$sock" if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then echo "master OPEN" else echo "no master (run setup)" fi return 0 fi cmd_hosts } cmd_exec() { local alias="${1:-}" [ -n "$alias" ] || die "usage: exec " shift local cmd="$*" [ -n "$cmd" ] || die "no command given" local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias" local addr port addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) local sock="$SSH_SOCKETS_DIR/$alias.sock" if [ ! -S "$sock" ] || ! ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then die "no open master for $alias — run 'setup $alias' first" fi # Multiplexed; no password needed. ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$cmd" } case "${1:-help}" in hosts|list) shift; cmd_hosts ;; add) shift; cmd_add "$@" ;; remove|rm) shift; cmd_remove "$@" ;; pass|passwd) shift; cmd_pass "$@" ;; setup|open) shift; cmd_setup "$@" ;; close|exit) shift; cmd_close "$@" ;; status) shift; cmd_status "$@" ;; exec|run) shift; cmd_exec "$@" ;; -h|--help|help) cmd_help ;; *) die "unknown subcommand: ${1:-} (run with --help)" ;; esac