#!/usr/bin/env bash # # RELAY-PHASE1 · staging bootstrap (mode B — self-signed TLS, no Let's Encrypt yet). # # Idempotent one-shot that prepares EVERYTHING the two services need to boot, EXCEPT starting them: # 1. strong random POSTGRES_PASSWORD + OPERATOR_PASSWORD (persisted 0600, reused on re-run) # 2. P5 capability keypair (deploy/scripts/gen-capability-key.sh) # 3. private agent-enrollment CA (deploy/scripts/gen-agent-ca.sh) # 4. self-signed browser :443 cert (mode B stand-in for issue-tls-cert.sh) # 5. /etc/relay/control-plane.env + /etc/relay/relay.env — the REAL env contract read by the code # (control-plane/src/env.ts + relay-run/src/main-phase1.ts), which is a SUPERSET of .env.example: # · relay needs AGENT_SERVER_CERT_PATH / AGENT_SERVER_KEY_PATH / ALLOWED_ORIGINS (template omits) # · relay hosts the STAGING /auth/mint, so it needs CAPABILITY_SIGN_PRIVKEY (base64 PKCS#8 DER) # + MINT_RATE_SALT. In Phase 1 the relay IS a token minter (staging shortcut); Phase 2 = WebAuthn. # 6. deploy/.env for docker-compose, then `docker compose up -d` Postgres + Redis (loopback-only). # # Secrets are generated in place (0600); NOTHING secret is printed except OPERATOR_PASSWORD (once). # Private keys are NEVER printed. Config via ENV (staging defaults for THIS deploy): # BASE_DOMAIN (yaojia.wang) SUBDOMAIN (kai) SERVER_IP (8.138.1.192) # RELAY_TRUST_DOMAIN (relay.yaojia.wang) RELAY_NODE_ID (relay-1) REPO (repo root) # # Verify (syntax): bash -n deploy/scripts/bootstrap-staging.sh set -euo pipefail BASE_DOMAIN="${BASE_DOMAIN:-yaojia.wang}" SUBDOMAIN="${SUBDOMAIN:-kai}" SERVER_IP="${SERVER_IP:-8.138.1.192}" RELAY_TRUST_DOMAIN="${RELAY_TRUST_DOMAIN:-relay.${BASE_DOMAIN}}" RELAY_NODE_ID="${RELAY_NODE_ID:-relay-1}" FQDN="${SUBDOMAIN}.${BASE_DOMAIN}" # Repo root = two levels up from this script, unless overridden. SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" REPO="${REPO:-$SELF}" ETC=/etc/relay CAP_DIR="${ETC}/capability" CA_DIR="${ETC}/ca" TLS_DIR="${ETC}/tls" SECRETS="${ETC}/secrets.env" command -v openssl >/dev/null 2>&1 || { echo "FATAL: openssl not on PATH" >&2; exit 3; } command -v docker >/dev/null 2>&1 || { echo "FATAL: docker not on PATH" >&2; exit 3; } umask 077 mkdir -p "${ETC}" "${TLS_DIR}" echo "== [1/6] passwords (persisted, reused on re-run) ==" # shellcheck disable=SC1090 [[ -f "${SECRETS}" ]] && source "${SECRETS}" POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32)}" OPERATOR_PASSWORD="${OPERATOR_PASSWORD:-$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | head -c 24)}" MINT_RATE_SALT="${MINT_RATE_SALT:-$(openssl rand -hex 16)}" { echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" echo "OPERATOR_PASSWORD=${OPERATOR_PASSWORD}" echo "MINT_RATE_SALT=${MINT_RATE_SALT}" } > "${SECRETS}" chmod 600 "${SECRETS}" echo "== [2/6] P5 capability keypair ==" KEY_DIR="${CAP_DIR}" bash "${REPO}/deploy/scripts/gen-capability-key.sh" > "${ETC}/capability-summary.txt" 2>&1 \ || { echo "FATAL: gen-capability-key.sh failed:" >&2; cat "${ETC}/capability-summary.txt" >&2; exit 1; } CAP_B64="$(grep -oE 'CAPABILITY_SIGN_PUBKEY_B64=[^ ]+' "${ETC}/capability-summary.txt" | head -1 | cut -d= -f2-)" CAP_B64URL="$(grep -oE 'RELAY_AUTH_VERIFY_PUBKEY=[^ ]+' "${ETC}/capability-summary.txt" | head -1 | cut -d= -f2-)" [[ -n "${CAP_B64}" && -n "${CAP_B64URL}" ]] || { echo "FATAL: could not parse capability pubkeys" >&2; exit 1; } # The relay ALSO mints operator tokens (staging /auth/mint) → it needs the private key as base64 PKCS#8 DER. CAP_PRIV_B64="$(openssl pkey -in "${CAP_DIR}/capability-sign.key.pem" -outform DER | base64 | tr -d '\n')" echo "== [3/6] private agent-enrollment CA (NOT Let's Encrypt) ==" CA_DIR="${CA_DIR}" AGENT_FACING_HOST="${FQDN}" AGENT_FACING_IP="${SERVER_IP}" \ RELAY_TRUST_DOMAIN="${RELAY_TRUST_DOMAIN}" \ bash "${REPO}/deploy/scripts/gen-agent-ca.sh" > "${ETC}/ca-summary.txt" 2>&1 \ || { echo "FATAL: gen-agent-ca.sh failed:" >&2; cat "${ETC}/ca-summary.txt" >&2; exit 1; } echo "== [4/6] self-signed browser :443 cert for ${FQDN} (mode B) ==" if [[ ! -f "${TLS_DIR}/privkey.pem" ]]; then openssl req -x509 -newkey rsa:2048 -nodes -days 825 \ -keyout "${TLS_DIR}/privkey.pem" -out "${TLS_DIR}/fullchain.pem" \ -subj "/CN=${FQDN}" -addext "subjectAltName=DNS:${FQDN},IP:${SERVER_IP}" fi chmod 600 "${TLS_DIR}/privkey.pem"; chmod 644 "${TLS_DIR}/fullchain.pem" echo "== [5/6] env files (REAL contract from env.ts + main-phase1.ts) ==" cat > "${ETC}/control-plane.env" < "${ETC}/relay.env" < "${REPO}/deploy/.env" <