Files
box/bin/exec-sign.sh

56 lines
2.6 KiB
Bash
Executable File

#!/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 <op> '<args-json>' [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 <op> '<args-json>' [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"