fix(access): ban guest-agent as access channel — SSH only [#403]
Previous sessions used `qm guest exec` to back-door SSH keys into ~30 VMs, bypassing sshd's audit trail in an ITAR/CMMC environment. Wire the ban deep so it cannot recur: - tests/remote.sh: remove the vm-guest mode + qm-guest-exec path entirely - scripts/check-rules.sh: rule #11 fails on any `qm guest exec` / `vm-guest` pattern in code (scans .sh/.bash/.py; docs may describe the ban freely) - AGENTS.md: codify "Access-channel policy: SSH only" as non-negotiable; add "Questions" rule banning harness question tools (use questions-v1.md) - tests/vm-validation.sh: drop guest-agent key re-injection; SSH-only - proxmox/perf/scripts/perf-matrix.sh + deploy-tuned-guests.sh: convert guest-agent execution to SSH (vmroot) now that VMs have key + sudo - bootstrap-all.sh: re-target the 8 remaining locked-out systems with correct users/methods; print a console one-liner for publickey-only Pis Guest-agent remains installable/checkable for Proxmox state visibility — never as an execution or key-delivery path. 💘 Generated with Crush Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -30,8 +30,6 @@ 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)
|
||||
|
||||
@@ -55,33 +53,6 @@ _copy() {
|
||||
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 "$*" ;;
|
||||
@@ -91,7 +62,6 @@ case "$mode" in
|
||||
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
|
||||
|
||||
+10
-36
@@ -54,10 +54,9 @@ VM_ID="${VM_ID:-}"
|
||||
REPO_URL="${REPO_URL:-https://git.knownelement.com/KNEL/KNELServerBuild.git}"
|
||||
REMOTE_REPO="${REMOTE_REPO:-KNELServerBuild}"
|
||||
SNAP_PREFIX="${SNAP_PREFIX:-pre-knel-deploy}"
|
||||
ACCESS_PUBKEY="${ACCESS_PUBKEY:-$HOME/.ssh/id_ed25519.pub}"
|
||||
# Re-inject the validation pubkey after each deploy (secharden-ssh replaces
|
||||
# authorized_keys with the managed production key set, locking out the
|
||||
# bootstrap/dev key). Set RESTORE_ACCESS=0 to disable.
|
||||
# SSH-only access policy: if SSH access is lost after a deploy, the agent does
|
||||
# NOT re-inject keys via a back-channel. Set RESTORE_ACCESS=1 to have the
|
||||
# script check and warn (it cannot self-restore).
|
||||
RESTORE_ACCESS="${RESTORE_ACCESS:-1}"
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -80,7 +79,6 @@ die() { log "ERROR: $*"; exit 1; }
|
||||
vm() { bash "$REMOTE" vm "$@"; } # as $VM_USER (SSH)
|
||||
vmroot() { bash "$REMOTE" vmroot "$@"; } # as root via sudo (SSH)
|
||||
vmfile() { bash "$REMOTE" vm-file "$@"; } # run local script on VM (SSH)
|
||||
vmguest() { bash "$REMOTE" vm-guest "$@"; } # as root via guest agent (no SSH/2FA)
|
||||
prox() { bash "$REMOTE" prox "$@"; } # as $PROX_USER on Proxmox
|
||||
|
||||
require_vm_id() {
|
||||
@@ -104,45 +102,21 @@ wait_for_vm_ssh() {
|
||||
resolve_remote_repo() {
|
||||
local p
|
||||
p="$(vm "cd ~/${REMOTE_REPO} 2>/dev/null && pwd" 2>/dev/null)"
|
||||
[[ -n "$p" ]] || p="$(vmguest "cd ~${VM_USER}/${REMOTE_REPO} 2>/dev/null && pwd" 2>/dev/null)"
|
||||
printf '%s' "$p"
|
||||
}
|
||||
|
||||
# Re-inject the validation pubkey into ~$VM_USER/.ssh/authorized_keys OUT OF
|
||||
# BAND via the Proxmox guest agent (qm guest exec runs as root inside the VM
|
||||
# and does not depend on SSH). This is necessary because secharden-ssh replaces
|
||||
# authorized_keys with the managed production key set, which would otherwise
|
||||
# lock out the bootstrap key used to drive validation. No-op if SSH still works.
|
||||
# SSH-only access policy (AGENTS.md): if SSH access is lost (e.g. secharden-ssh
|
||||
# replaced authorized_keys), the agent does NOT re-inject keys via guest-agent
|
||||
# or any other back-channel. Surface the loss for manual restore.
|
||||
restore_vm_access() {
|
||||
[[ "$RESTORE_ACCESS" = "1" ]] || { log "RESTORE_ACCESS=0; skipping access restore."; return 0; }
|
||||
[[ -f "$ACCESS_PUBKEY" ]] || { log "WARN: ACCESS_PUBKEY not found ($ACCESS_PUBKEY); cannot restore access."; return 0; }
|
||||
if vm 'true' >/dev/null 2>&1; then
|
||||
log "SSH access already works; no need to restore."
|
||||
log "SSH access works; no restore needed."
|
||||
return 0
|
||||
fi
|
||||
log "SSH access lost (expected after secharden-ssh). Restoring via Proxmox guest agent..."
|
||||
local payload_b64
|
||||
# Leading newline guards against the managed authorized_keys lacking a
|
||||
# trailing newline (which would otherwise concatenate two keys into one).
|
||||
payload_b64="$(printf '\n%s' "$(cat "$ACCESS_PUBKEY")" | base64 -w0)"
|
||||
prox "qm guest exec $VM_ID -- /bin/sh -c 'echo $payload_b64 | base64 -d >> /home/${VM_USER}/.ssh/authorized_keys'" \
|
||||
>/dev/null 2>&1 || { log "WARN: guest-agent key append failed."; return 0; }
|
||||
prox "qm guest exec $VM_ID -- /bin/sh -c 'chown ${VM_USER}:${VM_USER} /home/${VM_USER}/.ssh/authorized_keys; chmod 600 /home/${VM_USER}/.ssh/authorized_keys'" \
|
||||
>/dev/null 2>&1 || true
|
||||
if vm 'true' >/dev/null 2>&1; then
|
||||
log "Access restored."
|
||||
return 0
|
||||
fi
|
||||
# If SSH still fails after re-injecting the key, 2FA is almost certainly the
|
||||
# cause (secharden-2fa enforces publickey+keyboard-interactive, which no
|
||||
# non-interactive SSH client can satisfy). That is expected and not fatal:
|
||||
# the guest agent still gives us full out-of-band access for log fetch and
|
||||
# the validation suite.
|
||||
if vmguest 'grep -q "^AuthenticationMethods" /etc/ssh/sshd_config' >/dev/null 2>&1; then
|
||||
log "SSH requires 2FA (expected after secharden-2fa); using guest agent for further access."
|
||||
else
|
||||
log "WARN: access still not working after restore and 2FA not detected. Check sshd_config."
|
||||
fi
|
||||
log "WARN: SSH access lost (e.g. secharden-ssh replaced authorized_keys)."
|
||||
log " Per SSH-only access policy (AGENTS.md), no back-channel re-inject."
|
||||
log " Restore manually: console login + re-add the bootstrap key, then re-run."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user