Files
unibus/deploy/caddy/Caddyfile
T
egutierrez e3f40913bc chore(deploy): version the same-origin Caddy config for uniweb
Capture the reverse-proxy vhost that fronts the browser-native uniweb
client on magnus (chat-<hash>.organic-machine.com): the SPA at /, the
signed control plane under /api (prefix stripped so request signatures
verify), and the NATS-over-WebSocket data plane under /nats. One origin
means no CORS and keeps the cluster node IPs hidden behind the proxy.

Self-contained fragment (includes the shared security_headers snippet) so
it validates with `caddy validate` on its own; the other vhosts on magnus
carry basic-auth secrets and are intentionally left out of git. Documents
the matching membershipd flags this config requires (--cors-origins with
the same-origin host, --trusted-proxies naming the Caddy node).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:43:15 +02:00

85 lines
3.3 KiB
Caddyfile

# Same-origin reverse proxy for the browser-native uniweb chat client.
#
# This is the self-contained fragment that exposes uniweb on magnus
# (organic-machine.com). It is merged into magnus's /etc/caddy/Caddyfile, which
# also hosts unrelated services; only this service's blocks are versioned here
# (the other vhosts carry basic-auth secrets that do not belong in git). The live
# file imports the shared (security_headers) snippet that is duplicated below so
# this fragment validates on its own.
#
# One origin fronts the whole app so the SPA and the bus share an origin: no CORS,
# and the unibus cluster node IPs stay hidden behind this proxy. Caddy obtains and
# renews the Let's Encrypt certificate automatically (the *.organic-machine.com
# wildcard A record points here).
#
# / -> the static SPA (uniweb web/dist) with a single-page-app fallback
# /api/* -> the signed HTTPS control plane (membershipd :8470), prefix stripped
# /nats -> the NATS-over-WebSocket data plane (:8485 magnus / :8480 peers)
#
# Upstreams speak TLS with the bus's own self-signed CA, so Caddy skips upstream
# verification (the hop is still encrypted). The control plane signs requests over
# the UNPREFIXED path, so /api MUST be stripped (handle_path) or signatures fail.
#
# The membershipd nodes must run with the same-origin host in --cors-origins (so
# the NATS WebSocket Origin check accepts it) and with --trusted-proxies naming
# this Caddy node (127.0.0.1,::1,135.125.201.30) so the per-IP rate limit keys on
# the real client behind the proxy instead of collapsing to the proxy's one IP.
(security_headers) {
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
-Server
}
}
chat-c200aa64c3125ce8b5f068e0.organic-machine.com {
import security_headers
# Control plane: strip /api so /api/rooms reaches membershipd as /rooms (the
# path the client signs). Prefer the local node; lb_try_duration retries the
# next node within the request on a dial error (safe: a refused connection sent
# no bytes, so even a POST cannot double-apply), and fail_duration plus the
# active /healthz check take a down node out of rotation.
handle_path /api/* {
reverse_proxy https://127.0.0.1:8470 https://141.94.69.66:8470 https://51.91.100.142:8470 {
transport http {
tls_insecure_skip_verify
}
lb_policy first
lb_try_duration 5s
lb_try_interval 250ms
fail_duration 10s
health_uri /healthz
health_interval 10s
health_timeout 5s
}
}
# Data plane: NATS over WebSocket. Strip /nats so the upgrade reaches the ws
# listener at its root. Caddy proxies the WebSocket upgrade natively. The ws
# listener speaks TLS on :8485 (magnus; :8480 is taken by unibus_admin there)
# and :8480 on the peers. Passive failover only (an HTTP health probe would be
# rejected by the NATS ws endpoint).
handle_path /nats* {
reverse_proxy https://127.0.0.1:8485 https://141.94.69.66:8480 https://51.91.100.142:8480 {
transport http {
tls_insecure_skip_verify
}
lb_policy first
lb_try_duration 5s
lb_try_interval 250ms
fail_duration 30s
}
}
# SPA: static files with a client-side-routing fallback to index.html.
handle {
root * /opt/uniweb/dist
try_files {path} /index.html
file_server
}
}