#!/bin/bash # exec-watch.sh — 15-min watchdog for the bl exec server's signature-auth path. # Signs a canary op as the exec-canary identity and POSTs it to the # local exec-constrained server. Records result in /home/super/.exec-watch-status.json # and appends to logs/exec-watch.log. Failures are pull-based (status file + # log); wire push alerting here if the fleet wants paging. # # Runs via systemd user timer exec-watch.timer (OnUnitActiveSec=15min). # Server: exec-constrained.py — named ops ONLY, no arbitrary shell; # signature namespace: exec-constrained, with {op,args,ts,nonce} envelope. # NOTE: signers file (/home/super/.exec-signers) is synced MANUALLY from the # VM's /srv/board/allowed_signers on identity renames/adds — bl cannot ssh # back to the VM, so there is no pull sync. Operator step, documented in # docs/TOKEN_POLICY.md. set -uo pipefail KEY=/home/super/.exec-canary URL=https://100.123.153.75:8444/exec STATUS=/home/super/.exec-watch-status.json LOG=/home/super/Projects/NetVM/logs/exec-watch.log ts=$(date +%s) nonce=$(python3 -c "import secrets; print(secrets.token_hex(16))") payload=$(python3 -c "import json,sys; print(json.dumps({'op':'exec.ping','args':{},'ts':int(sys.argv[1]),'nonce':sys.argv[2]}))" "$ts" "$nonce") sig=$(printf '%s' "$payload" | ssh-keygen -Y sign -f "$KEY" -n exec-constrained 2>/dev/null) if [ -z "${sig:-}" ]; then result="sign-failed" else body=$(python3 -c "import json,sys; print(json.dumps({'identity': 'exec-canary', 'payload': sys.argv[1], 'signature': sys.argv[2]}))" "$payload" "$sig") out=$(curl -sk -m 25 -X POST "$URL" -H 'Content-Type: application/json' -d "$body" 2>/dev/null) if echo "$out" | grep -q '"rc": 0'; then result="ok" else result="bad-response" fi fi now_iso=$(date -u +%FT%TZ) { python3 - "$STATUS" "$now_iso" "$result" <<'PYEOF' import json, sys status_path, now_iso, result = sys.argv[1], sys.argv[2], sys.argv[3] try: st = json.load(open(status_path)) except Exception: st = {} if result == "ok": st.update({"last_ok": now_iso, "last_fail": None, "consecutive_failures": 0, "result": "ok"}) else: st.update({"last_ok": st.get("last_ok"), "last_fail": now_iso, "consecutive_failures": st.get("consecutive_failures", 0) + 1, "result": result}) json.dump(st, open(status_path, "w")) print(f"[{now_iso}] exec-watch: {result} " f"(consecutive_failures={st['consecutive_failures']})") PYEOF } >> "$LOG" 2>&1 [ "$result" = "ok" ]