# SITE_ADDRESS is the domain to serve (e.g. lottery.example.com) — Caddy # automatically requests a Let's Encrypt certificate for it. # # Left at the default "localhost" (dev mode, no domain), Caddy detects it's # not a public hostname and issues a locally-trusted self-signed certificate # instead, via its internal CA. Browsers will still warn on first visit # unless that CA is explicitly trusted — expected for local/dev use. {$SITE_ADDRESS:localhost} { # gzip buffers output, which would delay delivery on the SSE stream # (/rounds/stream, app/api/routes/rounds.py) — it needs each event flushed # to the client immediately, not batched. Everything else still compresses. @not_sse { not path /rounds/stream } encode @not_sse gzip # B-43: Caddy adds none of these on its own. The JWT lives in # localStorage, so any XSS exfiltrates it — CSP is the main mitigation. # script-src/style-src need 'unsafe-inline' because both SPAs # (app/static/index.html, admin.html) use inline onclick handlers and # style="" attributes throughout; removing those is a separate, # larger refactor, not a header change. fonts.googleapis.com/gstatic.com # are the one external asset (the Google Fonts @import in style.css/admin.css). header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'" } # B-54: Caddy *appends* the real peer address to whatever X-Forwarded-For the # client sent, so without this the header arrives as ", " and every IP-keyed control in the app (the login and # registration throttles of B-33, the SSE per-IP subscriber cap of B-38) is # defeated by simply rotating a fake value per request. Overwriting the header # with the actual peer makes the app's assumption true at the source; it also # reads the last hop rather than the first (app/api/client_ip.py), so the two # defences hold independently. reverse_proxy app:8123 { header_up X-Forwarded-For {remote_host} } }