Files
web-terminal/deploy/scripts/issue-tls-cert.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

116 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# RELAY-PHASE1 · E (TASK E) — PUBLIC-web TLS cert for the browser :443 endpoint (Let's Encrypt).
#
# STAGING TEMPLATE. This is the ONLY public-CA cert in the deployment: the browser hits
# https://<SUBDOMAIN>.<BASE_DOMAIN> (WSS same-origin), so it must chain to a public root. It is a
# DIFFERENT trust chain from the private agent-enrollment CA (deploy/scripts/gen-agent-ca.sh) — do
# NOT cross-wire them.
#
# ICP-備案 ASSUMPTION (mainland Alibaba Cloud): BASE_DOMAIN is ALREADY ICP-filed and its A-record
# <SUBDOMAIN>.<BASE_DOMAIN> -> 8.138.1.192 resolves. Serving :80/:443 on an unfiled domain in
# mainland CN is blocked upstream; HTTP-01 will then fail. If unfiled, use DNS-01 (no inbound :80).
#
# Two challenge methods (pick with ACME_METHOD):
# http-01 — a one-shot listener on :80 answers the challenge. REQUIRES inbound :80 open in the
# Aliyun security group DURING issuance (you can close it again after; renewals reopen).
# dns-01 — a TXT record under _acme-challenge.<SUBDOMAIN>.<BASE_DOMAIN>. No inbound :80. Needs the
# acme.sh DNS-API creds for your provider (e.g. Ali_Key/Ali_Secret for the Aliyun DNS API).
#
# Installs to TLS_CERT_PATH (fullchain) + TLS_KEY_PATH, matching relay.env.
#
# Config via ENV only:
# BASE_DOMAIN REQUIRED (e.g. term.example.com)
# SUBDOMAIN REQUIRED (the tenant sub; FQDN = <SUBDOMAIN>.<BASE_DOMAIN>)
# ACME_EMAIL REQUIRED account/registration email
# ACME_METHOD http-01 | dns-01 (default http-01)
# ACME_CLIENT acme.sh | certbot (default acme.sh)
# ACME_DNS_PROVIDER acme.sh dnsapi id for dns-01 (e.g. dns_ali) (dns-01 only)
# TLS_CERT_PATH install target for fullchain (default /etc/relay/tls/fullchain.pem)
# TLS_KEY_PATH install target for privkey (default /etc/relay/tls/privkey.pem)
# ACME_STAGING 1 to use the LE staging CA (untrusted, avoids rate limits while testing)
#
# Verify (syntax): bash -n deploy/scripts/issue-tls-cert.sh
set -euo pipefail
: "${BASE_DOMAIN:?FATAL: BASE_DOMAIN is required (your ICP-filed domain)}"
: "${SUBDOMAIN:?FATAL: SUBDOMAIN is required}"
: "${ACME_EMAIL:?FATAL: ACME_EMAIL is required}"
ACME_METHOD="${ACME_METHOD:-http-01}"
ACME_CLIENT="${ACME_CLIENT:-acme.sh}"
TLS_CERT_PATH="${TLS_CERT_PATH:-/etc/relay/tls/fullchain.pem}"
TLS_KEY_PATH="${TLS_KEY_PATH:-/etc/relay/tls/privkey.pem}"
FQDN="${SUBDOMAIN}.${BASE_DOMAIN}"
echo "== Issuing Let's Encrypt cert for ${FQDN} via ${ACME_CLIENT} (${ACME_METHOD}) =="
mkdir -p "$(dirname "${TLS_CERT_PATH}")" "$(dirname "${TLS_KEY_PATH}")"
case "${ACME_CLIENT}" in
acme.sh)
command -v acme.sh >/dev/null 2>&1 || { echo "FATAL: acme.sh not installed. curl https://get.acme.sh | sh" >&2; exit 3; }
acme.sh --register-account -m "${ACME_EMAIL}" >/dev/null 2>&1 || true
STAGING_FLAG=""
[[ "${ACME_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging"
case "${ACME_METHOD}" in
http-01)
echo ">> Ensure inbound :80 is OPEN in the Aliyun security group for the duration of issuance."
acme.sh --issue ${STAGING_FLAG} -d "${FQDN}" --standalone --httpport 80
;;
dns-01)
: "${ACME_DNS_PROVIDER:?FATAL: ACME_DNS_PROVIDER required for dns-01 (e.g. dns_ali); export the DNS-API creds too}"
acme.sh --issue ${STAGING_FLAG} -d "${FQDN}" --dns "${ACME_DNS_PROVIDER}"
;;
*)
echo "FATAL: ACME_METHOD must be http-01 or dns-01" >&2; exit 2 ;;
esac
# --install-cert copies the current material AND registers the renew-reload hook.
acme.sh --install-cert -d "${FQDN}" \
--key-file "${TLS_KEY_PATH}" \
--fullchain-file "${TLS_CERT_PATH}" \
--reloadcmd "systemctl try-reload-or-restart relay-data-plane.service"
;;
certbot)
command -v certbot >/dev/null 2>&1 || { echo "FATAL: certbot not installed (apt-get install certbot)." >&2; exit 3; }
STAGING_FLAG=""
[[ "${ACME_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging"
case "${ACME_METHOD}" in
http-01)
echo ">> Ensure inbound :80 is OPEN in the Aliyun security group for the duration of issuance."
certbot certonly ${STAGING_FLAG} --standalone --non-interactive --agree-tos \
-m "${ACME_EMAIL}" -d "${FQDN}"
;;
dns-01)
echo "FATAL: certbot dns-01 needs a provider plugin; prefer ACME_CLIENT=acme.sh for Aliyun DNS." >&2
exit 2 ;;
*)
echo "FATAL: ACME_METHOD must be http-01 or dns-01" >&2; exit 2 ;;
esac
install -m 644 "/etc/letsencrypt/live/${FQDN}/fullchain.pem" "${TLS_CERT_PATH}"
install -m 600 "/etc/letsencrypt/live/${FQDN}/privkey.pem" "${TLS_KEY_PATH}"
;;
*)
echo "FATAL: ACME_CLIENT must be acme.sh or certbot" >&2; exit 2 ;;
esac
chmod 600 "${TLS_KEY_PATH}" || true
chmod 644 "${TLS_CERT_PATH}" || true
cat <<SUMMARY
Installed:
TLS_CERT_PATH = ${TLS_CERT_PATH}
TLS_KEY_PATH = ${TLS_KEY_PATH}
RENEWAL (LE certs last ~90 days):
- acme.sh installs its OWN cron on install ('acme.sh --cron'); --install-cert registered the
reload hook above, so renewals auto-reload the data-plane. Verify: 'crontab -l | grep acme'.
- certbot: add a cron/timer, e.g.
0 3 * * * certbot renew --quiet --deploy-hook 'systemctl try-reload-or-restart relay-data-plane.service'
- http-01 renewals need :80 reachable at renew time; dns-01 does not.
SUMMARY