test: add live coordinator auth smoke

This commit is contained in:
Peter Steinberger 2026-05-02 03:25:12 +01:00
parent d1648f5551
commit 651c014270
No known key found for this signature in database
6 changed files with 157 additions and 6 deletions

View File

@ -17,6 +17,7 @@ Crabbox 0.3.0 adds the first trusted-operator image lifecycle for AWS runners: o
- Added `docs/commands/image.md` and linked the image command from the CLI docs, command index, docs site, and source map.
- Added `npm run docs:check` with internal Markdown link validation plus docs-site generation, and wired it into CI.
- Added `scripts/live-smoke.sh` for opt-in AWS, Hetzner, and Blacksmith Testbox live smoke coverage from a real repository checkout.
- Added `scripts/live-auth-smoke.sh` for opt-in live proof that shared tokens cannot call admin routes, admin tokens can, Access edge auth works, and raw Access identity headers are ignored.
### Changed

View File

@ -118,12 +118,14 @@ Useful proof commands:
curl -i https://crabbox-access.openclaw.ai/v1/health
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox doctor
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox whoami
CRABBOX_LIVE=1 CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-auth-smoke.sh
CRABBOX_LIVE=1 CRABBOX_LIVE_PROVIDERS=aws CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-smoke.sh
```
The first command should fail at Cloudflare Access without credentials. The CLI
commands should pass when local Access credentials and Crabbox broker auth are
configured.
The first command should fail at Cloudflare Access without credentials. The auth
smoke should pass when local Access credentials, shared broker auth, and admin
broker auth are configured. The provider smoke additionally proves the same
route can lease, run, and release a real machine.
Owner selection for bearer-token requests:

View File

@ -118,12 +118,16 @@ Use the protected route when testing the Cloudflare Access layer:
```sh
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox doctor
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox whoami
CRABBOX_LIVE=1 CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-auth-smoke.sh
CRABBOX_LIVE=1 CRABBOX_LIVE_PROVIDERS=aws CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-smoke.sh
```
`doctor` should report `access=service-token`. A raw request without Access
headers to `https://crabbox-access.openclaw.ai/v1/health` should return a
Cloudflare Access `403`.
`doctor` should report `access=service-token`. `scripts/live-auth-smoke.sh`
proves the auth boundary without leasing a machine: no Access headers are denied
at the edge, shared-token user auth works, raw Access identity spoofing is
ignored, shared-token admin calls fail, and admin-token admin calls pass. A raw
request without Access headers to `https://crabbox-access.openclaw.ai/v1/health`
should return a Cloudflare Access `403`.
Use `crabbox config show` to confirm which URL and provider the CLI will use:

View File

@ -77,3 +77,4 @@ Bootstrap is intentionally tiny: OpenSSH, CA certificates, curl, Git, rsync, jq,
- GoReleaser archives and Homebrew formula config: `.goreleaser.yaml`
- Docs link check, site builder, and Pages deployment: `scripts/check-docs-links.mjs`, `scripts/build-docs-site.mjs`, `.github/workflows/pages.yml`
- Live provider smoke coverage: `scripts/live-smoke.sh`
- Live coordinator auth smoke coverage: `scripts/live-auth-smoke.sh`

View File

@ -16,6 +16,7 @@
"scripts": {
"check": "node --check index.js && node --test index.test.js",
"docs:check": "node scripts/check-docs-links.mjs && node scripts/build-docs-site.mjs",
"live:auth": "CRABBOX_LIVE=1 scripts/live-auth-smoke.sh",
"test": "node --test index.test.js"
},
"files": [

142
scripts/live-auth-smoke.sh Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${CRABBOX_LIVE:-}" != "1" ]]; then
echo "set CRABBOX_LIVE=1 to run live coordinator auth smoke tests" >&2
exit 2
fi
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cb="${CRABBOX_BIN:-$root/bin/crabbox}"
coord="${CRABBOX_AUTH_SMOKE_COORDINATOR:-${CRABBOX_COORDINATOR:-https://crabbox-access.openclaw.ai}}"
config_path="$("$cb" config path)"
need_tool() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "missing required tool: $1" >&2
exit 2
fi
}
need_tool curl
need_tool jq
need_tool ruby
config_value() {
local key_path="$1"
ruby -ryaml -e '
value = ARGV[1].split(".").reduce(YAML.load_file(ARGV[0])) do |memo, key|
memo.is_a?(Hash) ? memo[key] : nil
end
exit 3 if value.nil? || value.to_s.empty?
print value
' "$config_path" "$key_path"
}
curl_quote() {
ruby -e 'print ARGV[0].inspect' "$1"
}
write_curl_config() {
local token="$1"
local url="$2"
local output="$3"
shift 3
: >"$output"
chmod 0600 "$output"
{
printf 'url = %s\n' "$(curl_quote "$url")"
printf 'request = "GET"\n'
printf 'connect-timeout = "10"\n'
printf 'max-time = "300"\n'
printf 'silent\n'
printf 'show-error\n'
printf 'location\n'
printf 'output = "-"\n'
printf 'write-out = "\\n%%{http_code}"\n'
printf 'header = %s\n' "$(curl_quote "Authorization: Bearer $token")"
if [[ -n "${access_client_id:-}" && -n "${access_client_secret:-}" ]]; then
printf 'header = %s\n' "$(curl_quote "CF-Access-Client-Id: $access_client_id")"
printf 'header = %s\n' "$(curl_quote "CF-Access-Client-Secret: $access_client_secret")"
fi
for header in "$@"; do
printf 'header = %s\n' "$(curl_quote "$header")"
done
} >>"$output"
}
request_json() {
local token="$1"
local path="$2"
local body_file="$3"
shift 3
local cfg
cfg="$(mktemp)"
write_curl_config "$token" "${coord%/}$path" "$cfg" "$@"
local response
response="$(curl --config "$cfg")"
rm -f "$cfg"
local status="${response##*$'\n'}"
local body="${response%$'\n'*}"
printf '%s' "$body" >"$body_file"
printf '%s' "$status"
}
shared_token="$(config_value broker.token)"
admin_token="$(config_value broker.adminToken)"
access_client_id="${CRABBOX_ACCESS_CLIENT_ID:-$(config_value broker.access.clientId 2>/dev/null || true)}"
access_client_secret="${CRABBOX_ACCESS_CLIENT_SECRET:-$(config_value broker.access.clientSecret 2>/dev/null || true)}"
owner="${CRABBOX_OWNER:-$(git config user.email 2>/dev/null || true)}"
owner="${owner:-crabbox-auth-smoke@example.invalid}"
org="${CRABBOX_ORG:-openclaw}"
if [[ "$coord" == *"crabbox-access.openclaw.ai"* ]]; then
no_access_code="$(curl -sS -o /dev/null -w '%{http_code}' "${coord%/}/v1/health")"
if [[ "$no_access_code" != "403" ]]; then
echo "failed no-access edge check: HTTP $no_access_code" >&2
exit 1
fi
echo "ok no-access edge denied http=403"
fi
whoami="$(env -u CRABBOX_COORDINATOR_TOKEN CRABBOX_COORDINATOR="$coord" "$cb" whoami --json)"
printf '%s\n' "$whoami" | jq -e '.auth == "bearer" and (.owner | length > 0) and (.org | length > 0)' >/dev/null
echo "ok shared token whoami owner=$(printf '%s\n' "$whoami" | jq -r '.owner') org=$(printf '%s\n' "$whoami" | jq -r '.org')"
body="$(mktemp)"
trap 'rm -f "$body"' EXIT
status="$(request_json "$shared_token" "/v1/whoami" "$body" \
"X-Crabbox-Owner: $owner" \
"X-Crabbox-Org: $org" \
"cf-access-authenticated-user-email: spoof@example.invalid")"
if [[ "$status" != "200" ]]; then
echo "failed raw Access spoof check: HTTP $status body=$(cat "$body")" >&2
exit 1
fi
spoof_owner="$(jq -r '.owner' "$body")"
if [[ "$spoof_owner" == "spoof@example.invalid" ]]; then
echo "failed raw Access spoof check: spoofed owner accepted" >&2
exit 1
fi
echo "ok raw Access identity spoof ignored owner=$spoof_owner"
status="$(request_json "$shared_token" "/v1/admin/leases?limit=1" "$body" \
"X-Crabbox-Owner: $owner" \
"X-Crabbox-Org: $org")"
if [[ "$status" != "403" ]]; then
echo "failed shared-token admin denial: HTTP $status body=$(cat "$body")" >&2
exit 1
fi
jq -e '.message == "admin token required"' "$body" >/dev/null
echo "ok shared token denied for admin http=403"
status="$(request_json "$admin_token" "/v1/admin/leases?limit=1" "$body" \
"X-Crabbox-Owner: $owner" \
"X-Crabbox-Org: $org")"
if [[ "$status" != "200" ]]; then
echo "failed admin-token admin check: HTTP $status body=$(cat "$body")" >&2
exit 1
fi
jq -e '.leases | type == "array"' "$body" >/dev/null
echo "ok admin token accepted for admin leases=$(jq '.leases | length' "$body")"