# 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 } }