# Phase 4.1 two-host routing simulation — docs/PHASE4_1_PREPRODUCTION_HARDENING.md §6. # # Proves the ONE thing that matters using REAL Docker network isolation, not mocks: a Gateway # container with no route to a Lab container's bridge network genuinely cannot reach it, and # can only reach it through the Lab Host Router. # # What's genuinely simulated (real Docker networking facts, not faked): # - `gateway` is attached ONLY to `sim-platform-net`. It has no network_mode: host, no # attachment to any per-Lab bridge network (rootnode-lab-net-, created dynamically by # the Provisioner) — its network namespace simply has no route to one. This is the same # kind of isolation two separate Proxmox VMs would have, achieved here with two Docker # networks on one daemon instead of two physical hosts. # - `lab-router` runs with network_mode: host — exactly like `egress-proxy` already does in # docker-compose.dev.yml (see that file's comment on why: a host-networked process can # reach every bridge network's containers via the host's own routing table, which is # exactly what a real Lab Host Router process running directly on rn-lab-host-01 needs). # It's also reachable FROM `sim-platform-net` via that network's own gateway IP — the same # mechanism the egress-proxy already relies on for the reverse direction. # - `provisioner`/`api`/`postgres` stay host-networked, unchanged from docker-compose.dev.yml # — this phase's new proof target is the Gateway-to-Lab DATA plane, not the CONTROL plane # (already proven with a real mTLS handshake in Phase 4). Keeping them as-is is a # deliberate scope choice, not an oversight — see the README in this directory. # # What this does NOT prove (needs the real hardware — docs/PRODUCTION_VALIDATION_CHECKLIST.md): # real cross-VM network latency/MTU, the actual Hetzner/Proxmox firewall enforcement, or # anything about two genuinely separate kernels. # # Usage: see README.md in this directory. services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: rootnode_platform_sim ports: - "5434:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 10 egress-proxy: build: ../../../platform/egress-proxy network_mode: host provisioner: build: ../../../platform/provisioner network_mode: host cap_add: - NET_ADMIN - NET_RAW volumes: - /var/run/docker.sock:/var/run/docker.sock # run-simulation.sh generates this with the real routing_mode: router / router_endpoint # once it knows sim-platform-net's actual gateway IP — overrides just this one file, the # image's own resource-policy.yaml underneath is untouched. - ${SIM_LAB_HOSTS_SEED:?set by run-simulation.sh}:/app/config/lab-hosts.seed.yaml:ro environment: PLATFORM_DB_URL: postgres://postgres:postgres@localhost:5434/rootnode_platform_sim PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default} PROVISIONER_PORT: "4101" LAB_IMAGE_REF: ${LAB_IMAGE_REF:-rootnode-academy:test} EGRESS_PROXY_PORT: "3128" DNS_RESOLVER: "127.0.0.11" NO_PROXY_CIDRS: "10.42.0.0/16,10.43.0.0/16" depends_on: postgres: condition: service_healthy api: build: context: ../../.. dockerfile: platform/api/Dockerfile network_mode: host environment: PLATFORM_DB_URL: postgres://postgres:postgres@localhost:5434/rootnode_platform_sim PROVISIONER_URL: "http://localhost:4101" PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default} PLATFORM_API_PORT: "4301" depends_on: postgres: condition: service_healthy provisioner: condition: service_started # Simulates rn-platform-01's Gateway — deliberately NOT host-networked, deliberately not # attached to any Lab bridge network. This is the isolation under test. gateway: build: ../../../platform/gateway networks: - sim-platform-net environment: GATEWAY_PORT: "4200" # Reaches host-networked Provisioner/API via sim-platform-net's own gateway IP — the # same "reachable via the bridge's gateway interface" mechanism egress-proxy already # relies on (see docker-compose.dev.yml). run-simulation.sh fills these in. PROVISIONER_URL: ${SIM_PROVISIONER_URL:?set by run-simulation.sh} PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default} PLATFORM_API_URL: ${SIM_PLATFORM_API_URL:?set by run-simulation.sh} LAB_ROUTER_TLS_CA: /tls/ca.pem LAB_ROUTER_TLS_CERT: /tls/gateway-client-cert.pem LAB_ROUTER_TLS_KEY: /tls/gateway-client-key.pem volumes: - ${SIM_TLS_DIR:?set by run-simulation.sh}:/tls:ro ports: - "4210:4200" # exposed to the test driver script only, not "public" in any real sense depends_on: - provisioner - api # Simulates rn-lab-host-01's Lab Host Router — host-networked like egress-proxy, for the same # reason (needs to reach whatever per-Lab bridge networks the Provisioner creates, which are # not known/attachable ahead of time). lab-router: build: ../../../platform/lab-router network_mode: host environment: LAB_ROUTER_PORT: "8443" LAB_ROUTER_TLS_CA: /tls/ca.pem LAB_ROUTER_TLS_CERT: /tls/router-server-cert.pem LAB_ROUTER_TLS_KEY: /tls/router-server-key.pem DOCKER_SOCKET: /var/run/docker.sock volumes: - /var/run/docker.sock:/var/run/docker.sock - ${SIM_TLS_DIR:?set by run-simulation.sh}:/tls:ro networks: # Pre-created by run-simulation.sh (so it can inspect the real gateway IP BEFORE any # container starts and hand it to provisioner/gateway/lab-router as config) — Compose is # told to use that existing network rather than create its own. sim-platform-net: external: true name: ${COMPOSE_NET_NAME:?set by run-simulation.sh}