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:
@@ -41,6 +41,29 @@ There are no exceptions to this rule.**
|
||||
or directly. Asking is always acceptable. Overstepping is never
|
||||
acceptable.
|
||||
|
||||
### Access-channel policy: SSH only (NON-NEGOTIABLE)
|
||||
|
||||
**The qemu guest-agent is NEVER an access, execution, or key-delivery
|
||||
channel.** SSH (`sshd`) is the only approved remote access path. Every
|
||||
command must flow through sshd so it is captured by the standard
|
||||
auth/logging/audit infrastructure. This is an ITAR/CMMC/TS/SCI
|
||||
environment — there is no back-door exception, ever.
|
||||
|
||||
- **Forbidden:** `qm guest exec` (runs arbitrary commands inside a guest
|
||||
over an unaudited channel) and any wrapper around it (e.g. a `vm-guest`
|
||||
mode). This includes using guest-agent to *deliver* an SSH key, even if
|
||||
the resulting SSH login is itself audited — the delivery bypassed audit.
|
||||
- **Allowed:** installing or checking qemu-guest-agent for its intended
|
||||
purpose — letting Proxmox see guest state (`qm guest cmd <id> ping`,
|
||||
`agent: 1` config, `apt install qemu-guest-agent`). Visibility only;
|
||||
never execution.
|
||||
- **Enforced mechanically:** `scripts/check-rules.sh` rule #11 fails on
|
||||
any `qm guest exec` / `vm-guest` pattern in code. `tests/remote.sh` has
|
||||
no guest-exec mode.
|
||||
- **If a system is locked out** (no SSH key, no guest-exec path): surface
|
||||
it to the user. Do NOT improvise an alternate back-channel. The user
|
||||
authorizes the unblock method (console login, credential, etc.).
|
||||
|
||||
### What this means in practice
|
||||
|
||||
- Discovered a typo in a config during approved work? **Finish the approved
|
||||
@@ -92,6 +115,16 @@ for a full audit or `--fast` for pre-commit speed. Bypass with `--no-verify`
|
||||
- **Use sub-agents as subcontractors:** scoped spec in, distilled deliverable out.
|
||||
Never read 10+ files sequentially; batch into agent calls.
|
||||
|
||||
## Questions (NON-NEGOTIABLE)
|
||||
|
||||
**NEVER use a harness "question"/"ask user" tool** (structured prompts,
|
||||
modal forms, tabbed questions). Banned across every project, every harness.
|
||||
They are not portable, not version-controlled, and bypass the git record.
|
||||
**All questions go in [questions-v1.md](questions-v1.md)** — write the
|
||||
question; the human edits the answer inline in the same file. Version up
|
||||
per round. Synthesize resolved Q&A into Discourse (decisions) and Redmine
|
||||
(work items). See `BASELINE-PROMPT.md` §10.
|
||||
|
||||
## Documentation policy (IMPORTANT)
|
||||
|
||||
**Discourse is the canonical source of truth for all knowledge documentation.**
|
||||
|
||||
+4
-1
@@ -5,4 +5,7 @@ A commit is blocked while any task below remains unchecked.
|
||||
|
||||
## Current Tasks
|
||||
|
||||
- [x] Create e1000e offload fix script (repo only, not deployed yet)
|
||||
- [x] Wire guest-agent-as-access ban: strip vm-guest from remote.sh, add check-rules.sh rule #11, codify in AGENTS.md [#403]
|
||||
- [x] Convert vm-validation.sh + perf-matrix.sh + deploy-tuned-guests.sh from guest-agent to SSH
|
||||
- [x] Rewrite bootstrap-all.sh for remaining 8 locked-out systems (user runs interactively)
|
||||
- [x] Ban harness question-tool in meta + project AGENTS.md (use questions-v1.md only)
|
||||
|
||||
+39
-30
@@ -1,14 +1,17 @@
|
||||
#!/usr/bin/bash
|
||||
# bootstrap-all.sh — push agent key + sudo to every remaining NO-KEY system.
|
||||
# bootstrap-all.sh — push agent SSH key + passwordless sudo to remaining systems.
|
||||
#
|
||||
# SSH is the ONLY approved access channel (see AGENTS.md "Access-channel
|
||||
# policy: SSH only"). This script reaches systems that still allow password
|
||||
# auth over sshd. Systems that reject password auth (publickey-only) cannot
|
||||
# be reached this way — see the CONSOLE-ONLY section printed at the end.
|
||||
#
|
||||
# Two escalation methods:
|
||||
# sudo → Ubuntu systems (no root password, localuser has sudo)
|
||||
# su → Debian systems (root has a password)
|
||||
# sudo → Ubuntu-style systems (no root pw; localuser has sudo)
|
||||
# su → Debian-style systems (root has a password)
|
||||
#
|
||||
# Passes AGENT_USER so agent-bootstrap.sh pushes the key + sudo to the
|
||||
# correct unprivileged user per system type.
|
||||
#
|
||||
# You'll enter passwords interactively. Idempotent: safe to re-run.
|
||||
# Passes AGENT_USER so agent-bootstrap.sh targets the correct unprivileged
|
||||
# user. You enter passwords interactively. Idempotent: safe to re-run.
|
||||
set -u
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
@@ -39,35 +42,41 @@ run_with_su() {
|
||||
echo
|
||||
}
|
||||
|
||||
# === Ubuntu: localuser + sudo ===
|
||||
echo "### Ubuntu (localuser + sudo) ###"
|
||||
# === localuser + sudo (password auth confirmed on) ===
|
||||
echo "### localuser + sudo ###"
|
||||
echo
|
||||
run_with_sudo tsys-siem 100.72.35.113 localuser
|
||||
run_with_sudo sectestbed-cloudron 100.97.140.105 localuser
|
||||
run_with_sudo kali-tsys 100.82.30.115 localuser
|
||||
run_with_sudo pfvsvrpi 100.91.151.113 localuser
|
||||
run_with_sudo tsys-siem 100.72.35.113 localuser
|
||||
run_with_sudo sectestbed-cloudron 100.97.140.105 localuser
|
||||
|
||||
# === Debian: subodev + su ===
|
||||
echo "### Debian: subodev + su ###"
|
||||
# === subodev/ultixfield + su (password auth confirmed on) ===
|
||||
echo "### subodev/ultixfield + su ###"
|
||||
echo
|
||||
run_with_su subopi3 100.93.17.77 subodev subodev
|
||||
run_with_su subopi-dev-3 100.64.231.65 subodev subodev
|
||||
run_with_su subopi-dev-4 100.65.224.85 subodev subodev
|
||||
|
||||
# === Debian: ultixfield + su ===
|
||||
echo "### Debian: ultixfield + su ###"
|
||||
echo
|
||||
run_with_su ultix-field 100.115.233.124 ultixfield ultixfield
|
||||
run_with_su subopi-dev-3 100.64.231.65 subodev subodev
|
||||
run_with_su ultix-field 100.115.233.124 ultixfield ultixfield
|
||||
|
||||
echo "========================================"
|
||||
echo "Done. Tell the agent to re-run access-matrix.sh to verify."
|
||||
echo "Done. Re-run access-matrix.sh to verify."
|
||||
echo "========================================"
|
||||
echo "Blocked — need console work (separate ticket):"
|
||||
echo " stlpc-3dscanner — bring online + rename [#417]"
|
||||
echo
|
||||
echo "CONSOLE-ONLY — sshd rejects password auth (publickey-only)."
|
||||
echo "Log in at the physical console (as root, or user then su/sudo)"
|
||||
echo "and paste this ONE line:"
|
||||
echo
|
||||
cat <<'ONELINER'
|
||||
KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming'; for u in root localuser subodev ultixfield; do getent passwd "$u">/dev/null||continue; H=$(getent passwd "$u"|cut -d: -f6); mkdir -p "$H/.ssh"; chmod 700 "$H/.ssh"; AK="$H/.ssh/authorized_keys"; touch "$AK"; chmod 600 "$AK"; grep -qF "$KEY" "$AK"||echo "$KEY">>"$AK"; chown -R "$u": "$H/.ssh"; done; for u in localuser subodev ultixfield; do getent passwd "$u">/dev/null&&[ -d /etc/sudoers.d ]&&{ echo "$u ALL=(ALL) NOPASSWD:ALL">/etc/sudoers.d/010-agent; chmod 440 /etc/sudoers.d/010-agent; }; done; echo DONE
|
||||
ONELINER
|
||||
echo
|
||||
echo " pfvsvrpi (localuser) — Raspberry Pi"
|
||||
echo " subopi3 (subodev) — Raspberry Pi"
|
||||
echo " subopi-dev-4 (subodev) — Raspberry Pi"
|
||||
echo "========================================"
|
||||
echo "Excluded (managed via API, not SSH):"
|
||||
echo " tsys-ucs-01, tsys-ucs-02"
|
||||
echo "Deferred (separate ticket):"
|
||||
echo " stlp-3dscanner — rename + bring online first [#417]"
|
||||
echo "========================================"
|
||||
echo "2FA by design (leave alone):"
|
||||
echo " sectestbed-sandbox"
|
||||
echo "By design (leave alone):"
|
||||
echo " sectestbed-sandbox — 2FA enforced"
|
||||
echo "========================================"
|
||||
echo "Excluded by policy (no SSH access):"
|
||||
echo " pfv-bms (API), tsys-cloudron (prod revenue),"
|
||||
echo " tsys-ucs-01/02 (API-managed), tsys-umbrel (treasury)"
|
||||
echo "========================================"
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
#!/usr/bin/bash
|
||||
# Deploy tuned inside VMs via qm guest exec (no SSH needed)
|
||||
# Uses Proxmox qemu-guest-agent for out-of-band command execution
|
||||
# Deploy tuned inside VMs over SSH (sshd is the only approved access channel —
|
||||
# see AGENTS.md "Access-channel policy: SSH only"). Formerly used the
|
||||
# qemu guest-agent channel; converted to SSH now that all VMs have key + sudo.
|
||||
set -uo pipefail
|
||||
|
||||
# Profile mapping: most VMs get throughput-performance, k8s/network-sensitive get network-latency
|
||||
PROFILE="${1:-throughput-performance}"
|
||||
REMOTE_SH="${REMOTE_SH:-$(cd "$(dirname "$0")/../../.." && pwd)/tests/remote.sh}"
|
||||
VM_USER="${VM_USER:-localuser}"
|
||||
|
||||
deploy_vm() {
|
||||
local host="$1" vmid="$2" name="$3"
|
||||
# name is the Tailscale hostname (SSH target); vmid retained for reference.
|
||||
echo -n " VMID $vmid ($name) on $host: "
|
||||
PROX_HOST=$host bash tests/remote.sh prox "
|
||||
# Check if guest agent is available
|
||||
if ! qm guest cmd \$vmid ping >/dev/null 2>&1; then
|
||||
echo 'NO GUEST AGENT - skip'
|
||||
exit 0
|
||||
fi
|
||||
# Install tuned
|
||||
result=\$(qm guest exec \$vmid --timeout 120 -- /bin/sh -c 'DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tuned 2>/dev/null; tuned-adm profile $PROFILE 2>/dev/null; systemctl enable tuned 2>/dev/null; systemctl restart tuned 2>/dev/null; tuned-adm active 2>/dev/null' 2>/dev/null)
|
||||
if echo \"\$result\" | grep -q 'Current active'; then
|
||||
echo \"\$result\" | grep -o 'Current active.*' | head -1
|
||||
else
|
||||
echo 'INSTALL FAILED (apt issue or no network)'
|
||||
fi
|
||||
" 2>&1
|
||||
local result
|
||||
result=$(VM_IP="$name" VM_USER="$VM_USER" bash "$REMOTE_SH" vmroot \
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tuned 2>/dev/null; \
|
||||
tuned-adm profile $PROFILE 2>/dev/null; \
|
||||
systemctl enable --now tuned 2>/dev/null; \
|
||||
tuned-adm active 2>/dev/null" </dev/null 2>&1)
|
||||
if echo "$result" | grep -q 'Current active'; then
|
||||
echo "$result" | grep -o 'Current active.*' | head -1
|
||||
elif echo "$result" | grep -qi 'permission denied\|no route\|timed out'; then
|
||||
echo "SSH FAILED (no key/no sudo) — run bootstrap-all.sh first"
|
||||
else
|
||||
echo "INSTALL FAILED (apt issue or no network)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "============================================"
|
||||
echo " Deploying tuned ($PROFILE) to VMs"
|
||||
echo " Deploying tuned ($PROFILE) to VMs via SSH"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
@@ -55,7 +59,7 @@ deploy_vm pfv-tsys6 604 tsys-proxmox-mailgw-01
|
||||
echo "--- pfv-tsys7 ---"
|
||||
deploy_vm pfv-tsys7 701 pfv-k8s-wnode-tsys7
|
||||
deploy_vm pfv-tsys7 702 hfnoc-uisp
|
||||
deploy_vm pfv-tsys7 703 rr-middleware-01
|
||||
deploy_vm pfv-tsys7 703 pfv-rr-middleware-01
|
||||
deploy_vm pfv-tsys7 705 pfv-k8s-cnode2
|
||||
deploy_vm pfv-tsys7 706 kali-rd
|
||||
deploy_vm pfv-tsys7 707 tsys-siem
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
# Prerequisites:
|
||||
# - iperf3 installed on all hosts (systemd service: iperf3-server)
|
||||
# - iperf3 installed inside guest VMs
|
||||
# - Guest agent enabled on VMs for remote command execution
|
||||
# - SSH key + passwordless sudo on all guest VMs (remote.sh; sshd is the
|
||||
# only approved access channel — see AGENTS.md)
|
||||
#
|
||||
# Usage:
|
||||
# bash perf-matrix.sh # run all tests
|
||||
@@ -41,19 +42,19 @@ DATANET_IP[pfv-tsys9]="10.100.100.9"
|
||||
|
||||
HOSTS="pfv-tsys1 pfv-tsys3 pfv-tsys4 pfv-tsys5 pfv-tsys6 pfv-tsys7 pfv-tsys9"
|
||||
|
||||
# --- k8s/ultix VM guest-agent targets ---
|
||||
# Format: prox_host:vmid:datanet_ip:label
|
||||
# --- k8s/ultix VM targets (SSH for control; datanet_ip for iperf traffic) ---
|
||||
# Format: prox_host:vmid:datanet_ip:ssh_host:label
|
||||
GUEST_TARGETS="
|
||||
pfv-tsys1:102:10.100.100.10:cnode1
|
||||
pfv-tsys7:705:10.100.100.11:cnode2
|
||||
pfv-tsys6:603:10.100.100.12:cnode3
|
||||
pfv-tsys3:313:10.100.100.13:wnode-tsys3
|
||||
pfv-tsys5:500:10.100.100.14:wnode-tsys5
|
||||
pfv-tsys6:601:10.100.100.15:wnode-tsys6
|
||||
pfv-tsys7:701:10.100.100.16:wnode-tsys7
|
||||
pfv-tsys9:905:10.100.100.17:wnode-tsys9
|
||||
pfv-tsys5:5111:10.100.100.18:ultix-streaming
|
||||
pfv-tsys5:5112:10.100.100.19:ultix-offstage
|
||||
pfv-tsys1:102:10.100.100.10:pfv-k8s-cnode1:cnode1
|
||||
pfv-tsys7:705:10.100.100.11:pfv-k8s-cnode2:cnode2
|
||||
pfv-tsys6:603:10.100.100.12:pfv-k8s-cnode3:cnode3
|
||||
pfv-tsys3:313:10.100.100.13:pfv-k8s-wnode-tsys3:wnode-tsys3
|
||||
pfv-tsys5:500:10.100.100.14:pfv-k8s-wnode-tsys5:wnode-tsys5
|
||||
pfv-tsys6:601:10.100.100.15:pfv-k8s-wnode-tsys6:wnode-tsys6
|
||||
pfv-tsys7:701:10.100.100.16:pfv-k8s-wnode-tsys7:wnode-tsys7
|
||||
pfv-tsys9:905:10.100.100.17:pfv-k8s-wnode-tsys9:wnode-tsys9
|
||||
pfv-tsys5:5111:10.100.100.18:ultix-streaming:ultix-streaming
|
||||
pfv-tsys5:5112:10.100.100.19:ultix-offstage:ultix-offstage
|
||||
"
|
||||
|
||||
DURATION="${DURATION:-3}" # seconds per iperf3 test
|
||||
@@ -134,14 +135,13 @@ test_guests() {
|
||||
# Start iperf3 server on the first guest (cnode1)
|
||||
local server_entry
|
||||
server_entry=$(echo "$GUEST_TARGETS" | head -2 | tail -1)
|
||||
local s_host s_vid s_ip s_label
|
||||
s_host=$(echo "$server_entry" | cut -d: -f1)
|
||||
s_vid=$(echo "$server_entry" | cut -d: -f2)
|
||||
local s_ip s_ssh s_label
|
||||
s_ip=$(echo "$server_entry" | cut -d: -f3)
|
||||
s_label=$(echo "$server_entry" | cut -d: -f4)
|
||||
s_ssh=$(echo "$server_entry" | cut -d: -f4)
|
||||
s_label=$(echo "$server_entry" | cut -d: -f5)
|
||||
|
||||
echo " Starting iperf3 server on $s_label ($s_ip)..."
|
||||
PROX_HOST="$s_host" VM_ID="$s_vid" bash "$REMOTE_SH" vm-guest \
|
||||
VM_IP="$s_ssh" bash "$REMOTE_SH" vmroot \
|
||||
'pkill iperf3 2>/dev/null; iperf3 -s -D' >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
@@ -150,15 +150,14 @@ test_guests() {
|
||||
|
||||
while read -r entry; do
|
||||
[ -z "$entry" ] && continue
|
||||
local c_host c_vid c_ip c_label
|
||||
c_host=$(echo "$entry" | cut -d: -f1)
|
||||
c_vid=$(echo "$entry" | cut -d: -f2)
|
||||
local c_ip c_ssh c_label
|
||||
c_ip=$(echo "$entry" | cut -d: -f3)
|
||||
c_label=$(echo "$entry" | cut -d: -f4)
|
||||
c_ssh=$(echo "$entry" | cut -d: -f4)
|
||||
c_label=$(echo "$entry" | cut -d: -f5)
|
||||
[ "$c_ip" = "$s_ip" ] && continue
|
||||
|
||||
local result
|
||||
result=$(PROX_HOST="$c_host" VM_ID="$c_vid" bash "$REMOTE_SH" vm-guest \
|
||||
result=$(VM_IP="$c_ssh" bash "$REMOTE_SH" vmroot \
|
||||
"iperf3 -c $s_ip -t $DURATION -P $STREAMS -f m 2>&1" </dev/null \
|
||||
| awk '/SUM.*receiver/{printf "%.0f", $6}')
|
||||
if [ -n "$result" ]; then
|
||||
@@ -171,7 +170,7 @@ test_guests() {
|
||||
done <<< "$GUEST_TARGETS"
|
||||
|
||||
# Cleanup
|
||||
PROX_HOST="$s_host" VM_ID="$s_vid" bash "$REMOTE_SH" vm-guest \
|
||||
VM_IP="$s_ssh" bash "$REMOTE_SH" vmroot \
|
||||
'pkill iperf3' >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -242,4 +242,25 @@ if [ "$RULE_FAST" = false ] && [ -x scripts/test.sh ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 11. Access-channel policy — qemu guest-agent must NEVER be used as an
|
||||
# execution or key-delivery channel. SSH (sshd) is the ONLY approved
|
||||
# remote access path; all commands must be logged through standard
|
||||
# auth/audit infrastructure. ITAR/CMMC environment — non-negotiable.
|
||||
# Allowed: installing/checking qemu-guest-agent for Proxmox state
|
||||
# visibility. Forbidden: `qm guest exec` + any `vm-guest` wrapper.
|
||||
# Scans CODE only (not .md) so docs may describe the ban.
|
||||
# ----------------------------------------------------------------------------
|
||||
$RULE_VERBOSE && log_step "Access-channel policy (no guest-agent exec)"
|
||||
GA_HITS="$(grep -rnE 'qm guest exec|vm-guest|_vm_guest' \
|
||||
--include='*.sh' --include='*.bash' --include='*.py' \
|
||||
. 2>/dev/null | grep -vE 'scripts/check-rules\.sh|/archive/' || true)"
|
||||
if [ -z "$GA_HITS" ]; then
|
||||
check "No guest-agent exec / vm-guest access patterns in code" "pass"
|
||||
else
|
||||
GA_COUNT="$(printf '%s\n' "$GA_HITS" | grep -c . || true)"
|
||||
$RULE_VERBOSE && printf '%s\n' "$GA_HITS" | sed 's/^/ /'
|
||||
check "${GA_COUNT} guest-agent exec / vm-guest reference(s) — SSH-only access policy (AGENTS.md)" "fail"
|
||||
fi
|
||||
|
||||
print_summary_and_exit
|
||||
|
||||
@@ -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