CI / verify (pull_request) Successful in 51s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 1m49s
CI / image-build (pull_request) Successful in 17s
CI / deploy-smoke (same-origin) (pull_request) Successful in 48s
CI / deploy-smoke (split) (pull_request) Successful in 55s
An installation can now serve the Dashboard and the API on sibling hostnames (dash./api.) beside the default same-origin topology, from one image and with configuration alone. - The Dashboard learns its API origin at run time: the built page carries one empty <meta name="ekklesia-api-origin">, which the API fills with its identity base URL's origin at startup, before computing the page's ETag. The shell accepts only a canonical http(s) origin and otherwise fails closed without making a request. The build-time EKKLESIA_API_BASE_URL is gone from the bundle; the development server injects the same element. - The display host stays on the API's origin, so its device cookies, requests, and offline cache are unchanged. - The API trusts its own origin (trusted origins plus the identity base URL), as Better Auth already does, so the display page's mutations pass the origin check without operators listing it. - Doctor validates EKKLESIA_DASHBOARD_ORIGIN exactly as the API's startup does (it previously skipped it), reports the topology without printing hostnames, and warns when split hostnames are not siblings. - The module UI contract states the CORS header limit (safelisted headers plus Content-Type, safelisted response headers only). - Reference deployment: Caddyfile.split.example (Dashboard hostname serves / and its script; API hostname serves /api/*, /display/*, and the display script; each refuses the other's paths) and EKKLESIA_DASHBOARD_HOSTNAME in the proxy overlay and env examples. - The deploy smoke test runs either topology (CI runs both). The split run installs the module template as a test-only module, checks per-hostname routing and exact CORS, then drives a digest-pinned headless Chromium over the DevTools protocol, with no added dependency: sign-in on the Dashboard hostname, the session cookie host-only on the API hostname, the module UI imported cross-origin and its credentialed request succeeding, the display and its service worker on the API hostname, and sign-out. ADR 0045 is marked Accepted. Co-Authored-By: Claude Opus 5.5 <[email protected]>
110 lines
4.2 KiB
Caddyfile
110 lines
4.2 KiB
Caddyfile
# Ekklesia reference proxy configuration for the split topology (ADR 0045): the Dashboard and the API
|
|
# on separate hostnames of one site, for example dash.example.org and api.example.org. For the
|
|
# default same-origin topology use Caddyfile.example instead. Copy this file to deploy/Caddyfile.
|
|
#
|
|
# Hostnames, the ACME contact, and the tunnel connector's address come from the proxy service's
|
|
# environment (deploy/.env): EKKLESIA_DASHBOARD_HOSTNAME is the Dashboard's, EKKLESIA_HOSTNAME the
|
|
# API's (which also serves the display host), EKKLESIA_STORAGE_HOSTNAME the storage origin's. Only
|
|
# the two site blocks below differ from Caddyfile.example.
|
|
{
|
|
# ACME account contact. Certificates use the HTTP-01 challenge on port 80 by default, which needs
|
|
# all three hostnames to resolve publicly to this host with 80 and 443 reachable.
|
|
email {$EKKLESIA_ACME_EMAIL}
|
|
|
|
# DNS-01, for an installation that keeps Ekklesia off the public Internet, serves only a LAN
|
|
# (split-horizon DNS), or is reachable only through Cloudflare Tunnel. Cloudflare terminates TLS at
|
|
# its edge, so HTTP-01 cannot reach this proxy through a tunnel.
|
|
#
|
|
# DNS-01 needs a DNS provider module that the stock Caddy image does not contain. Build an image
|
|
# with it (for example `xcaddy build --with github.com/caddy-dns/cloudflare`), pin that image by
|
|
# digest in your own overlay, give it CLOUDFLARE_API_TOKEN through a root-owned env file, then
|
|
# enable:
|
|
#
|
|
# acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
|
|
|
servers {
|
|
# A client-address header is honored only from the tunnel connector's fixed address
|
|
# (compose.tunnel.yaml). From every other peer, the client address is the TCP peer itself and
|
|
# any such header is ignored. Without a tunnel, nothing has this address.
|
|
trusted_proxies static {$EKKLESIA_TUNNEL_ADDRESS}/32
|
|
client_ip_headers CF-Connecting-IP
|
|
trusted_proxies_strict
|
|
}
|
|
|
|
# Access logs are off unless enabled. If you enable them, keep request headers out: they carry
|
|
# cookies and forwarding chains (docs/development/security.md, launch checklist item 7).
|
|
}
|
|
|
|
# What every request to the API carries upstream: exactly one forwarding header, overwritten with
|
|
# the client address Caddy resolved, and none of the headers a caller could use to choose its own.
|
|
(ekklesia_forwarding) {
|
|
header_up X-Forwarded-For {client_ip}
|
|
header_up -CF-Connecting-IP
|
|
header_up -X-Real-IP
|
|
header_up -Forwarded
|
|
header_up -True-Client-IP
|
|
}
|
|
|
|
# The Dashboard: its page and its one script, nothing else. Emailed verification and reset links land
|
|
# on `/` with a query. `/api/*` is refused here on purpose, so the session cookie has exactly one
|
|
# home, the API hostname, and the Dashboard can never fall back to a same-origin API.
|
|
{$EKKLESIA_DASHBOARD_HOSTNAME} {
|
|
@dashboard path / /assets/ekklesia-dashboard/*
|
|
|
|
handle @dashboard {
|
|
reverse_proxy api:3000 {
|
|
import ekklesia_forwarding
|
|
}
|
|
}
|
|
|
|
handle {
|
|
respond 404
|
|
}
|
|
}
|
|
|
|
# The API, and the display host on the API's own origin: a screen's pages, service worker, device
|
|
# cookies, and requests are all same-origin (ADR 0045). Nothing is served at `/`.
|
|
{$EKKLESIA_HOSTNAME} {
|
|
@api path /api/* /display/* /assets/ekklesia-display/*
|
|
|
|
# Module realtime is Server-Sent Events: flush every event immediately.
|
|
@realtime path /api/v1/module-realtime/*
|
|
|
|
handle @realtime {
|
|
reverse_proxy api:3000 {
|
|
import ekklesia_forwarding
|
|
flush_interval -1
|
|
}
|
|
}
|
|
|
|
handle @api {
|
|
# Request bodies stream through unbuffered and unlimited here; the API enforces the platform's
|
|
# ceilings itself (8 KiB for authentication, up to 32 MiB for a streamed module route).
|
|
reverse_proxy api:3000 {
|
|
import ekklesia_forwarding
|
|
}
|
|
}
|
|
|
|
handle {
|
|
respond 404
|
|
}
|
|
}
|
|
|
|
# Browsers fetch signed object URLs here (S3_DELIVERY_ENDPOINT, ADR 0031).
|
|
{$EKKLESIA_STORAGE_HOSTNAME} {
|
|
reverse_proxy https://object-storage:8443 {
|
|
# An S3 signature covers the Host header: pass the original through unchanged, or every signed
|
|
# URL fails verification.
|
|
header_up Host {host}
|
|
header_up -CF-Connecting-IP
|
|
header_up -X-Real-IP
|
|
header_up -Forwarded
|
|
header_up -True-Client-IP
|
|
transport http {
|
|
# object-storage presents a certificate from the installation-private CA.
|
|
tls_trust_pool file /etc/caddy/ekklesia-ca.crt
|
|
tls_server_name object-storage
|
|
}
|
|
}
|
|
}
|