We use structured logging to emit metrics. Key metrics to monitor:
challenge_issued_total: Volume of unlock requests initiated.unlock_success_total: Successful prompt decryptions.unlock_failure_total: Failed attempts (labeled by reason).rate_limit_hit_total: Blocked requests (labeled by type).api_request_duration_ms: Latency of the unlock flow.
The /api/health endpoint provides a basic signal of service availability.
Default limits (defined in src/lib/observability/rateLimiter.ts):
- Challenge: 10 requests per minute per IP.
- Unlock: 5 requests per minute per IP/Wallet.
The following fields are automatically redacted from logs:
plaintextsecretprivateKeysignedMessage- Authorization headers
- Ensure the user's wallet is signing the exact message returned by the challenge endpoint.
- Verify that the nonce hasn't expired (default TTL: 5 minutes).
- Check if the transaction for purchasing the prompt has been confirmed on the Stellar network.
- Ensure the indexer or RPC being used is up to date with the latest ledger.
See docs/secret-rotation.md for the full rotation guide. This section is the operational quick-reference.
- Scheduled: every 30–90 days via cron (
scripts/cron-rotation.example). - Unscheduled: immediately on suspected compromise or relevant personnel change.
# Confirm required env vars are present on the server
echo "CHALLENGE_TOKEN_SECRET set: ${CHALLENGE_TOKEN_SECRET:+yes}"
echo "ADMIN_ROTATION_TOKEN set : ${ADMIN_ROTATION_TOKEN:+yes}"
echo "UNLOCK_SERVICE_URL : ${UNLOCK_SERVICE_URL:-NOT SET}"
# Dry-run — validate all preflight checks without mutating state
./scripts/rotate-secrets.sh --dry-run --env productionThe dry-run must report preflightOk: true before proceeding with a real rotation.
# Via script (recommended) — 10-minute grace period for production
./scripts/rotate-secrets.sh --grace-period 600 --env production
# Via API directly
curl -X POST "${UNLOCK_SERVICE_URL}/api/auth/rotateSecret" \
-H "Authorization: Bearer ${ADMIN_ROTATION_TOKEN}" \
-H "Content-Type: application/json"# 1. Health check
curl -s "${UNLOCK_SERVICE_URL}/api/health" | jq '.'
# 2. Request a new challenge token — must succeed with the new secret
curl -s -X POST "${UNLOCK_SERVICE_URL}/api/auth/challenge" \
-H "Content-Type: application/json" \
-d '{"walletAddress": "GTEST..."}' | jq '.'
# 3. Monitor structured logs for:
# event=secret_rotation_success (expected)
# event=secret_rotation_error (alert immediately)If the new secret causes problems while the previous secret is still valid:
# Restore the previous secret as current
export CHALLENGE_TOKEN_SECRET="$CHALLENGE_TOKEN_SECRET_PREVIOUS"
unset CHALLENGE_TOKEN_SECRET_PREVIOUS
unset CHALLENGE_TOKEN_ROTATION_TIMESTAMP
# Restart the service
systemctl restart unlock-service
# Verify
curl -s "${UNLOCK_SERVICE_URL}/api/health" | jq '.'- Retrieve the previous secret value from your secrets manager (AWS Secrets Manager, Vault, etc.).
- Restore it as
CHALLENGE_TOKEN_SECRETand restart the service. - If the previous value is unrecoverable, rotate again immediately. In-flight tokens (max 5-min TTL) will fail; users must re-request a challenge.
Immediately invalidate all active tokens by setting the grace period to 0:
./scripts/rotate-secrets.sh --grace-period 0 --env productionThen verify with the health check and notify the security team.
| Symptom | Likely cause | Resolution |
|---|---|---|
| Script exits before making a request | Preflight error (missing/bad env var) | Read script output; fix reported env var and retry |
| HTTP 401 from endpoint | Wrong ADMIN_ROTATION_TOKEN |
Verify value matches server-side config |
| HTTP 422 from endpoint | Server preflight failure | Read details in response; fix server env |
| Unlock failures spike after rotation | Grace period too short | Increase CHALLENGE_TOKEN_GRACE_PERIOD_MS; roll back if within grace window |
CHALLENGE_TOKEN_SECRET_PREVIOUS still set days later |
Cleanup not running | unset CHALLENGE_TOKEN_SECRET_PREVIOUS CHALLENGE_TOKEN_ROTATION_TIMESTAMP and restart |