Compare commits
2
Commits
f1d6501239
...
4a9012f64c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9012f64c | ||
|
|
2a64383af5 |
@@ -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 {
|
function clientAddressCheck(input: ClientAddressConfigInput | undefined): DoctorCheck {
|
||||||
try {
|
try {
|
||||||
const config = createClientAddressConfig(input, TRUSTED_CLIENT_ADDRESS_HEADER);
|
const config = createClientAddressConfig(input, TRUSTED_CLIENT_ADDRESS_HEADER);
|
||||||
if (config.mode === "direct") {
|
if (config.mode === "direct") {
|
||||||
return freezeCheck("identity.client-address", "pass", "Direct client-address mode", [
|
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.",
|
"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", [
|
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")}.`,
|
`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.",
|
"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) {
|
} catch (error) {
|
||||||
const code =
|
const code =
|
||||||
@@ -926,7 +934,7 @@ function clientAddressCheck(input: ClientAddressConfigInput | undefined): Doctor
|
|||||||
"identity.client-address",
|
"identity.client-address",
|
||||||
"failure",
|
"failure",
|
||||||
"Client-address configuration invalid",
|
"Client-address configuration invalid",
|
||||||
[`[${code}] Configured client-address values failed validation.`],
|
[`[${code}] Configured client-address values failed validation.`, CLIENT_ADDRESS_SCOPE],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ Identity
|
|||||||
Identity runtime configuration (signing key, base URL, trusted origins) is not set.
|
Identity runtime configuration (signing key, base URL, trusted origins) is not set.
|
||||||
✓ Direct client-address mode
|
✓ Direct client-address mode
|
||||||
Trusts only the actual immediate socket peer for rate limiting; no forwarded header is trusted.
|
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
|
||||||
○ Object storage not configured
|
○ Object storage not configured
|
||||||
|
|||||||
@@ -958,6 +958,21 @@ describe("Ekklesia Doctor diagnostics", () => {
|
|||||||
expect(report.result).toBe("ready-with-warnings");
|
expect(report.result).toBe("ready-with-warnings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("says whose client-address configuration it checked: this process's, not a running API's", async () => {
|
||||||
|
for (const clientAddress of [
|
||||||
|
undefined,
|
||||||
|
{ header: "x-forwarded-for", mode: "trusted-proxy" as const, trustedProxies: ["10.0.0.1"] },
|
||||||
|
{ header: "x-forwarded-for", mode: "trusted-proxy" as const, trustedProxies: ["bad"] },
|
||||||
|
]) {
|
||||||
|
const report = await diagnoseEkklesia(
|
||||||
|
{ ...options(), ...(clientAddress ? { clientAddress } : {}) },
|
||||||
|
dependencies(),
|
||||||
|
);
|
||||||
|
const details = check(report, "identity.client-address").details;
|
||||||
|
expect(details.some((detail) => detail.includes("environment Doctor runs with"))).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("passes client-address configuration with an explicitly configured trusted-proxy mode", async () => {
|
it("passes client-address configuration with an explicitly configured trusted-proxy mode", async () => {
|
||||||
const report = await diagnoseEkklesia(
|
const report = await diagnoseEkklesia(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
`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.**
|
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/`) |
|
| Task | Command (from `deploy/`) |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Plan migrations (read-only) | `docker compose run --rm migrate` |
|
| Plan migrations (read-only) | `docker compose run --rm migrate` |
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
# its lower half; the proxy (and the tunnel connector, if used) take addresses from the upper half,
|
# its lower half; the proxy (and the tunnel connector, if used) take addresses from the upper half,
|
||||||
# which Docker never hands to another container.
|
# 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:
|
services:
|
||||||
proxy:
|
proxy:
|
||||||
image: caddy:2.11.4-alpine@sha256:6aeddd44c3078b0f9a35206472a11420648a79c184603ef95957d0a20044cb2b
|
image: caddy:2.11.4-alpine@sha256:6aeddd44c3078b0f9a35206472a11420648a79c184603ef95957d0a20044cb2b
|
||||||
@@ -59,12 +66,12 @@ services:
|
|||||||
api:
|
api:
|
||||||
networks:
|
networks:
|
||||||
edge: {}
|
edge: {}
|
||||||
environment:
|
environment: *ekklesia-client-address
|
||||||
# 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.
|
# Operator commands (migrations, Doctor) run as `migrate`. It gets the API's client-address
|
||||||
EKKLESIA_CLIENT_ADDRESS_MODE: trusted-proxy
|
# configuration too, so that Doctor checks what the API actually runs rather than an empty one.
|
||||||
EKKLESIA_CLIENT_ADDRESS_HEADER: x-forwarded-for
|
migrate:
|
||||||
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: ${EKKLESIA_PROXY_ADDRESS:-10.249.250.254}/32
|
environment: *ekklesia-client-address
|
||||||
|
|
||||||
object-storage:
|
object-storage:
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Vendored
+6
@@ -2,6 +2,12 @@
|
|||||||
# Compose reads automatically from this directory. Nothing here is a secret; secrets live in the
|
# Compose reads automatically from this directory. Nothing here is a secret; secrets live in the
|
||||||
# per-service files under EKKLESIA_ENV_DIR.
|
# 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.
|
# The image to run, by registry, name, and tag (or digest). Required.
|
||||||
EKKLESIA_IMAGE=
|
EKKLESIA_IMAGE=
|
||||||
|
|
||||||
|
|||||||
@@ -468,12 +468,16 @@ grep -q 'TLS required with certificate and hostname verification (explicit priva
|
|||||||
<<<"$doctor_output" || fail "Doctor did not report verify-full with the private CA."
|
<<<"$doctor_output" || fail "Doctor did not report verify-full with the private CA."
|
||||||
grep -q '✓ Object storage reachable' <<<"$doctor_output" ||
|
grep -q '✓ Object storage reachable' <<<"$doctor_output" ||
|
||||||
fail "Doctor could not reach object storage over HTTPS."
|
fail "Doctor could not reach object storage over HTTPS."
|
||||||
|
# Doctor runs as `migrate`; it must check the client-address configuration the API runs with, which
|
||||||
|
# behind the proxy is trusted-proxy mode, not the direct mode an unconfigured process reports.
|
||||||
|
grep -q 'Trusted-proxy client-address mode' <<<"$doctor_output" ||
|
||||||
|
fail "Doctor did not check the API's trusted-proxy client-address configuration."
|
||||||
for value in "$postgres_password" "$s3_secret_key" "$identity_secret"; do
|
for value in "$postgres_password" "$s3_secret_key" "$identity_secret"; do
|
||||||
if grep -qF -- "$value" <<<"$doctor_output"; then
|
if grep -qF -- "$value" <<<"$doctor_output"; then
|
||||||
fail "Doctor output contains a secret."
|
fail "Doctor output contains a secret."
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo "ok Doctor: READY, verify-full with the private CA, storage reachable, no secret printed"
|
echo "ok Doctor: READY, verify-full with the private CA, storage reachable, the API's client-address mode, no secret printed"
|
||||||
|
|
||||||
api_logs="$("${compose[@]}" logs --no-color api 2>&1)"
|
api_logs="$("${compose[@]}" logs --no-color api 2>&1)"
|
||||||
for value in "$postgres_password" "$s3_secret_key" "$identity_secret" "$owner_password"; do
|
for value in "$postgres_password" "$s3_secret_key" "$identity_secret" "$owner_password"; do
|
||||||
|
|||||||
@@ -153,6 +153,11 @@ describe("reference deployment compose files", () => {
|
|||||||
EKKLESIA_CLIENT_ADDRESS_MODE: "trusted-proxy",
|
EKKLESIA_CLIENT_ADDRESS_MODE: "trusted-proxy",
|
||||||
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: `${proxyAddress}/32`,
|
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: `${proxyAddress}/32`,
|
||||||
});
|
});
|
||||||
|
// Doctor runs as `migrate`. Given anything else, it validates a configuration the API is not
|
||||||
|
// running, and reports "Direct" for an installation that trusts its proxy.
|
||||||
|
expect(proxyOverlay.services?.migrate?.environment).toEqual(
|
||||||
|
proxyOverlay.services?.api?.environment,
|
||||||
|
);
|
||||||
expect(proxyOverlay.networks?.edge?.ipam?.config).toEqual([
|
expect(proxyOverlay.networks?.edge?.ipam?.config).toEqual([
|
||||||
{
|
{
|
||||||
ip_range: "${EKKLESIA_EDGE_DYNAMIC_RANGE:-10.249.250.0/25}",
|
ip_range: "${EKKLESIA_EDGE_DYNAMIC_RANGE:-10.249.250.0/25}",
|
||||||
|
|||||||
Reference in New Issue
Block a user