summaryrefslogtreecommitdiffstats
path: root/packaging/caddy/meshbay.org.Caddyfile
blob: c16120227968f24246b4437b57f830e73fe95706 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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/<hash>/*    the versioned module graph. The old snippet in
#                  devel-phases-next.md 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/<hash>/*, /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}
		}
	}
}