Merge pull request #846 from MBombeck/fix/restore-perms-trust-proxy

fix: write restored PHI 0o600 and let TRUST_PROXY_HOPS reach the container
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-08-29 18:15:06 +02:00
commit 5196aff766
5 files changed
+35 -9

No files matched your search

+6
View File
@@ -169,6 +169,12 @@ API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
# regex- and length-bounded), but the gating decision is up to the
# operator — there is no way for the server to detect whether the flag
# matches the real deployment topology. When in doubt, leave it off.
# Number of trusted reverse-proxy hops in front of the app. The client IP is
# read that many entries from the right of the X-Forwarded-For chain. Leave
# unset for a directly-exposed host; set to 1 behind a single reverse proxy.
# A wrong value makes the app refuse to read XFF and share one rate-limit
# bucket across all anonymous callers.
# TRUST_PROXY_HOPS="1"
# TRUST_CF_CONNECTING_IP=""
+6
View File
@@ -306,6 +306,12 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
# address and the geo lookup resolves the wrong location. Set this to "1"
# ONLY when Cloudflare is in front -- a direct-exposed self-host must leave
# it unset, or an attacker could forge the header. Off by default.
# Number of trusted reverse-proxy hops in front of the app; the client IP is
# read that many entries from the right of the X-Forwarded-For chain. Unset
# for a directly-exposed host, 1 behind a single reverse proxy. A wrong
# value makes the app refuse XFF and collapse anonymous callers into one
# rate-limit bucket.
# TRUST_PROXY_HOPS="1"
# TRUST_CF_CONNECTING_IP="1"
# -----------------------------------------------------------------------------
+7
View File
@@ -75,6 +75,13 @@ x-healthlog-env: &healthlog_env
# free ip-api.com endpoint) — off by default, an explicit operator choice.
# Listed here so operator values in .env reach the container (the
# `environment:` block is a whitelist — vars not listed never propagate).
# TRUST_PROXY_HOPS is the number of trusted reverse-proxy hops in front of
# the app: the client IP is read that many entries from the right of the
# X-Forwarded-For chain. Wrong value and the app refuses to read XFF and
# collapses every anonymous caller into one rate-limit bucket. Listed here
# so an operator value in .env reaches the container (the `environment:`
# block is a whitelist).
TRUST_PROXY_HOPS: "${TRUST_PROXY_HOPS:-}"
TRUST_CF_CONNECTING_IP: "${TRUST_CF_CONNECTING_IP:-}"
IP_GEO_LOOKUP_URL: "${IP_GEO_LOOKUP_URL:-}"
IP_GEO_LOOKUP_DISABLED: "${IP_GEO_LOOKUP_DISABLED:-}"
+13 -8
View File
@@ -1,6 +1,6 @@
{
"$schema": "./env-manifest.schema.json",
"description": "Manifest of env vars required by a HealthLog production deployment. Used by `pnpm check-env` to catch missing/empty vars BEFORE a deploy ships. v1.4.42 W6 — born out of the v1.4.40 AP-2 .p8 install gap that shipped silently for 3 days.",
"description": "Manifest of env vars required by a HealthLog production deployment. Used by `pnpm check-env` to catch missing/empty vars BEFORE a deploy ships. v1.4.42 W6 \u2014 born out of the v1.4.40 AP-2 .p8 install gap that shipped silently for 3 days.",
"groups": [
{
"name": "Core",
@@ -37,7 +37,7 @@
},
{
"name": "Withings OAuth",
"description": "Withings API credentials are entered per user in Settings → Integrations (no env-var path). Only the webhook shared secret is operator-level.",
"description": "Withings API credentials are entered per user in Settings \u2192 Integrations (no env-var path). Only the webhook shared secret is operator-level.",
"required": false,
"variables": [
{
@@ -100,7 +100,7 @@
},
{
"name": "OIDC SSO options",
"description": "Optional tuning for an enabled OIDC provider. Each stands alone (deliberately NOT allOrNone — any subset is valid) and each has a safe default when unset.",
"description": "Optional tuning for an enabled OIDC provider. Each stands alone (deliberately NOT allOrNone \u2014 any subset is valid) and each has a safe default when unset.",
"required": false,
"variables": [
{
@@ -157,7 +157,7 @@
},
{
"name": "APNs (iOS push)",
"description": "All four core APNS_* vars together. Missing any disables APNs silently — the v1.4.40 AP-2 gap that motivated this manifest. allOrNone trips the synthetic <all-or-none> required row whenever 1-3 of the 4 are set.",
"description": "All four core APNS_* vars together. Missing any disables APNs silently \u2014 the v1.4.40 AP-2 gap that motivated this manifest. allOrNone trips the synthetic <all-or-none> required row whenever 1-3 of the 4 are set.",
"required": false,
"allOrNone": true,
"variables": [
@@ -175,7 +175,7 @@
},
{
"name": "APNS_KEY",
"purpose": "APNs auth key (.p8 contents). Use APNS_KEY for 12-factor env-block deploys; APNS_KEY_FILE is the filesystem alternative — at least one of the two must be set.",
"purpose": "APNs auth key (.p8 contents). Use APNS_KEY for 12-factor env-block deploys; APNS_KEY_FILE is the filesystem alternative \u2014 at least one of the two must be set.",
"anyOf": ["APNS_KEY", "APNS_KEY_FILE"]
}
]
@@ -202,13 +202,13 @@
"variables": [
{
"name": "TLS_LEAF_SPKI_PINS",
"purpose": "Comma-separated known-good leaf SPKI pins, base64(sha256(DER subjectPublicKeyInfo)) — the same value the native client pins. Hold both the current and the next pin during a dual-pin renewal window."
"purpose": "Comma-separated known-good leaf SPKI pins, base64(sha256(DER subjectPublicKeyInfo)) \u2014 the same value the native client pins. Hold both the current and the next pin during a dual-pin renewal window."
}
]
},
{
"name": "SMTP (email channel)",
"description": "Operator SMTP transport for the email notification channel (v1.17.1). All of SMTP_HOST, SMTP_PORT, SMTP_FROM together enable the channel; SMTP_USER/SMTP_PASS are optional (unauthenticated relay). Missing any of the three core vars disables email silently — the channel is never offered and the Settings card hides itself.",
"description": "Operator SMTP transport for the email notification channel (v1.17.1). All of SMTP_HOST, SMTP_PORT, SMTP_FROM together enable the channel; SMTP_USER/SMTP_PASS are optional (unauthenticated relay). Missing any of the three core vars disables email silently \u2014 the channel is never offered and the Settings card hides itself.",
"required": false,
"allOrNone": true,
"variables": [
@@ -250,7 +250,7 @@
},
{
"name": "BACKUP_ENCRYPTION_KEY",
"purpose": "Separate AES key for the backup body — MUST NOT match ENCRYPTION_KEY."
"purpose": "Separate AES key for the backup body \u2014 MUST NOT match ENCRYPTION_KEY."
}
]
},
@@ -268,6 +268,11 @@
"name": "SESSION_COOKIE_SECURE",
"purpose": "Forces the Secure flag on every cookie the app issues. Unset, it tracks NODE_ENV. Set false for a LAN / VPN-only self-host served over plain HTTP, where the default makes login look broken.",
"example": "false"
},
{
"name": "TRUST_PROXY_HOPS",
"purpose": "Trusted reverse-proxy hop count; the client IP is read that many entries from the right of X-Forwarded-For.",
"example": "1"
}
]
}
+3 -1
View File
@@ -115,7 +115,9 @@ async function main() {
);
}
writeFileSync(out, plaintext, "utf8");
// The restored file is decrypted PHI in plaintext. Write it 0o600 so it is
// not world-readable on the operator's disk while it sits there.
writeFileSync(out, plaintext, { encoding: "utf8", mode: 0o600 });
console.log(
`Restored userId=${body.userId} (${plaintext.length} bytes) -> ${out}`,
);