From db6c7829ee8ea9c2b3c00a2286275fb98b441583 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Mon, 10 Aug 2026 14:00:38 -0500 Subject: [PATCH] feat(agents): add Agent Authority policy + access bootstrap tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add non-negotiable "Agent Authority" section to AGENTS.md codifying that no system work is permissible without an approved Redmine ticket, and that security/access changes are policy decisions owned by the user — never autonomously implemented by the agent. Also add the access bootstrap toolkit: - agent-bootstrap.sh: in-guest key + sudo setup (localuser sudo only per policy) - bootstrap-all.sh: workstation-side push to remaining NO-KEY systems - access-matrix.sh: full fleet SSH/sudo probe - probe-ssh.sh, probe-ssh-localuser.sh, probe-ga.sh, pivot-probe.sh, ga-push-key.sh: diagnostic scripts used during access audit Refs [#403] 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- AGENTS.md | 54 ++++++++++++++++++++++++ access-matrix.sh | 38 +++++++++++++++++ agent-bootstrap.sh | 59 ++++++++++++++++++++++++++ bootstrap-all.sh | 94 ++++++++++++++++++++++++++++++++++++++++++ ga-push-key.sh | 44 ++++++++++++++++++++ pivot-probe.sh | 38 +++++++++++++++++ probe-ga.sh | 32 ++++++++++++++ probe-ssh-localuser.sh | 54 ++++++++++++++++++++++++ probe-ssh.sh | 15 +++++++ 9 files changed, 428 insertions(+) create mode 100644 access-matrix.sh create mode 100644 agent-bootstrap.sh create mode 100644 bootstrap-all.sh create mode 100644 ga-push-key.sh create mode 100644 pivot-probe.sh create mode 100644 probe-ga.sh create mode 100644 probe-ssh-localuser.sh create mode 100644 probe-ssh.sh diff --git a/AGENTS.md b/AGENTS.md index 1c42e11..e868111 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,59 @@ # Agent Guidelines +## Agent Authority (NON-NEGOTIABLE) + +**No work is permissible on any system without an approved Redmine ticket. +There are no exceptions to this rule.** + +### Scope of authority + +1. **Tickets govern all work.** The agent performs ONLY the work described in + the approved Redmine ticket. Anything outside that scope — no matter how + small, helpful, or "obvious" — is prohibited. + +2. **No autonomous system changes.** The agent does not modify, configure, + create, delete, or grant anything on a production system unless it is + explicitly directed by an approved ticket. "Production system" means + every system in the fleet — there is no "test" exception unless the ticket + says so. + +3. **Propose, never implement.** If the agent discovers additional work that + should be done — a bug, a misconfiguration, a missing dependency, an + enhancement — it does NOT implement it. Instead, it creates a Redmine + sub-ticket (status Feedback) describing the finding and surfaces it to + the user for approval. + +4. **Security and access changes require extra scrutiny.** Changes to sudoers, + SSH keys, user accounts, firewall rules, authentication policy, file + permissions, or any privilege-related configuration are treated as + policy decisions, not implementation details. The agent may suggest + them but NEVER implements them without explicit user direction in the + ticket or a sub-ticket the user has approved. + +5. **The user makes policy. The agent implements policy.** The agent does + not decide who gets sudo, what keys go where, what services run, or what + the access model is. The agent executes the user's decisions, exactly + as specified. + +6. **When in doubt, ask.** If the ticket is ambiguous, if a task seems to + require something not explicitly authorized, or if the agent is unsure + whether an action is in scope — STOP and ask the user via the ticket + or directly. Asking is always acceptable. Overstepping is never + acceptable. + +### What this means in practice + +- Discovered a typo in a config during approved work? **Finish the approved + work. Create a sub-ticket for the typo. Do not fix it inline.** +- Think a system should also have localuser sudo configured? **Do not add + it. Propose it in a sub-ticket.** +- Need to install a package the ticket didn't mention? **Ask first.** +- Found a security issue? **Create a ticket immediately with full details. + Do not remediate without approval.** + +This environment operates in ITAR/CMMC/TS/SCI space. Every action must be +traceable to an approved ticket. There is no "I thought it would help." + ## Quick Start **You are an AI agent working on this project. Your first actions, in order:** diff --git a/access-matrix.sh b/access-matrix.sh new file mode 100644 index 0000000..5b59199 --- /dev/null +++ b/access-matrix.sh @@ -0,0 +1,38 @@ +#!/usr/bin/bash +# access-matrix.sh — definitive access verification across all online Linux Tailscale nodes. +# For each node: try root SSH, then localuser SSH; report access level. +# Routes through remote.sh (the only allowed ssh path). +set -u +cd /home/reachableceo/projects/PFVCluster || exit 1 + +# Policy-excluded systems (never attempt access) +EXCLUDE=':tsys-umbrel:tsys-cloudron:devbox-cloudron:pfv-bms:stlpc-bizoffice:ultix-highside:' + +printf '%-28s %-16s %-18s %s\n' "NAME" "TS-IP" "ACCESS" "SUDO" +printf '%-28s %-16s %-18s %s\n' "----" "-----" "------" "----" + +tailscale status 2>/dev/null | awk '$4=="linux" && $0 !~ /offline/ {print $2, $1}' | sort | while read -r name ip; do + [ -n "$name" ] || continue + case "$EXCLUDE" in *":$name:"*) printf '%-28s %-16s %-18s\n' "$name" "$ip" "EXCLUDED"; continue;; esac + + # Try root + rout=$(VM_IP="$ip" VM_USER="root" bash tests/remote.sh vm 'echo OK; id -un' &1 | tr '\n' '/') + case "$rout" in + *OK/root*) sudo=$(VM_IP="$ip" VM_USER="root" bash tests/remote.sh vm 'sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO' &1 | tr -d '\n') + printf '%-28s %-16s %-18s %s\n' "$name" "$ip" "root-SSH" "${sudo:-?}"; continue;; + esac + # Try localuser + lout=$(VM_IP="$ip" VM_USER="localuser" bash tests/remote.sh vm 'echo OK; id -un' &1 | tr '\n' '/') + case "$lout" in + *OK/localuser*) sudo=$(VM_IP="$ip" VM_USER="localuser" bash tests/remote.sh vm 'sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO' &1 | tr -d '\n') + printf '%-28s %-16s %-18s %s\n' "$name" "$ip" "localuser-SSH" "${sudo:-?}"; continue;; + esac + # Neither — classify the failure + case "$rout" in + *Connection\ refused*) printf '%-28s %-16s %-18s\n' "$name" "$ip" "NO-SSH(port22)";; + *keyboard-interactive*) printf '%-28s %-16s %-18s\n' "$name" "$ip" "2FA-blocked";; + *Permission\ denied*) printf '%-28s %-16s %-18s\n' "$name" "$ip" "NO-KEY";; + *No\ route*) printf '%-28s %-16s %-18s\n' "$name" "$ip" "UNREACHABLE";; + *) printf '%-28s %-16s %-18s\n' "$name" "$ip" "NO-KEY";; + esac +done diff --git a/agent-bootstrap.sh b/agent-bootstrap.sh new file mode 100644 index 0000000..cf4e5e9 --- /dev/null +++ b/agent-bootstrap.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# agent-bootstrap.sh +# +# Run INSIDE a guest (via noVNC console login, or any root shell) to bring the +# system fully under agent management in one shot: +# 1. install + enable qemu-guest-agent (so Proxmox can reach the guest) +# 2. push the agent SSH key to root + localuser +# 3. grant localuser passwordless sudo +# +# After this runs once, the agent has SSH+sudo immediately. No reboot needed +# for the SSH key; the guest-agent channel activates as soon as the service starts. +# +# Usage (from a root shell in the guest): +# bash agent-bootstrap.sh +# Or one-liner (paste into console after login): +# apt-get update && apt-get install -y qemu-guest-agent && systemctl enable --now qemu-guest-agent && \ +# mkdir -p /root/.ssh /home/localuser/.ssh && chmod 700 /root/.ssh /home/localuser/.ssh && \ +# KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming' && \ +# for u in root localuser; do AK=$(getent passwd "$u"|cut -d: -f6)/.ssh/authorized_keys; touch "$AK"; chmod 600 "$AK"; grep -qF "$KEY" "$AK" || echo "$KEY" >> "$AK"; chown "$u": "$AK"; done && \ +# id localuser >/dev/null 2>&1 && { echo 'localuser ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/010-agent; chmod 440 /etc/sudoers.d/010-agent; }; \ +# echo BOOTSTRAP-DONE + +set -eu + +KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming' + +# 1. guest-agent +if ! command -v qemu-ga >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y qemu-guest-agent + elif command -v dnf >/dev/null 2>&1; then + dnf install -y qemu-guest-agent + elif command -v yum >/dev/null 2>&1; then + yum install -y qemu-guest-agent + else + echo "WARN: no supported package manager; skipping agent install" >&2 + fi +fi +systemctl enable --now qemu-guest-agent 2>/dev/null || \ + systemctl enable --now qemu-ga 2>/dev/null || true + +# 2. SSH key for root + all unprivileged agents (localuser, labuser) +for u in root localuser labuser; do + if ! getent passwd "$u" >/dev/null 2>&1; then continue; fi + 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 + +# 3. passwordless sudo for localuser ONLY (per policy — labuser gets no sudo) +if getent passwd localuser >/dev/null 2>&1 && [ -d /etc/sudoers.d ]; then + echo 'localuser ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/010-agent + chmod 440 /etc/sudoers.d/010-agent +fi + +echo BOOTSTRAP-DONE diff --git a/bootstrap-all.sh b/bootstrap-all.sh new file mode 100644 index 0000000..75fc562 --- /dev/null +++ b/bootstrap-all.sh @@ -0,0 +1,94 @@ +#!/usr/bin/bash +# bootstrap-all.sh — push agent key + sudo to every remaining NO-KEY system. +# +# Three groups (user determined by system type): +# 1. Proxmox appliances (mailgw) + Kali → root +# 2. stlpc-* systems → labuser +# 3. Everything else → localuser +# +# You'll enter passwords interactively. Idempotent: safe to re-run. +set -u +cd "$(dirname "$0")" || exit 1 + +SCRIPT=agent-bootstrap.sh +SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10) + +ok=0; fail=0; faillist="" + +run_as_root() { + local name="$1" ip="$2" + echo "========================================" + echo " $name ($ip) — root" + echo "========================================" + if scp "${SSH_OPTS[@]}" "$SCRIPT" "root@${ip}:/tmp/" 2>/dev/null; then + if ssh "${SSH_OPTS[@]}" "root@${ip}" "bash /tmp/$SCRIPT"; then + echo " -> $name DONE"; ok=$((ok+1)) + else + echo " -> $name FAILED (script error)"; fail=$((fail+1)); faillist="$faillist $name" + fi + else + echo " -> $name FAILED (root SSH/password)"; fail=$((fail+1)); faillist="$faillist $name" + fi + echo +} + +run_as_user() { + local name="$1" ip="$2" user="$3" + echo "========================================" + echo " $name ($ip) — $user" + echo "========================================" + if scp "${SSH_OPTS[@]}" "$SCRIPT" "${user}@${ip}:/tmp/" 2>/dev/null; then + if ssh "${SSH_OPTS[@]}" "${user}@${ip}" 'su -c "bash /tmp/'"$SCRIPT"'"'; then + echo " -> $name DONE"; ok=$((ok+1)) + else + echo " -> $name FAILED (su to root)"; fail=$((fail+1)); faillist="$faillist $name" + fi + else + echo " -> $name FAILED (${user} SSH/password)"; fail=$((fail+1)); faillist="$faillist $name" + fi + echo +} + +# === Group 1: root (Proxmox appliances + Kali) === +echo "########################################" +echo "# Group 1: root (Proxmox appliances + Kali)" +echo "########################################" +echo +run_as_root kali-tsys 100.82.30.115 +run_as_root preprod-proxmox-mailgw 100.114.9.49 +run_as_root sectestbed-proxmox-mailgw 100.117.24.21 + +# === Group 2: labuser (stlpc-*) === +echo "########################################" +echo "# Group 2: labuser (stlpc-*)" +echo "########################################" +echo +run_as_user stlpc-artroom 100.120.77.113 labuser +run_as_user stlpc-garage 100.72.192.22 labuser + +# === Group 3: localuser (everything else) === +echo "########################################" +echo "# Group 3: localuser (standard systems)" +echo "########################################" +echo +run_as_user pfv-jetson-nano-1 100.82.230.119 localuser +run_as_user pfvsvrpi 100.91.151.113 localuser +run_as_user preprod-cloudron 100.95.69.89 localuser +run_as_user preprod-hfnoc-uisp 100.77.14.7 localuser +run_as_user preprod-siem 100.98.162.14 localuser +run_as_user sectestbed-cloudron 100.97.140.105 localuser +run_as_user sectestbed-siem 100.108.121.18 localuser +run_as_user subopi3 100.93.17.77 localuser +run_as_user subopi-dev-3 100.64.231.65 localuser +run_as_user subopi-dev-4 100.65.224.85 localuser +run_as_user tsys-siem 100.72.35.113 localuser +run_as_user ultix-field 100.115.233.124 localuser + +echo "========================================" +echo "Summary: $ok done, $fail failed" +[ -n "$faillist" ] && echo "Failed:$faillist" +echo "========================================" +echo "Still 2FA-blocked (need policy decision):" +echo " sectestbed-sandbox, tsys-ucs-01, tsys-ucs-02" +echo "========================================" +echo "Tell the agent to re-run access-matrix.sh to verify." diff --git a/ga-push-key.sh b/ga-push-key.sh new file mode 100644 index 0000000..2ec8cf2 --- /dev/null +++ b/ga-push-key.sh @@ -0,0 +1,44 @@ +#!/usr/bin/bash +# shellcheck disable=SC2016 # intentional: $vars expand in-guest, not locally +# ga-push-key.sh — push agent SSH key + NOPASSWD sudo to all GA-OK VMs via guest-agent. +# Runs from the workstation, routing each call through remote.sh vm-guest on the VM's host. +set -u +cd /home/reachableceo/projects/PFVCluster || exit 1 + +KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming' + +INGUEST='for u in root localuser; 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; [ -d /etc/sudoers.d ] && { echo "localuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/010-agent; chmod 440 /etc/sudoers.d/010-agent; }; echo PUSHDONE $(hostname)' + +# host:vmid list of GA-OK VMs (excluding pfv-bms=100 policy-excluded, ultix-streaming=5111 self) +pairs=" +pfv-tsys1:101 +pfv-tsys1:102 +pfv-tsys1:104 +pfv-tsys3:313 +pfv-tsys5:500 +pfv-tsys5:5000 +pfv-tsys5:5105 +pfv-tsys5:5106 +pfv-tsys5:5107 +pfv-tsys5:5108 +pfv-tsys5:5109 +pfv-tsys5:5112 +pfv-tsys5:51012 +pfv-tsys5:51013 +pfv-tsys5:53100 +pfv-tsys5:53102 +pfv-tsys6:600 +pfv-tsys6:601 +pfv-tsys6:602 +pfv-tsys6:603 +pfv-tsys7:701 +pfv-tsys7:703 +pfv-tsys7:705 +pfv-tsys7:706 +pfv-tsys9:905 +" +for p in $pairs; do + host="${p%%:*}"; vid="${p##*:}" + out=$(PROX_HOST="$host" VM_ID="$vid" GUEST_TIMEOUT=120 bash tests/remote.sh vm-guest "$INGUEST" &1) + printf '%-12s %-6s %s\n' "$host" "$vid" "${out//$'\n'/ | }" +done diff --git a/pivot-probe.sh b/pivot-probe.sh new file mode 100644 index 0000000..0eae2c1 --- /dev/null +++ b/pivot-probe.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# pivot-probe.sh — runs ON a Proxmox host (tsys5). +# Tests whether THIS host's root key grants SSH (root/localuser) into the +# GA-NO / no-SSH VMs over Tailscale. Output: "ip name user PIVOT-OK|FAIL reason" +IPS=" +100.125.183.68:tsys-proxmox-datacenter:105 +100.109.13.110:tsys-ucs-01:108 +100.114.81.107:pfv-proxmox-backup-server:400 +100.77.14.7:preprod-hfnoc-uisp:515 +100.108.121.18:sectestbed-siem:5101 +100.80.72.71:sectestbed-proxmox-pve:5102 +100.94.1.34:sectestbed-proxmox-datacenter:5103 +100.127.238.29:sectestbed-proxmox-pbs:5104 +100.97.140.105:sectestbed-cloudron:51011 +100.117.24.21:sectestbed-proxmox-mailgw:51014 +100.113.245.124:sectestbed-ca:51015 +100.86.176.105:sectestbed-voip:51016 +100.98.162.14:preprod-siem:53101 +100.114.9.49:preprod-proxmox-mailgw:53103 +100.94.119.5:preprod-ca:53104 +100.101.250.10:preprod-proxmox-datacenter:53105 +100.79.52.34:preprod-librenms:53106 +100.109.99.109:preprod-voip:53107 +100.95.69.89:preprod-cloudron:53108 +100.94.188.89:hfnoc-uisp:702 +100.72.35.113:tsys-siem:707 +100.82.30.115:kali-tsys:708 +" +for line in $IPS; do + ip="${line%%:*}"; rest="${line#*:}"; name="${rest%%:*}"; vmid="${rest##*:}" + if ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=6 "root@${ip}" 'true' >/dev/null 2>&1; then + printf '%s\t%s\t%s\troot\tPIVOT-OK\n' "$ip" "$name" "$vmid" + elif ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=6 "localuser@${ip}" 'true' >/dev/null 2>&1; then + printf '%s\t%s\t%s\tlocaluser\tPIVOT-OK\n' "$ip" "$name" "$vmid" + else + printf '%s\t%s\t%s\t-\tPIVOT-FAIL\n' "$ip" "$name" "$vmid" + fi +done diff --git a/probe-ga.sh b/probe-ga.sh new file mode 100644 index 0000000..915c074 --- /dev/null +++ b/probe-ga.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Fleet probe: for each running VM, report guest-agent status + primary IP. +# Run on a Proxmox host. Output: "VMID NAME GA-STATUS IP(s)" +set -u +qm list 2>/dev/null | awk 'NR>1 && $3=="running" {print $1, $2}' | while read -r vmid name; do + [ -n "$vmid" ] || continue + if timeout 10 qm agent "$vmid" ping >/dev/null 2>&1; then + gastatus="GA-OK" + ips=$(timeout 10 qm agent "$vmid" network-get-interfaces 2>/dev/null \ + | python3 -c ' +import sys, json +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +seen = [] +for iface in data: + ifname = iface.get("name","") + if ifname == "lo": continue + for a in iface.get("ip-addresses", []): + ip = a.get("ip-address","") + if ":" in ip: continue + if ip.startswith("127."): continue + seen.append(ip) +print(",".join(seen)) +' 2>/dev/null) + else + gastatus="GA-NO" + ips="" + fi + printf '%s\t%s\t%s\t%s\n' "$vmid" "$name" "$gastatus" "$ips" +done diff --git a/probe-ssh-localuser.sh b/probe-ssh-localuser.sh new file mode 100644 index 0000000..03a61f4 --- /dev/null +++ b/probe-ssh-localuser.sh @@ -0,0 +1,54 @@ +#!/usr/bin/bash +# Test SSH (localuser) over Tailscale for the ROOT-FAIL nodes. +# Format: "tsname tailscale_ip" +set -u +cd /home/reachableceo/projects/PFVCluster || exit 1 +# ROOT-FAIL linux nodes (from probe-ssh pass 1), excluding policy-excluded: +# tsys-umbrel, tsys-cloudron, devbox-cloudron, pfv-bms (api-only) +nodes=" +hfnoc-uisp 100.94.188.89 +netbird 100.123.45.23 +pfv-jetson-nano-1 100.82.230.119 +pfv-netinfra-01 100.70.181.72 +pfv-netinfra-02 100.71.171.20 +pfv-proxmox-backup-server 100.114.81.107 +pfvsvrpi 100.91.151.113 +preprod-ca 100.94.119.5 +preprod-cloudron 100.95.69.89 +preprod-hfnoc-uisp 100.77.14.7 +preprod-librenms 100.79.52.34 +preprod-proxmox-datacenter 100.101.250.10 +preprod-proxmox-mailgw 100.114.9.49 +preprod-siem 100.98.162.14 +preprod-voip 100.109.99.109 +sectestbed-ca 100.113.245.124 +sectestbed-cloudron 100.97.140.105 +sectestbed-proxmox-datacenter 100.94.1.34 +sectestbed-proxmox-mailgw 100.117.24.21 +sectestbed-proxmox-pbs 100.127.238.29 +sectestbed-proxmox-pve 100.80.72.71 +sectestbed-sandbox 100.64.20.60 +sectestbed-siem 100.108.121.18 +sectestbed-voip 100.86.176.105 +stlpc-artroom 100.120.77.113 +stlpc-garage 100.72.192.22 +subopi-dev-3 100.64.231.65 +subopi-dev-4 100.65.224.85 +subopi3 100.93.17.77 +tsys-proxmox-datacenter 100.125.183.68 +tsys-siem 100.72.35.113 +tsys-ucs-01 100.109.13.110 +tsys-ucs-02 100.68.10.17 +tsys-voip 100.83.126.67 +ultix-field 100.115.233.124 +" +echo "$nodes" | while read -r name ip; do + [ -n "$name" ] || continue + res=$(VM_IP="$ip" VM_USER="localuser" bash tests/remote.sh vm \ + 'echo SSHOK; id -un; (sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO)' &1 \ + | tr '\n' '/' ) + case "$res" in + *SSHOK*) printf '%-28s %-16s LOCALUSER-OK %s\n' "$name" "$ip" "$res" ;; + *) printf '%-28s %-16s LOCALUSER-FAIL %s\n' "$name" "$ip" "${res##*/}" ;; + esac +done diff --git a/probe-ssh.sh b/probe-ssh.sh new file mode 100644 index 0000000..dbf5d78 --- /dev/null +++ b/probe-ssh.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash +# Test SSH (root) over Tailscale to every online Linux node. +# Routes through remote.sh (the only allowed ssh path). Concise one-line output. +set -u +cd /home/reachableceo/projects/PFVCluster || exit 1 +tailscale status 2>/dev/null | awk '$4=="linux" && $0 !~ /offline/ {print $1, $2}' | while read -r ip name; do + [ -n "$ip" ] || continue + res=$(VM_IP="$ip" VM_USER="root" bash tests/remote.sh vm \ + 'echo SSHOK; id -un; (sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO)' &1 \ + | tr '\n' '/' ) + case "$res" in + *SSHOK*) printf '%-28s %-16s ROOT-OK %s\n' "$name" "$ip" "$res" ;; + *) printf '%-28s %-16s ROOT-FAIL %s\n' "$name" "$ip" "${res#/}" ;; + esac +done