#!/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. "." (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://: 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 <}. *** root.key.pem is the offline trust anchor — after first use, move it OFF this host. *** SUMMARY