fix(deploy): give migrate the API's client-address settings, and say what Doctor checked #57

Merged
Zayin_Admin merged 2 commits from fix/doctor-client-address into main 2026-10-06 01:18:19 +00:00
5 changed files with 37 additions and 7 deletions
Showing only changes of commit f1d6501239 - Show all commits
+9 -1
View File
@@ -905,17 +905,25 @@ function databaseTransportCheck(
}
}
/** Doctor reads only its own process's environment, so it says so: run as the reference
* deployment's `migrate`, it is given the API's values by the proxy overlay, but run any other way
* it may be checking a configuration the API is not running. */
const CLIENT_ADDRESS_SCOPE =
"Checked: the environment Doctor runs with. In the reference deployment `migrate` is given the API's client-address values; a running API's own value is shown by `docker compose exec api printenv EKKLESIA_CLIENT_ADDRESS_MODE`.";
function clientAddressCheck(input: ClientAddressConfigInput | undefined): DoctorCheck {
try {
const config = createClientAddressConfig(input, TRUSTED_CLIENT_ADDRESS_HEADER);
if (config.mode === "direct") {
return freezeCheck("identity.client-address", "pass", "Direct client-address mode", [
"Trusts only the actual immediate socket peer for rate limiting; no forwarded header is trusted.",
CLIENT_ADDRESS_SCOPE,
]);
}
return freezeCheck("identity.client-address", "pass", "Trusted-proxy client-address mode", [
`Trusts header "${config.header}" only from a request whose immediate socket peer matches one of ${config.trustedProxies.length} configured trusted ${plural(config.trustedProxies.length, "proxy")}.`,
"The reverse proxy must overwrite (never append to) this header, and the origin must not be reachable except through it.",
CLIENT_ADDRESS_SCOPE,
]);
} catch (error) {
const code =
@@ -926,7 +934,7 @@ function clientAddressCheck(input: ClientAddressConfigInput | undefined): Doctor
"identity.client-address",
"failure",
"Client-address configuration invalid",
[`[${code}] Configured client-address values failed validation.`],
[`[${code}] Configured client-address values failed validation.`, CLIENT_ADDRESS_SCOPE],
);
}
}
+1
View File
@@ -117,6 +117,7 @@ Identity
Identity runtime configuration (signing key, base URL, trusted origins) is not set.
✓ Direct client-address mode
Trusts only the actual immediate socket peer for rate limiting; no forwarded header is trusted.
Checked: the environment Doctor runs with. In the reference deployment \`migrate\` is given the API's client-address values; a running API's own value is shown by \`docker compose exec api printenv EKKLESIA_CLIENT_ADDRESS_MODE\`.
Object storage
○ Object storage not configured
+8
View File
@@ -440,6 +440,14 @@ for a development checkout, not for the image.
`migrate` has `profiles: ["ops"]`, so `up` never starts it. It never restarts, has no health check,
and its default command is the read-only plan. **Nothing applies migrations automatically.**
Run operator commands with the same Compose files as the stack. `COMPOSE_FILE` in `deploy/.env`
(set in [`env/compose.env.example`](env/compose.env.example)) does that for every plain
`docker compose` command below. It matters for Doctor in particular: Doctor checks the
client-address configuration of the process it runs in. The proxy overlay gives `migrate` the
API's trusted-proxy settings, so Doctor reports what the API runs. Without the overlay, Doctor
reports direct mode for an API that trusts its proxy. The running API's own value is shown by
`docker compose exec api printenv EKKLESIA_CLIENT_ADDRESS_MODE`.
| Task | Command (from `deploy/`) |
| --- | --- |
| Plan migrations (read-only) | `docker compose run --rm migrate` |
+13 -6
View File
@@ -13,6 +13,13 @@
# its lower half; the proxy (and the tunnel connector, if used) take addresses from the upper half,
# which Docker never hands to another container.
# Exactly one trusted peer: this proxy. Set from the same variable that fixes the proxy's address,
# so the two cannot drift apart. These override any value in ekklesia.env.
x-ekklesia-client-address: &ekklesia-client-address
EKKLESIA_CLIENT_ADDRESS_MODE: trusted-proxy
EKKLESIA_CLIENT_ADDRESS_HEADER: x-forwarded-for
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: ${EKKLESIA_PROXY_ADDRESS:-10.249.250.254}/32
services:
proxy:
image: caddy:2.11.4-alpine@sha256:6aeddd44c3078b0f9a35206472a11420648a79c184603ef95957d0a20044cb2b
@@ -59,12 +66,12 @@ services:
api:
networks:
edge: {}
environment:
# Exactly one trusted peer: this proxy. Set here from the same variable that fixes the proxy's
# address, so the two cannot drift apart. These override any value in ekklesia.env.
EKKLESIA_CLIENT_ADDRESS_MODE: trusted-proxy
EKKLESIA_CLIENT_ADDRESS_HEADER: x-forwarded-for
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: ${EKKLESIA_PROXY_ADDRESS:-10.249.250.254}/32
environment: *ekklesia-client-address
# Operator commands (migrations, Doctor) run as `migrate`. It gets the API's client-address
# configuration too, so that Doctor checks what the API actually runs rather than an empty one.
migrate:
environment: *ekklesia-client-address
object-storage:
networks:
+6
View File
@@ -2,6 +2,12 @@
# Compose reads automatically from this directory. Nothing here is a secret; secrets live in the
# per-service files under EKKLESIA_ENV_DIR.
# Which Compose files make up this installation, so that every plain `docker compose …` command
# run from this directory, operator commands through `migrate` included, sees the same services and
# settings as the running stack. Add compose.tunnel.yaml if you use the tunnel connector. Without
# the proxy overlay here, Doctor checks a `migrate` that lacks the API's client-address settings.
COMPOSE_FILE=compose.yaml:compose.proxy.yaml
# The image to run, by registry, name, and tag (or digest). Required.
EKKLESIA_IMAGE=