#!/usr/bin/env bash
# scripts/site-forward.sh {render|install|status|remove|list} <site> [--key PATH]
#
# Durable, ROOTLESS SSH access from the vcloud jumphost to a site service VM
# (D-126). Manages a per-site systemd --user unit that holds an autossh-style
# local port-forward alive across reboots, so `http://127.0.0.1:<port>` on the
# jumphost reaches a service VM that lives behind a containment/edge VM -- with
# NO root, NO persisted L3 route (which would race libvirt bringing up virbr2),
# and NO sudo at run time. It reuses the SAME transport we already use by hand
# (ssh through voffice1 / a DC containment VM), just kept up by the user manager.
#
# WHY user-systemd + a forward, not a route: the jumphost reaches office1-netbox
# (10.10.1.10) only through voffice1's lxdbr0 NAT. A `sudo ip route` is lost on
# reboot and, persisted via netplan, races libvirt (virbr2 is libvirt-managed).
# A user unit is rootless, survives reboot (with linger), and the SAME shape
# generalizes to the DCs via ProxyJump through their containment VM.
#
# SECRETS BOUNDARY: this tool NEVER reads key or token material. The SSH key is
# operator-supplied via --key (a PATH, passed to `ssh -i`; ssh reads it, we do
# not) and is NEVER baked in. The NetBox API token is a separate concern the
# importers handle on the netbox host -- this forward only carries read/UI TCP.
#
# `install` and the first connect are OPERATOR-RUN: they touch the operator's
# key + accept a host key (TOFU) + may need `loginctl enable-linger` (printed,
# not executed here). `render`/`list`/`status` are read-only. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # self-locate (L9)
# --- site table: MEASURED/as-built values only (hard rule 2). Format:
# name = "SSH_DEST|PROXYJUMP|TARGET_HOST|TARGET_PORT|LOCAL_PORT"
# SSH_DEST the host ssh connects TO and that opens the forward
# PROXYJUMP optional `-J` hop (empty = none); the DC form jumps THROUGH the
# containment VM to reach an inner service VM
# TARGET_* what SSH_DEST forwards to (host:port on SSH_DEST's near side)
# LOCAL_PORT the 127.0.0.1 port bound on the jumphost
#
# office1-netbox: voffice1 (10.10.0.20, as-built) is the lxdbr0 gateway, so it
# forwards straight to 10.10.1.10:8000 (measured: HTTP 302). No ProxyJump.
# DC inner VMs are DEFERRED: their containment-VM + inner IPs are not measured
# yet -- add a row ONLY once measured (never an inferred IP). See the template.
declare -A SITES=(
[office1-netbox]="jessea123@10.10.0.20||10.10.1.10|8000|8000"
# [vr1-dc0-<svc>]="<inner-vm-user@ip>|<containment-vm-user@ip>|<inner-ip>|<port>|<localport>" # MEASURE first
)
UNIT_PREFIX="site-forward-"
die() { echo "site-forward: $*" >&2; exit 1; }
usage() {
cat <<EOF
usage: bash scripts/site-forward.sh <cmd> <site> [--key PATH]
render <site> [--key PATH] print the systemd --user unit (read-only; --key
optional here -- omitted shows a <KEY-PATH> slot)
install <site> --key PATH write + enable --now the user unit (OPERATOR-RUN:
needs the key + first-connect host-key accept)
status <site> systemctl --user status for the unit
remove <site> disable + delete the user unit (rootless)
list show configured sites
sites: ${!SITES[*]}
EOF
}
lookup() {
local site="$1"
[ -n "${SITES[$site]+x}" ] || die "unknown site '$site' (have: ${!SITES[*]})"
IFS='|' read -r DEST PJUMP THOST TPORT LPORT <<<"${SITES[$site]}"
[ -n "$DEST" ] && [ -n "$THOST" ] && [ -n "$TPORT" ] && [ -n "$LPORT" ] \
|| die "site '$site' row is malformed"
}
render_unit() {
# $1 site, $2 key-path-or-empty
local site="$1" key="${2:-}"
lookup "$site"
local keyexpr pj=""
if [ -n "$key" ]; then keyexpr="-i $key"; else keyexpr="-i <KEY-PATH>"; fi
[ -n "$PJUMP" ] && pj="-J $PJUMP "
# MEASURE ssh at render time (hard rule 2 -- never bake an unmeasured path); the
# unit renders on the host it runs on, so this is the runtime path too.
local sshbin; sshbin="$(command -v ssh)" || die "no ssh binary on PATH"
# No network-online ordering: the --user manager does NOT provide that target,
# so it would be inert. Resilience is Restart=always + ExitOnForwardFailure --
# ssh simply fails until the network + jump host are up, and systemd retries.
# -N no shell, -T no tty, BatchMode (never prompt), ExitOnForwardFailure so a
# dead forward exits (Restart brings it back) instead of a live-but-useless
# tunnel, keepalives to reap half-open, accept-new for first-run TOFU, bind
# 127.0.0.1 only (never 0.0.0.0).
cat <<EOF
[Unit]
Description=D-126 durable SSH forward to ${site} (127.0.0.1:${LPORT} -> ${THOST}:${TPORT})
[Service]
Type=simple
ExecStart=${sshbin} -N -T ${pj}${keyexpr} \\
-o BatchMode=yes -o ExitOnForwardFailure=yes \\
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \\
-o StrictHostKeyChecking=accept-new \\
-L 127.0.0.1:${LPORT}:${THOST}:${TPORT} ${DEST}
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
EOF
}
cmd="${1:-}"; site="${2:-}"; KEY=""
shift $(( $# > 0 ? 1 : 0 )) || true
# parse --key from the remaining args
args=("$@")
for ((i=0; i<${#args[@]}; i++)); do
case "${args[$i]}" in
--key) KEY="${args[$((i+1))]:-}"; [ -n "$KEY" ] || die "--key needs a PATH" ;;
esac
done
case "$cmd" in
-h|--help|help|"") usage; [ -z "$cmd" ] && exit 1 || exit 0 ;;
list) echo "configured sites:"; for s in "${!SITES[@]}"; do echo " $s -> ${SITES[$s]}"; done ;;
render)
[ -n "$site" ] || die "render needs a <site>"
render_unit "$site" "$KEY"
;;
install)
[ -n "$site" ] || die "install needs a <site>"
[ -n "$KEY" ] || die "install needs --key PATH (operator-supplied; never inferred)"
[ -r "$KEY" ] || die "key path not readable: $KEY"
lookup "$site"
udir="$HOME/.config/systemd/user"
unit="${UNIT_PREFIX}${site}.service"
mkdir -p "$udir"
render_unit "$site" "$KEY" > "$udir/$unit"
echo "wrote $udir/$unit"
systemctl --user daemon-reload
systemctl --user enable --now "$unit"
systemctl --user --no-pager status "$unit" | head -6 || true
if [ "$(loginctl show-user "$USER" -p Linger --value 2>/dev/null)" != "yes" ]; then
echo
echo "NOTE: linger is OFF -- the unit will NOT start at boot until you run"
echo " (once, may need sudo): loginctl enable-linger $USER"
fi
;;
status)
[ -n "$site" ] || die "status needs a <site>"
lookup "$site"
systemctl --user --no-pager status "${UNIT_PREFIX}${site}.service" || true
;;
remove)
[ -n "$site" ] || die "remove needs a <site>"
lookup "$site"
unit="${UNIT_PREFIX}${site}.service"
systemctl --user disable --now "$unit" 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/$unit"
systemctl --user daemon-reload
echo "removed $unit"
;;
*) die "unknown command '$cmd' (see --help)" ;;
esac