#!/bin/bash # exec-sign.sh — call the bl exec-constrained server with SSH-signature auth. # No bearer token, no secret crosses the wire: you sign the request envelope # with your registered fleet key and the server verifies it against the # signers file. A signature is not a secret, so this never trips # secret-handling guardrails. # # Server: exec-constrained.py — named ops ONLY, no arbitrary shell. # Signature namespace: exec-constrained # Envelope: {"op","args","ts","nonce"} — op must be in the server allowlist: # dm.send, dm.thread, dm.read, job.run, chat.messages, chat.send, # health.check, exec.ping # Nonce: 16+ hex chars, replay-protected server-side. ts: unix epoch, ±300s skew. # # Usage: exec-sign.sh '' [identity] [keyfile] [url] # op a named op, e.g. exec.ping # args-json JSON object of the op's arguments, e.g. '{}' # identity defaults to operator-646 (must be a principal in the # server's signers file) # keyfile defaults to ~/.ssh/id_frontdoor # url defaults to https://exec.muse-dev.online/exec (cloudflared). # NOTE: the old VM Caddy /exec route to bl:8443 died with # exec-server.py — do not point this at the sslip.io URL. set -euo pipefail OP="${1:?usage: exec-sign.sh '' [identity] [keyfile] [url]}" # NOTE: do NOT write this as ${2:-{}} — bash matches the first } as the # expansion's close brace and appends a literal } when $2 is set. ARGS_JSON="${2-}" if [ -z "$ARGS_JSON" ]; then ARGS_JSON='{}'; fi IDENTITY="${3:-operator-646}" KEY="${4:-$HOME/.ssh/id_frontdoor}" URL="${5:-https://exec.muse-dev.online/exec}" TS=$(date +%s) NONCE=$(python3 -c "import secrets; print(secrets.token_hex(16))") PAYLOAD=$(python3 -c " import json, sys op, args_json, ts, nonce = sys.argv[1:5] args = json.loads(args_json) if not isinstance(args, dict): raise SystemExit('args-json must be a JSON object') print(json.dumps({'op': op, 'args': args, 'ts': int(ts), 'nonce': nonce})) " "$OP" "$ARGS_JSON" "$TS" "$NONCE") SIG=$(printf '%s' "$PAYLOAD" | ssh-keygen -Y sign -f "$KEY" -n exec-constrained) BODY=$(python3 -c " import json, sys ident, payload, sig = sys.argv[1:4] print(json.dumps({'identity': ident, 'payload': payload, 'signature': sig})) " "$IDENTITY" "$PAYLOAD" "$SIG") # Cloudflare Bot Fight Mode blocks python-urllib POSTs (error 1010): # use curl with a browser User-Agent instead. curl -sS -X POST "$URL" \ -H 'Content-Type: application/json' \ -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \ --data "$BODY"