# Caddy configuration for meshbay.org # # Two things share one origin: the public site (`site/`, static, meshbay.org # specific) and the hub (FastAPI on loopback:8000, generic and reusable). # # The rule is an ALLOWLIST for the site, and everything else to the hub. # Not the reverse. The hub mounts its whole static directory at "/" # (`app.py`, RevalidatingStatics), so a `root * site` with `try_files` would # shadow it and break the application in ways that are not obvious: # # /sw.js the service worker MUST stay at the root or its scope stops # covering the pages it intercepts downloads for. A 404 here # silently breaks streamed downloads on Firefox and Safari. # /a//* the versioned module graph. The old snippet proxied # `/style.css` and `/*.js`, which matches neither this prefix # nor /locales/*.js — it predates asset versioning and would # 404 the entire bundle. # /style.css old bookmarks, still served unversioned by the hub. # # Deployment: the site is NOT pushed by the hub deploy procedure. Sync it # separately to /srv/meshbay/site (see QE/server-state/meshbay.org.md). meshbay.org { encode zstd gzip root * /srv/meshbay/site # The public site. Extensionless URLs work: /about → about.html. # Keep this list explicit — anything not named here belongs to the hub. @site path / /about /about.html /downloads /downloads.html /assets/* handle @site { # These pages are pure HTML and CSS: no script, no external asset, no # form. The policy says exactly that, so an injection has nowhere to go. header { Content-Security-Policy "default-src 'none'; style-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" # Deliberate: this host is HTTPS only. Removing it later takes # max-age to expire in every browser that saw it. Strict-Transport-Security "max-age=31536000; includeSubDomains" } try_files {path} {path}.html file_server } # Everything else is the hub: /v1/*, /app, /app/*, /a//*, /sw.js, # /style.css, /locales/*, /vendor/*, and the /v1/nodes/ws WebSocket # (reverse_proxy upgrades it without extra configuration). # # The hub sets its own CSP for the application, which needs # `wasm-unsafe-eval` for the Argon2id bundle KDF. Do not add a header here: # a second policy on the same response is intersected with the first, and # the strictest wins — which would lock every user out of their keys. handle { reverse_proxy 127.0.0.1:8000 { # The hub honours X-Forwarded-For from a trusted proxy only, and # reads the rightmost hop (draft-v5 §6.4). Caddy's default is to # APPEND the real address to whatever the client sent, which the # rightmost-hop rule already handles; this replaces it outright so # nothing a client invents ever reaches the compliance log. header_up X-Forwarded-For {remote_host} } } }