# The console, served on the WireGuard hub address. # # Runs inside the provisioning container's network namespace, so it listens on # 10.66.0.1 — the tunnel address itself. A VPN client therefore needs no route # beyond the management subnet it already has, and the request arrives at the # application with its real 10.66.0.x source, which is what the console's own # IP allowlist checks. # # The certificate is the one the public Caddy already obtains and renews for # this hostname. A certificate is bound to the NAME, not to the address that # serves it, so the same file is valid here. Shared read-only rather than # duplicated: a second ACME client would be a second thing to renew, and a # private address cannot satisfy an HTTP challenge in any case. { admin off auto_https off } https://{$VPN_INTERNAL_HOST}:443 { bind {$VPN_HUB_ADDRESS} tls {$VPN_CERT_PATH} {$VPN_KEY_PATH} # To the application directly. Resolvable because this container shares the # provisioning container's namespace, which is on the compose network. reverse_proxy app:80 { header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto https header_up Host {host} } } # A health port the deployment can ask, on the hub address and nowhere else. # # The site above matches on the console's HOSTNAME. A probe to the bare address # sends no SNI, is offered no certificate and fails the handshake — which is # indistinguishable from the gateway being down, and that is precisely the # mistake that left VPN_READY false on a healthy tunnel. Plain HTTP on a # separate port removes TLS, SNI and name resolution from the question and # answers only what is being asked: is this gateway listening, in this network # namespace, right now. # # Not a hole: 10.66.0.1 exists only inside the tunnel, and this port is never # published. Caddy also refuses to start when the certificate above cannot be # read, so a health port that answers proves the whole file loaded. http://{$VPN_HUB_ADDRESS}:{$VPN_HEALTH_PORT:8081} { # bind, not merely a matching hostname: without it Caddy listens on every # interface in the namespace and only MATCHES on the address. Nothing is # published, so it was not reachable from outside either way — but "on the # hub address and nowhere else" should be what the file actually does. bind {$VPN_HUB_ADDRESS} respond /healthz 204 respond 404 }