Files
ced4568 d2280d0f0d feat(security): production Compose topology, firewall rulesets, Docker mTLS
Named-bridge-network production Compose (not host networking, unlike dev)
for rn-platform-01; egress-proxy + agents for rn-lab-host-01. Caddy reverse
proxy, single public origin, Grafana restricted to management_cidr.
iptables-restore firewall rulesets for all three hosts (Proxmox host,
rn-platform-01, rn-lab-host-01) codifying the Platform/Lab-Host boundary:
no student-to-student traffic, no direct Proxmox exposure, no direct
Postgres/Provisioner exposure from the Lab Host.

Mutual TLS for the Provisioner<->Lab Host Docker connection: certificate
generation/verification scripts, dockerd TLS config, firewall-restricted
port 2376. Application support already existed since Phase 1; this is
purely the operational half.

Validated: docker compose config, caddy validate, firewall structural lint,
and a real openssl s_server/s_client mTLS handshake proving the cert chain
and mutual-auth rejection (legitimate client verified, rogue client
correctly rejected with verify code 19).
2026-08-18 17:13:09 -05:00

39 lines
1.6 KiB
Caddyfile

# Caddy 2 — single stable RootNode Academy origin, automatic HTTPS (Let's Encrypt) unless
# Cloudflare origin certs are used instead (see docs/PRODUCTION_NETWORK_SECURITY.md §8 for the
# Cloudflare-vs-direct-LE decision). Rendered by the `platform` Ansible role from
# infra/ansible/group_vars/production.yml's `academy_domain`.
#
# The student/instructor path proxies to the Gateway (platform/gateway) — the sole
# student-facing entrypoint, unchanged architectural rule from Phase 2A onward. Nothing here
# reaches the Provisioner, Postgres, or Docker TLS directly; those have no public route at all.
# Grafana is reachable at /grafana/ but restricted to the management CIDR even though it has
# its own login — internal observability tooling shouldn't be internet-facing at all, same
# posture as everything else that isn't the student/instructor product surface.
{{ academy_domain }} {
encode gzip zstd
handle_path /grafana/* {
@notmanagement not remote_ip {{ management_cidr }}
respond @notmanagement 403
reverse_proxy grafana:3000
}
# WebSocket terminal traffic (shell-ws) needs the connection upgraded, not buffered —
# Caddy's reverse_proxy handles this automatically for Upgrade: websocket requests, no
# special directive needed, but flush_interval -1 avoids buffering SSE/long-poll-shaped
# responses too. `gateway` is the Docker Compose service name (rn-platform-internal
# network), not localhost — Caddy no longer shares host networking with the Gateway in
# production, unlike the dev compose file.
reverse_proxy gateway:4200 {
flush_interval -1
}
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 7
}
}
}