Files
ced4568 eae8e48d43 feat(routing): Lab Host Router closes the Gateway-to-Lab cross-VM gap
Adds platform/lab-router/ — a narrow service that runs on the Lab Host,
authenticates the Gateway via mTLS, and resolves an opaque routing token to
a local container purely from Docker label state (no Postgres access, no
callback to the Provisioner). Replaces the Gateway's env.internal_ip
assumption (only valid when Gateway and Lab share a Docker daemon) with a
data-driven direct/router decision per lab_host
(lab_hosts.routing_mode/router_endpoint, migration 004), resolved in one
place (platform/gateway/src/lab-endpoint.js's labTarget()) that every
proxy/fetch call site in server.js now goes through.

Extends the existing Docker TLS CA (infra/scripts/docker-tls/generate-docker-tls.sh)
with a second mTLS leaf pair (router-server/gateway-client) for this new
data-plane connection, deployed by the docker-tls Ansible role into a
separate directory from the control-plane pair. Firewall
(rn-lab-host-01.rules) replaces the old speculative wide port-range
allowance with one narrow rule: platform_ip -> the Router's port only.

dev-local stays routing_mode: direct — zero behavior change for local dev,
confirmed by the full existing regression suite passing unmodified.
2026-08-24 20:14:39 -05:00

105 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Generates a private CA and TWO mutual-TLS cert/key pairs, both under that one CA:
# 1. Provisioner <-> rn-lab-host-01 Docker Engine (CONTROL plane, port 2376). The application
# already supports this (platform/provisioner/src/docker.js buildClient() has required
# mTLS since Phase 1) — this script only ever needed to exist for the CERTIFICATES.
# 2. Gateway <-> rn-lab-host-01 Lab Host Router (DATA plane, port 8443) — Phase 4.1, see
# docs/PHASE4_1_PREPRODUCTION_HARDENING.md §1-3 and docs/PRODUCTION_NETWORK_SECURITY.md.
# One CA for both keeps the PKI surface small (one thing to protect, one rotation runbook) —
# see docs/RUNBOOK_DOCKER_TLS_ROTATION.md, which now covers all four leaf certs, not just two.
#
# Output goes to $OUT_DIR (default ./out, gitignored) — never into a tracked path. Deploy the
# resulting files with the `docker-tls` Ansible role, which places:
# rn-lab-host-01: ca.pem, server-cert.pem, server-key.pem -> dockerd --tlsverify
# ca.pem, router-server-cert.pem, router-server-key.pem -> lab-router (data plane)
# rn-platform-01: ca.pem, provisioner-client-cert.pem, provisioner-client-key.pem
# -> /etc/rootnode/docker-tls/ (referenced by lab-hosts.yaml)
# ca.pem, gateway-client-cert.pem, gateway-client-key.pem
# -> /etc/rootnode/lab-router-tls/ (referenced by gateway's LAB_ROUTER_TLS_*)
#
# Usage:
# ./generate-docker-tls.sh [--out-dir DIR] [--lab-host-ip IP] [--lab-host-dns NAME] [--days N]
set -euo pipefail
OUT_DIR="./out"
LAB_HOST_IP="127.0.0.1"
LAB_HOST_DNS="rn-lab-host-01.internal"
DAYS=825 # ~2.25 years — matches modern browser/OS max cert lifetime conventions; see rotation runbook for cadence
while [ $# -gt 0 ]; do
case "$1" in
--out-dir) OUT_DIR="$2"; shift 2 ;;
--lab-host-ip) LAB_HOST_IP="$2"; shift 2 ;;
--lab-host-dns) LAB_HOST_DNS="$2"; shift 2 ;;
--days) DAYS="$2"; shift 2 ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done
mkdir -p "$OUT_DIR"
cd "$OUT_DIR"
umask 077 # private keys must never be group/world readable, from the moment they're written
echo "==> Generating CA"
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days "$DAYS" -key ca-key.pem -sha256 -out ca.pem \
-subj "/O=RootNode Academy/CN=RootNode Docker TLS CA"
echo "==> Generating server cert for rn-lab-host-01 (dockerd)"
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$LAB_HOST_DNS" -sha256 -new -key server-key.pem -out server.csr
cat > server-extfile.cnf <<EOF
subjectAltName = DNS:$LAB_HOST_DNS,IP:$LAB_HOST_IP
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -days "$DAYS" -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile server-extfile.cnf
echo "==> Generating client cert for the Provisioner"
openssl genrsa -out provisioner-client-key.pem 4096
openssl req -subj "/CN=rootnode-provisioner" -new -key provisioner-client-key.pem -out client.csr
cat > client-extfile.cnf <<EOF
extendedKeyUsage = clientAuth
EOF
openssl x509 -req -days "$DAYS" -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out provisioner-client-cert.pem -extfile client-extfile.cnf
echo "==> Generating server cert for rn-lab-host-01 (Lab Host Router, data plane)"
openssl genrsa -out router-server-key.pem 4096
openssl req -subj "/CN=$LAB_HOST_DNS" -sha256 -new -key router-server-key.pem -out router-server.csr
cat > router-server-extfile.cnf <<EOF
subjectAltName = DNS:$LAB_HOST_DNS,IP:$LAB_HOST_IP
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -days "$DAYS" -sha256 -in router-server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out router-server-cert.pem -extfile router-server-extfile.cnf
echo "==> Generating client cert for the Gateway (data plane)"
openssl genrsa -out gateway-client-key.pem 4096
openssl req -subj "/CN=rootnode-gateway" -new -key gateway-client-key.pem -out gateway-client.csr
openssl x509 -req -days "$DAYS" -sha256 -in gateway-client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out gateway-client-cert.pem -extfile client-extfile.cnf
rm -f server.csr client.csr router-server.csr gateway-client.csr server-extfile.cnf client-extfile.cnf router-server-extfile.cnf
chmod 0400 ca-key.pem server-key.pem provisioner-client-key.pem router-server-key.pem gateway-client-key.pem
chmod 0444 ca.pem server-cert.pem provisioner-client-cert.pem router-server-cert.pem gateway-client-cert.pem
echo "==> Done. Generated in $OUT_DIR:"
ls -la
echo
echo "CONTROL plane (Provisioner -> dockerd, port 2376):"
echo " Deploy server-{cert,key}.pem + ca.pem to rn-lab-host-01 (docker-tls Ansible role)."
echo " Deploy provisioner-client-{cert,key}.pem + ca.pem to rn-platform-01 (same role)."
echo "DATA plane (Gateway -> Lab Host Router, port 8443):"
echo " Deploy router-server-{cert,key}.pem + ca.pem to rn-lab-host-01 (lab-router Ansible role)."
echo " Deploy gateway-client-{cert,key}.pem + ca.pem to rn-platform-01 (platform Ansible role)."
echo
echo "Verify Docker control-plane TLS with: ../verify-docker-tls.sh $OUT_DIR <lab-host-ip>"