Ctrl K
ComunidadGuías…

docker/Caddyfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Edge for MDMesh. Serves the SPA and reverse-proxies the API/files/wake-socket to the server.
# SITE_ADDRESS is set by the setup wizard:
#   own-domain  → "mdm.example.com" (Caddy auto-provisions + renews a Let's Encrypt cert)
#   cloudflare  → ":80" (Cloudflare terminates TLS; cloudflared connects to this internally)
{
	email "{$ACME_EMAIL}"
	# Who Caddy believes about a client's address ({client_ip}, which the server records as a device's public IP).
	# Set per listener, so each hosting mode gets its own rule. Strict mode reads a header list from the right, so an
	# address a client prepends never counts.
	#   :443  own domain. Devices connect to Caddy directly, so nothing they send is believed: only a peer in
	#         TRUSTED_PROXIES may set X-Forwarded-For. The compose files pass it from .env; empty there (the default)
	#         trusts no peer, and unset, loopback only, which no Docker peer is.
	#   :80   set by docker/edge-peers.sh (the image's entrypoint), by hosting mode. Cloudflare tunnel: cloudflared's
	#         CF-Connecting-IP, from a private peer. Anything else (the dev stack, a proxy of your own in front):
	#         TRUSTED_PROXIES and X-Forwarded-For, as on :443. The defaults here are that second rule.
	# Own-domain's :80 only redirects to HTTPS, so the :80 rule never applies to its traffic.
	servers :443 {
		trusted_proxies static {$TRUSTED_PROXIES:127.0.0.1/32 ::1/128}
		trusted_proxies_strict
	}
	servers :80 {
		trusted_proxies static {$HTTP_TRUSTED_PEERS:127.0.0.1/32 ::1/128}
		client_ip_headers {$HTTP_CLIENT_IP_HEADER:X-Forwarded-For}
		trusted_proxies_strict
	}
}

# Every reverse_proxy to the server imports this. The server reads the client's address from X-Forwarded-For (and
# X-Real-IP, in code inherited from Headwind), so send exactly one value, Caddy's {client_ip}, and drop a client-sent
# CF-Connecting-IP. header_up runs after reverse_proxy's own X-Forwarded-For handling, so it wins.
(client_address) {
	header_up X-Forwarded-For {client_ip}
	header_up X-Real-IP {client_ip}
	header_up -CF-Connecting-IP
}

# Browser security headers, on every response the edge sends, the error fallback's included (handle_errors imports
# this too). No MIME sniffing. No framing: nothing MDMesh serves is meant to be framed (the console, its QR view and
# the recovery page are top-level pages, and the agent has no WebView). No Referer to other sites; the map's tile
# images send just the origin, which OpenStreetMap requires (web/src/ui/LocationMap.tsx). `defer` applies them
# after the upstream's own headers, so these are the values the browser gets.
# HSTS only where the browser is on HTTPS: Caddy's own TLS (own domain), or a proxy in front that says so in
# X-Forwarded-Proto (Cloudflare tunnel, your own proxy). A browser ignores HSTS received over plain HTTP, so a forged
# X-Forwarded-Proto gains nothing, and HTTP-only (LAN) installs are unaffected. Six months, this host only, no preload.
# Importers define @https (a site's named matchers are visible in its handle_errors, and may be defined only once).
(security_headers) {
	header {
		defer
		X-Content-Type-Options nosniff
		X-Frame-Options DENY
		Content-Security-Policy "frame-ancestors 'none'"
		Referrer-Policy same-origin
	}
	header @https {
		defer
		Strict-Transport-Security "max-age=15552000"
	}
}

{$SITE_ADDRESS} {
	encode gzip
	@https expression `{http.request.scheme} == "https" || {http.request.header.X-Forwarded-Proto} == "https"`
	import security_headers

	# A server path that the server's routing would read differently from its security filters: a ";" path
	# parameter, or an encoded ";", "/" or "\" (or a raw "\"), anywhere in the path of /rest, /files or /agent (the
	# query string is not looked at). Refused here with 400, before it is proxied; the server refuses these too
	# (AmbiguousPathFilter), for installs without this edge. {http.request.uri} is the path as sent, still escaped.
	@ambiguous {
		path_regexp ^(?i)/(rest|files|agent)([/;\\]|$)
		expression `{http.request.uri}.matches("^(?i)[^?]*(;|%3b|%2f|%5c|\\\\)")`
	}
	handle @ambiguous {
		respond 400
	}

	# Canonical agent-APK URL: /files/agent.apk works on BOTH deployment shapes (native serves a
	# real file from Tomcat; here it's the supervisor's sha256-verified release mirror — the same
	# upstream as /update/agent.apk, which stays below for back-compat). The exact-path matcher
	# outranks the /files/* → server route (Caddy sorts handles longest-path-first), so this must
	# stay more specific than @backend.
	handle /files/agent.apk {
		rewrite * /update/agent.apk
		reverse_proxy supervisor:9000
	}

	# API, uploaded files, and the agent wake WebSocket → the Tomcat server. reverse_proxy
	# upgrades WebSockets automatically, so /agent/ws works without extra config.
	@backend path /rest/* /files/* /agent/ws/*
	handle @backend {
		reverse_proxy server:8080 {
			import client_address
		}
	}

	# Updater status + recovery page → the decoupled supervisor (always up, independent of the
	# server), so "update available" + recovery stay reachable even mid-update.
	handle /update/* {
		reverse_proxy supervisor:9000
	}
	handle /recovery* {
		reverse_proxy supervisor:9000
	}

	# Health probes for uptime monitors. Both answer 200 "ok" or 503, never the SPA's catch-all 200:
	#   /healthz            the edge AND the API server answer (the probe apply.sh/rollback.sh use to call
	#                       an update healthy: GET /rest/public/name, no DB round-trip, no auth)
	#   /healthz/supervisor the updater/recovery supervisor answers (update checks, recovery page, and the
	#                       /files/agent.apk mirror that new enrollments download)
	# Each also answers with a trailing slash (exact paths only, nothing below them), so /healthz/ cannot fall through
	# to the SPA's 200. `rewrite` inside reverse_proxy changes only the upstream request, so handle_errors below still
	# sees the probe path.
	@healthz path /healthz /healthz/
	@healthzSupervisor path /healthz/supervisor /healthz/supervisor/
	handle @healthz {
		reverse_proxy server:8080 {
			import client_address
			rewrite /rest/public/name
			@up status 2xx
			handle_response @up {
				respond "ok" 200
			}
			handle_response {
				respond "server unavailable" 503
			}
		}
	}
	handle @healthzSupervisor {
		reverse_proxy supervisor:9000 {
			rewrite /healthz
			@up status 2xx
			handle_response @up {
				respond "ok" 200
			}
			handle_response {
				respond "supervisor unavailable" 503
			}
		}
	}

	# Everything else is the single-page app.
	handle {
		root * /srv
		try_files {path} /index.html
		file_server
	}

	# If the server (API) is down/erroring, fall back to the supervisor's recovery page. Not for the health
	# probes: proxied as-is, /healthz would reach the supervisor's own /healthz and call a dead server "ok".
	handle_errors {
		import security_headers
		handle @healthz {
			respond "server unavailable" 503
		}
		handle @healthzSupervisor {
			respond "supervisor unavailable" 503
		}
		handle {
			reverse_proxy supervisor:9000
		}
	}
}