Newer
Older
openstack-caracal-dc-dc / scripts / site-ssh-config.sh
#!/usr/bin/env bash
# scripts/site-ssh-config.sh {render|install|remove|list} [--key PATH] [--config-dir DIR]
#
# Durable, ROOTLESS *shell* access from the vcloud jumphost to the VR1 site VMs
# (D-126, shell layer). Generates an ssh_config fragment of Host aliases with the
# right User + ProxyJump baked from as-built, so `ssh office1-netbox` (etc.) just
# works -- no remembering `-J jessea123@10.10.0.20 ubuntu@10.10.1.10`, and the SAME
# ProxyJump-through-the-containment-VM shape generalizes to the DC VMs once they
# exist. This is the FOUNDATION; scripts/site-forward.sh (the systemd port-forward)
# is a convenience that can ride these same aliases.
#
# SECRETS BOUNDARY (identical to site-forward.sh): this tool NEVER reads key
# material. The SSH key is operator-supplied via --key (a PATH written as an
# `IdentityFile`; ssh reads it, we do not) and is NEVER baked in. The generated
# fragment (which names that path) is written LOCALLY to ~/.ssh/config.d/ and is
# NOT committed -- the repo ships this generator + a <KEY-PATH> template only.
#
# `install` writes the fragment + ensures ~/.ssh/config has an `Include config.d/*`
# line; it does NOT connect (first connect is operator-run: TOFU host-key accept).
# `render`/`list` are read-only. Rootless throughout. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"   # self-locate (L9)

FRAG_NAME="vr1-sites"

# --- host table: MEASURED/as-built values only (hard rule 2). Format:
#     alias = "HOSTNAME|USER|PROXYJUMP|NOTE"
#   PROXYJUMP empty = reached directly from vcloud (same L2 as virbr2/office1-local);
#             else the alias to jump THROUGH (itself defined above it).
# As-built (docs/vr1-office1-as-built.md): voffice1 10.10.0.20 (jessea123, direct);
# office1-netbox 10.10.1.10 + office1-tailscale 10.10.1.11 (ubuntu, via voffice1's
# lxdbr0); OPNsense edge 10.10.0.1 (root, direct, tcsh). DC VMs are DEFERRED: their
# containment-VM + inner IPs are not deployed/measured yet -- add rows ONLY once
# measured (never an inferred IP); the DC form sets PROXYJUMP to the containment VM.
#
# PER-ENV KEY (D-126, operator ruling 2026-07-16): each DC env gets its OWN access
# key in its OWN creds folder (`~/vr1-dc0-creds/`, `~/vr1-dc1-creds/`) -- NOT office1's
# key. This tool applies ONE `--key` per invocation, so a DC env is installed as its
# OWN fragment: `install --env vr1-dc0 --key ~/vr1-dc0-creds/<dc0-key>` (the `--env`
# selector is added when the first DC row lands; office1 today = the default env).
# The DC keys are MINTED into those creds folders at DC deploy (pubkey -> DC cloud-init),
# per creds-manifests/<site>.manifest -- not generated speculatively here.
declare -a ALIAS_ORDER=(voffice1 office1-netbox office1-tailscale office1-opnsense)
declare -A HOSTS=(
  [voffice1]="10.10.0.20|jessea123||Office1 host (libvirt + LXD + MAAS region)"
  [office1-netbox]="10.10.1.10|ubuntu|voffice1|VR1 IPAM apex (NetBox, :8000)"
  [office1-tailscale]="10.10.1.11|ubuntu|voffice1|Office1 subnet router (D-107)"
  [office1-opnsense]="10.10.0.1|root||OPNsense edge -- root shell is tcsh, feed sh -s"
  # [vr1-dc0-<vm>]="<inner-ip>|<user>|<containment-alias>|<note>"  # MEASURE post-apply
)

die() { echo "site-ssh-config: $*" >&2; exit 1; }

usage() {
  cat <<EOF
usage: bash scripts/site-ssh-config.sh <cmd> [--key PATH] [--config-dir DIR]
  render    print the ssh_config fragment (read-only; --key optional -- omitted
            shows a <KEY-PATH> slot)
  install   write ~/.ssh/config.d/${FRAG_NAME} + ensure an Include line (needs --key;
            does NOT connect -- first connect is operator-run, accepts host keys)
  remove    delete the fragment (leaves the Include line + ~/.ssh/config)
  list      show configured host aliases
aliases: ${ALIAS_ORDER[*]}
EOF
}

render_frag() {
  local key="${1:-}" keyline
  if [ -n "$key" ]; then keyline="  IdentityFile $key"; else keyline="  IdentityFile <KEY-PATH>"; fi
  echo "# ssh_config fragment -- VR1 sites (D-126 shell layer). GENERATED by"
  echo "# scripts/site-ssh-config.sh -- do not hand-edit; re-render instead. NOT committed"
  echo "# (names an operator key path). accept-new = TOFU on first connect."
  local a hn user pj note
  for a in "${ALIAS_ORDER[@]}"; do
    IFS='|' read -r hn user pj note <<<"${HOSTS[$a]}"
    echo
    echo "Host $a    # $note"
    echo "  HostName $hn"
    echo "  User $user"
    [ -n "$pj" ] && echo "  ProxyJump $pj"
    echo "$keyline"
    echo "  IdentitiesOnly yes"
    echo "  StrictHostKeyChecking accept-new"
  done
}

cmd="${1:-}"; shift $(( $# > 0 ? 1 : 0 )) || true
KEY=""; CONFIG_DIR="$HOME/.ssh"
args=("$@")
for ((i=0; i<${#args[@]}; i++)); do
  case "${args[$i]}" in
    --key)        KEY="${args[$((i+1))]:-}"; [ -n "$KEY" ] || die "--key needs a PATH" ;;
    --config-dir) CONFIG_DIR="${args[$((i+1))]:-}"; [ -n "$CONFIG_DIR" ] || die "--config-dir needs a DIR" ;;
  esac
done

case "$cmd" in
  -h|--help|help|"") usage; [ -z "$cmd" ] && exit 1 || exit 0 ;;
  list)
    echo "configured aliases:"
    for a in "${ALIAS_ORDER[@]}"; do echo "  $a -> ${HOSTS[$a]}"; done
    ;;
  render)
    render_frag "$KEY"
    ;;
  install)
    [ -n "$KEY" ] || die "install needs --key PATH (operator-supplied; never inferred)"
    [ -r "$KEY" ] || die "key path not readable: $KEY"
    mkdir -p "$CONFIG_DIR/config.d"
    chmod 700 "$CONFIG_DIR" 2>/dev/null || true
    frag="$CONFIG_DIR/config.d/$FRAG_NAME"
    render_frag "$KEY" > "$frag"
    chmod 600 "$frag"
    cfg="$CONFIG_DIR/config"
    if [ ! -f "$cfg" ] || ! grep -qE '^\s*Include\s+config\.d/\*' "$cfg" 2>/dev/null; then
      { echo "Include config.d/*"; [ -f "$cfg" ] && cat "$cfg"; } > "$cfg.tmp"
      mv "$cfg.tmp" "$cfg"; chmod 600 "$cfg"
      echo "added 'Include config.d/*' to $cfg"
    fi
    echo "wrote $frag"
    echo "next (operator): ssh voffice1   # first connect accepts host keys (TOFU)"
    ;;
  remove)
    rm -f "$CONFIG_DIR/config.d/$FRAG_NAME"
    echo "removed $CONFIG_DIR/config.d/$FRAG_NAME (Include line + config left intact)"
    ;;
  *) die "unknown command '$cmd' (see --help)" ;;
esac