Files
web-terminal/deploy/scripts/gen-agent-ca.sh
Yaojia Wang aa1912b962 feat(relay): Phase1 waves A2-E — server entry, shared-store data plane, agent runtime, deploy artifacts
RELAY-PHASE1 Wave A2/B/C/D/E (12-agent workflow, all tsc-clean, 314/314 tests pass):
- A2: control-plane server.ts entry + boot/redis.ts revocation-bus wiring + start script.
- B1: relay-run shared-store EnforceDeps (relay-auth ports over the SAME Postgres+Redis as P3).
- B2: registry-backed MtlsVerifier (verifyAgentCert, fail-closed, INV14).
- B3: store-backed RouteResolver (subdomain->hostId).
- B4: Redis relay:revocations subscriber -> tunnel teardown (INV12).
- B5: main-phase1.ts production entry (public bind, real TLS, async-mTLS prefetch bridge) + staging /auth/mint.
- B6 (PARTIAL): relay-web operator login + browser DPoP; proof offered via term.dpop.<b64u> subprotocol.
- C: agent dist/cli.js build (esbuild) + runTunnel run-loop + CliDeps.
- D1: same-origin static serve of relay-web/public from the browser WSS.
- E: systemd units + gen-ca/gen-capability-key/issue-tls-cert scripts + deploy/RUNBOOK.md.
Adversarial review: all hard invariants PASS. Follow-ups (B7): close DPoP-subprotocol read on
browser-server (blocks browser connect); rate-limit /auth/mint (F1); wire activeSessionCount (F2);
scrub error logs (F5). Excludes unrelated public/style.css (concurrent iOS job).
2026-07-06 16:13:34 +02:00

146 lines
7.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# RELAY-PHASE1 · E (TASK E) — Private ENROLLMENT CA for agent mTLS.
#
# STAGING TEMPLATE. This mints the PRIVATE trust chain the rendezvous-relay uses to (a) verify agent
# client certs (SPIFFE mTLS, INV14) and (b) present the relay's agent-facing SERVER cert on
# AGENT_BIND_PORT so the dialing agent can authenticate the relay.
#
# *** THIS IS NOT LET'S ENCRYPT. *** The browser-facing :443 cert is public-web PKI and is issued
# by deploy/scripts/issue-tls-cert.sh. This CA is a SEPARATE, private trust root that must NEVER be
# a public CA — mixing them would let any web-PKI leaf enroll as an agent.
#
# Hierarchy produced (Ed25519, to match the control-plane dev signer's algorithm):
# root (self-signed, offline anchor)
# └── intermediate (signs agent SPIFFE leaves at CP /enroll, AND the relay agent-server cert)
# ├── relay agent-server leaf (TLS serverAuth, CN/SAN = the agent-facing host)
# └── (agent leaves are issued at runtime by the control-plane, not here)
#
# Outputs (0600 keys, 0644 certs) under CA_DIR (default /etc/relay/ca):
# root.key.pem root.cert.pem
# intermediate.key.pem intermediate.cert.pem <- CA_INTERMEDIATE_CERT_PATH (+ its key = the CP signer)
# agent-ca.bundle.pem = intermediate + root <- AGENT_CA_CHAIN_PATH / NODE_MTLS_TRUST_BUNDLE_PATH
# agent-ca.cert.pem = intermediate cert <- AGENT_CA_CERT_PATH
# relay-agent-server.key.pem relay-agent-server.cert.pem (present on AGENT_BIND_PORT)
#
# Config via ENV only (no hardcoded hosts/ports/secrets):
# CA_DIR output dir (default /etc/relay/ca)
# AGENT_FACING_HOST REQUIRED — CN/SAN for the relay agent-server cert; the host the agent dials,
# e.g. "<sub>.<BASE_DOMAIN>" (must match RELAY_URL's host in the agent config).
# AGENT_FACING_IP OPTIONAL — extra IP SAN (e.g. 8.138.1.192) if the agent dials the raw IP.
# RELAY_TRUST_DOMAIN OPTIONAL — SPIFFE trust domain (recorded in a NOTE only; agent SPIFFE leaves
# are minted by the control-plane /enroll, not by this script).
# CA_DAYS_ROOT / CA_DAYS_INT / CA_DAYS_LEAF cert lifetimes (defaults 3650 / 1825 / 825).
#
# Verify (syntax): bash -n deploy/scripts/gen-agent-ca.sh
set -euo pipefail
CA_DIR="${CA_DIR:-/etc/relay/ca}"
CA_DAYS_ROOT="${CA_DAYS_ROOT:-3650}"
CA_DAYS_INT="${CA_DAYS_INT:-1825}"
CA_DAYS_LEAF="${CA_DAYS_LEAF:-825}"
if [[ -z "${AGENT_FACING_HOST:-}" ]]; then
echo "FATAL: AGENT_FACING_HOST is required (CN/SAN of the relay agent-server cert)." >&2
echo " e.g. AGENT_FACING_HOST=term1.example.com bash $0" >&2
exit 2
fi
command -v openssl >/dev/null 2>&1 || { echo "FATAL: openssl not found on PATH." >&2; exit 3; }
umask 077
mkdir -p "${CA_DIR}"
cd "${CA_DIR}"
echo "== Enrollment CA (PRIVATE — not Let's Encrypt) into ${CA_DIR} =="
# ---- 1. Root CA (offline anchor) ------------------------------------------------------------------
if [[ ! -f root.key.pem ]]; then
openssl genpkey -algorithm ed25519 -out root.key.pem
fi
openssl req -x509 -new -key root.key.pem -days "${CA_DAYS_ROOT}" \
-subj "/O=web-terminal-relay/OU=agent-enrollment/CN=web-terminal Agent Enrollment Root" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" \
-out root.cert.pem
# ---- 2. Intermediate CA (the control-plane's leaf-signing key in staging) --------------------------
# The CP dev in-process signer (CA_INTERMEDIATE_KMS_KEY_REF=dev-local) signs agent SPIFFE leaves with
# THIS intermediate key. In Phase 2 the intermediate key moves into a non-exportable KMS/HSM (§3.1).
if [[ ! -f intermediate.key.pem ]]; then
openssl genpkey -algorithm ed25519 -out intermediate.key.pem
fi
openssl req -new -key intermediate.key.pem \
-subj "/O=web-terminal-relay/OU=agent-enrollment/CN=web-terminal Agent Enrollment Intermediate" \
-out intermediate.csr.pem
cat > intermediate.ext <<'EXT'
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
EXT
openssl x509 -req -in intermediate.csr.pem \
-CA root.cert.pem -CAkey root.key.pem -CAcreateserial \
-days "${CA_DAYS_INT}" -extfile intermediate.ext \
-out intermediate.cert.pem
# ---- 3. Trust bundle (intermediate + root) --------------------------------------------------------
# The relay walks leaf -> intermediate -> root, so the anchor (root) MUST be in the bundle
# (relay-auth verifyChain terminates at a self-signed root present in the set, INV14).
cat intermediate.cert.pem root.cert.pem > agent-ca.bundle.pem
cp intermediate.cert.pem agent-ca.cert.pem
# ---- 4. Relay agent-facing SERVER cert (presented on AGENT_BIND_PORT) -----------------------------
# The agent dials wss://<AGENT_FACING_HOST>:<AGENT_BIND_PORT> and pins THIS CA to authenticate the
# relay server. CN/SAN must equal the host the agent dials.
if [[ ! -f relay-agent-server.key.pem ]]; then
openssl genpkey -algorithm ed25519 -out relay-agent-server.key.pem
fi
openssl req -new -key relay-agent-server.key.pem \
-subj "/O=web-terminal-relay/OU=agent-data-plane/CN=${AGENT_FACING_HOST}" \
-out relay-agent-server.csr.pem
{
echo "basicConstraints = critical,CA:FALSE"
echo "keyUsage = critical,digitalSignature"
echo "extendedKeyUsage = serverAuth"
echo "subjectKeyIdentifier = hash"
echo "authorityKeyIdentifier = keyid,issuer"
if [[ -n "${AGENT_FACING_IP:-}" ]]; then
echo "subjectAltName = DNS:${AGENT_FACING_HOST},IP:${AGENT_FACING_IP}"
else
echo "subjectAltName = DNS:${AGENT_FACING_HOST}"
fi
} > relay-agent-server.ext
openssl x509 -req -in relay-agent-server.csr.pem \
-CA intermediate.cert.pem -CAkey intermediate.key.pem -CAcreateserial \
-days "${CA_DAYS_LEAF}" -extfile relay-agent-server.ext \
-out relay-agent-server.cert.pem
# ---- 5. Permissions + summary ---------------------------------------------------------------------
chmod 600 ./*.key.pem
chmod 644 ./*.cert.pem ./*.bundle.pem
rm -f ./*.csr.pem ./*.ext
cat <<SUMMARY
Enrollment CA ready in ${CA_DIR}. Wire these env paths (relay.env / control-plane.env):
CA_INTERMEDIATE_CERT_PATH = ${CA_DIR}/intermediate.cert.pem
NODE_MTLS_TRUST_BUNDLE_PATH = ${CA_DIR}/agent-ca.bundle.pem
AGENT_CA_CERT_PATH = ${CA_DIR}/agent-ca.cert.pem
AGENT_CA_CHAIN_PATH = ${CA_DIR}/agent-ca.bundle.pem
relay agent-server cert = ${CA_DIR}/relay-agent-server.cert.pem
relay agent-server key = ${CA_DIR}/relay-agent-server.key.pem (0600)
intermediate signing key = ${CA_DIR}/intermediate.key.pem (0600 — the CP dev signer)
NOTE (staging integration seam, owned by the A-wave CP boot/ca-wiring, NOT by this script):
The control-plane's dev in-process signer must sign agent leaves with intermediate.key.pem so the
leaves chain to this bundle. CA_INTERMEDIATE_KMS_KEY_REF=dev-local selects the dev signer; point it
at ${CA_DIR}/intermediate.key.pem. Phase 2 replaces this with a KMS/HSM ref (§3.1).
SPIFFE trust domain for agent leaves = ${RELAY_TRUST_DOMAIN:-<RELAY_TRUST_DOMAIN unset>}.
*** root.key.pem is the offline trust anchor — after first use, move it OFF this host. ***
SUMMARY