feat(agents): add Agent Authority policy + access bootstrap tooling

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
This commit is contained in:
2026-08-10 14:00:38 -05:00
parent c115ea8ea1
commit db6c7829ee
9 changed files with 428 additions and 0 deletions
+54
View File
@@ -1,5 +1,59 @@
# Agent Guidelines # 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 ## Quick Start
**You are an AI agent working on this project. Your first actions, in order:** **You are an AI agent working on this project. Your first actions, in order:**
+38
View File
@@ -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' </dev/null 2>&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' </dev/null 2>&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' </dev/null 2>&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' </dev/null 2>&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
+59
View File
@@ -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
+94
View File
@@ -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."
+44
View File
@@ -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" </dev/null 2>&1)
printf '%-12s %-6s %s\n' "$host" "$vid" "${out//$'\n'/ | }"
done
+38
View File
@@ -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
+32
View File
@@ -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
+54
View File
@@ -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)' </dev/null 2>&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
+15
View File
@@ -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)' </dev/null 2>&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