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.
This commit is contained in:
@@ -22,6 +22,7 @@ declare -A IMAGES=(
|
||||
[rootnode/gateway]="platform/gateway/Dockerfile platform/gateway"
|
||||
[rootnode/provisioner]="platform/provisioner/Dockerfile platform/provisioner"
|
||||
[rootnode/egress-proxy]="platform/egress-proxy/Dockerfile platform/egress-proxy"
|
||||
[rootnode/lab-router]="platform/lab-router/Dockerfile platform/lab-router"
|
||||
[rootnode/lab]="Dockerfile ."
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generates a private CA and a mutual-TLS cert/key pair for the Provisioner <-> rn-lab-host-01
|
||||
# Docker Engine connection (docs/PRODUCTION_NETWORK_SECURITY.md §7). 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, not new app code.
|
||||
# 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's --tlsverify config
|
||||
# rn-platform-01: ca.pem, provisioner-client-cert.pem, provisioner-client-key.pem
|
||||
# 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]
|
||||
@@ -61,13 +68,37 @@ 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
|
||||
|
||||
rm -f server.csr client.csr server-extfile.cnf client-extfile.cnf
|
||||
chmod 0400 ca-key.pem server-key.pem provisioner-client-key.pem
|
||||
chmod 0444 ca.pem server-cert.pem provisioner-client-cert.pem
|
||||
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 "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 "Verify with: ../verify-docker-tls.sh $OUT_DIR <lab-host-ip>"
|
||||
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>"
|
||||
|
||||
@@ -36,3 +36,17 @@ sed -e 's/{{ academy_domain }}/academy.example.com/g' -e 's/{{ management_cidr }
|
||||
chmod a+r "$TMPDIR/Caddyfile"
|
||||
docker run --rm -v "$TMPDIR:/etc/caddy:ro" caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile 2>&1 | grep -q "Valid configuration" \
|
||||
&& echo "OK: Caddyfile is valid" || { echo "FAIL: Caddyfile did not validate"; exit 1; }
|
||||
|
||||
# Phase 4.1: rn-lab-host-01's compose file (egress-proxy, lab-router, observability agents)
|
||||
# was never covered here before — added when the lab-router service landed.
|
||||
echo
|
||||
echo "--- lab-host production compose ---"
|
||||
LABHOST_TMPDIR="$(mktemp -d)"
|
||||
sed \
|
||||
-e 's/{{ registry }}/registry.example.com/g' \
|
||||
-e 's/{{ image_tag }}/test/g' \
|
||||
-e "s/{{ lab_router_port | default('8443') }}/8443/g" \
|
||||
../lab-host/docker-compose.production.yml > "$LABHOST_TMPDIR/docker-compose.yml"
|
||||
docker compose -f "$LABHOST_TMPDIR/docker-compose.yml" config --quiet
|
||||
echo "OK: lab-host production compose file is well-formed YAML and passes docker compose's own schema validation"
|
||||
rm -rf "$LABHOST_TMPDIR"
|
||||
|
||||
Reference in New Issue
Block a user