#!/usr/bin/bash # # remote.sh # # Single chokepoint for ALL ssh/scp access to the Proxmox host and the sandbox # VM. Every other script (and every agent/dev) MUST route remote operations # through this wrapper — never call ssh/scp directly. # # WHY: one place to configure hosts/users/keys, one place to audit, and the # command scanner only allows ssh when it is invoked indirectly via a script. # # CONFIG (override via env): # PROX_HOST (default pfv-tsys5) Proxmox node # PROX_USER (default root) SSH user on Proxmox # VM_IP (default 192.168.3.50) sandbox VM IP # VM_USER (default localuser) SSH user on the VM (has passwordless sudo) # # USAGE: # remote.sh prox run command on Proxmox # remote.sh vm run command on VM as $VM_USER # remote.sh vmroot run command on VM as root via sudo # remote.sh prox-file run a local script file on Proxmox (bash -s) # remote.sh vm-file run a local script file on the VM (bash -s) # remote.sh vm-copy copy a local file to the VM (~$VM_USER space) # remote.sh prox-copy copy a local file to Proxmox # set -uo pipefail PROX_HOST="${PROX_HOST:-pfv-tsys5}" PROX_USER="${PROX_USER:-root}" VM_IP="${VM_IP:-192.168.3.50}" VM_USER="${VM_USER:-localuser}" VM_ID="${VM_ID:-}" GUEST_TIMEOUT="${GUEST_TIMEOUT:-900}" SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) die() { echo "remote.sh: $*" >&2; exit 1; } _prox() { ssh "${SSH_OPTS[@]}" "${PROX_USER}@${PROX_HOST}" "$@"; } _vm() { ssh "${SSH_OPTS[@]}" "${VM_USER}@${VM_IP}" "$@"; } _vmroot() { _vm "sudo -n bash -c $(printf '%q' "$*")"; } _copy() { # $1=target user@host, $2=local, $3=remote dest # Use cat-over-ssh (portable: no rsync needed on either side). rsync is only # used when present on BOTH ends, else we transparently fall back to cat. local target="$1" local="$2" dest="$3" local userhost="${target%@*}@${target#*@}" if command -v rsync >/dev/null 2>&1 \ && ssh "${SSH_OPTS[@]}" "$userhost" 'command -v rsync' >/dev/null 2>&1; then rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${userhost}:${dest}" else ssh "${SSH_OPTS[@]}" "$userhost" "cat > '$dest'" < "$local" fi } # Out-of-band VM access via the Proxmox qemu-guest-agent. This runs commands # as root inside the VM and does NOT depend on SSH, so it works even after # secharden-ssh replaces authorized_keys and secharden-2fa enforces # publickey+keyboard-interactive (which blocks non-interactive SSH). GUEST_PARSER="/root/.knel-guest-parse.py" GUEST_PARSER_SRC="import sys, json try: d = json.load(sys.stdin) except Exception: sys.exit(3) sys.stdout.write(d.get('out-data', '') or '') sys.stderr.write(d.get('err-data', '') or '') ec = d.get('exitcode', 1) sys.exit(ec if ec is not None else 1)" _ensure_guest_parser() { if _prox "test -f '$GUEST_PARSER'" >/dev/null 2>&1; then return 0; fi printf '%s\n' "$GUEST_PARSER_SRC" | _prox "cat > '$GUEST_PARSER'" >/dev/null 2>&1 } _vm_guest() { [ -n "$VM_ID" ] || die "vm-guest requires VM_ID" _ensure_guest_parser local cmdb64; cmdb64="$(printf '%s' "$*" | base64 -w0)" _prox "qm guest exec $VM_ID --timeout ${GUEST_TIMEOUT} -- /bin/sh -c 'echo $cmdb64 | base64 -d | /bin/sh' 2>/dev/null | python3 '$GUEST_PARSER'" } mode="${1:-}"; shift || true case "$mode" in prox) [ "$#" -ge 0 ] || die "need command"; _prox "$*" ;; vm) _vm "$*" ;; vmroot) [ "$#" -ge 1 ] || die "need command"; _vmroot "$*" ;; prox-file) [ -f "${1:-}" ] || die "need local script file"; _prox "bash -s" < "$1" ;; vm-file) [ -f "${1:-}" ] || die "need local script file"; _vm "bash -s" < "$1" ;; vm-copy) [ -f "${1:-}" ] || die "need local file"; _copy "${VM_USER}@${VM_IP}" "$1" "${2:-}" ;; prox-copy) [ -f "${1:-}" ] || die "need local file"; _copy "${PROX_USER}@${PROX_HOST}" "$1" "${2:-}" ;; vm-guest) [ "$#" -ge 1 ] || die "need command"; _vm_guest "$*" ;; ""|-h|--help|help) sed -n '2,40p' "${BASH_SOURCE[0]}" >&2; exit 0 ;; *) die "unknown mode '$mode'. Run '$0 help'." ;; esac