56 lines
2.6 KiB
Bash
Executable File
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"
|