CI / verify (pull_request) Successful in 54s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m48s
CI / image-build (pull_request) Successful in 27s
CI / deploy-smoke (pull_request) Successful in 42s
Implement ADR 0044's reference deployment as a base Compose file plus overlays, so an installation that already runs a proxy can use the base alone. - compose.yaml: api, postgres, object-storage, and the operator-only migrate (profile ops, plan by default). No published port. api and migrate run init, read-only, tmpfs /tmp, cap_drop ALL, no-new-privileges. postgres is only on the internal data network. - TLS everywhere from one installation-private CA: PostgreSQL TLS 1.3 only, with pg_hba rejecting every non-TLS connection, and the API on verify-full via EKKLESIA_DATABASE_CA (PEM text). SeaweedFS serves S3 over HTTPS (ADR 0030 requires https in production), trusted by the API through NODE_EXTRA_CA_CERTS. No Core change. - compose.proxy.yaml + Caddyfile.example: Caddy pinned by digest, ACME HTTP-01 (DNS-01 documented), routes /, /api/*, /display/*, /assets/* to api and refuses everything else; the storage hostname preserves Host. The API trusts exactly the proxy's fixed /32 on an edge network whose dynamic range excludes the fixed addresses; Caddy overwrites X-Forwarded-For and strips caller-supplied forwarding headers. - compose.tunnel.yaml: cloudflared pinned by digest, token from its own env file; Caddy honors CF-Connecting-IP only from its fixed address. - Four per-service root-owned env files, examples with no values; scripts/init-tls.sh (idempotent, never overwrites a key) and scripts/gen-secrets.sh (prints only). - CI: pnpm deploy:config in ci:verify for all three combinations, and a deploy-smoke job (scripts/smoke-deploy.sh) proving readiness, the Dashboard, signed storage delivery, the client-address chain, initial owner sign-in/out, verify-full database TLS, and Doctor through the proxy, against disposable services. tests/deploy.test.ts pins the structural guarantees. - ADR 0044 (still Proposed) amended to match: API-to-storage HTTPS, DNS-01 needs a custom Caddy build and tunnel-only needs DNS-01, the tunnel's origin TLS settings live in the Cloudflare dashboard, port 80 serves HTTP-01, and secrets are one env file per service. Co-Authored-By: Claude Opus 5.5 <[email protected]>
88 lines
3.4 KiB
Caddyfile
88 lines
3.4 KiB
Caddyfile
# Ekklesia reference proxy configuration (ADR 0044). Copy to deploy/Caddyfile, which the proxy
|
|
# overlay mounts read-only. Hostnames, the ACME contact, and the tunnel connector's address come from
|
|
# the proxy service's environment (deploy/.env), so this file normally needs no edits.
|
|
{
|
|
# ACME account contact. Certificates use the HTTP-01 challenge on port 80 by default, which needs
|
|
# the Ekklesia and storage 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
|
|
}
|
|
|
|
{$EKKLESIA_HOSTNAME} {
|
|
# Everything the API serves: the Dashboard at /, the API, display pages and their service
|
|
# worker, and the Dashboard and display host scripts. Any other path is refused here.
|
|
@ekklesia path / /api/* /display/* /assets/*
|
|
|
|
# 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 @ekklesia {
|
|
# 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
|
|
}
|
|
}
|
|
}
|