docs: document public reverse-proxy exposure (reviewer.hemoglobina.store) #12

Merged
hibryda merged 2 commits from docs/public-reverse-proxy-exposure into main 2026-06-03 18:04:16 +02:00
Owner

Documents the now-live optional public-exposure setup (mirrors git.hemoglobina.store): DO Caddy auto-TLS + WireGuard → NAS viewport, OAuth-gated.

  • deployment.md: new Public reverse-proxy exposure section — DNS, Caddy block (reload-not-restart), the NAS-local docker-compose.override.yml wg dual-bind (survives install-on-nas.sh since it only overwrites docker-compose.yml), STATUS_PUBLIC_URLS+TRUST_PROXY, OAuth callback-URI registration, recreate, and the non-interactive verification curls.
  • configuration.md: STATUS_PUBLIC_URL / STATUS_PUBLIC_URLS / TRUST_PROXY in the Auth section.

Docs-only; follows the multi-origin auth feature (#11).

Documents the now-live optional public-exposure setup (mirrors `git.hemoglobina.store`): DO Caddy auto-TLS + WireGuard → NAS viewport, OAuth-gated. - **deployment.md**: new *Public reverse-proxy exposure* section — DNS, Caddy block (reload-not-restart), the NAS-local `docker-compose.override.yml` wg dual-bind (survives `install-on-nas.sh` since it only overwrites `docker-compose.yml`), `STATUS_PUBLIC_URLS`+`TRUST_PROXY`, OAuth callback-URI registration, recreate, and the non-interactive verification curls. - **configuration.md**: `STATUS_PUBLIC_URL` / `STATUS_PUBLIC_URLS` / `TRUST_PROXY` in the Auth section. Docs-only; follows the multi-origin auth feature (#11).
docs: document public reverse-proxy exposure (reviewer.hemoglobina.store)
All checks were successful
pr-reviewer CI / vitest (pull_request) Successful in 1m1s
pr-reviewer CI / tsc build (pull_request) Successful in 35s
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
440d2baad0
Record the optional public-exposure setup mirroring git.hemoglobina.store: DO
Caddy auto-TLS + WireGuard tunnel → the NAS viewport, OAuth-gated (unauth →
302 /auth/login, never UI content).

- deployment.md: new "Public reverse-proxy exposure" section — DNS A-record,
  Caddy block (reload-not-restart), the NAS-local docker-compose.override.yml
  wg dual-bind (survives full deploys; carries the deployment-specific wg IP so
  it stays out of the repo), STATUS_PUBLIC_URLS + TRUST_PROXY, OAuth callback-URI
  registration, recreate, and the non-interactive verification curls.
- configuration.md: document STATUS_PUBLIC_URL / STATUS_PUBLIC_URLS (multi-origin
  allow-list) / TRUST_PROXY in the Auth section.

hib-pr-reviewer review — PR #12 (hib/hib-pr-reviewer)

Round 1 — head 440d2baad095, base main, trigger opened

TL;DR: CONDITIONAL_APPROVE — 1 agreed finding kept (TRUSTED_PROXY_HOPS missing from table), 1 unique-to-B verified and kept (TRUST_PROXY description omits primary SSE per-IP-cap motivation), 1 unique-to-A verified and kept (deployment.md ini omits TRUSTED_PROXY_HOPS); all minor/info, no blocking issues.

Summary

Arbitration summary

All three actionable concerns across A and B are verified against HEAD source and kept.

Agreed finding (A + B): Both independently flag TRUSTED_PROXY_HOPS absent from the new configuration.md table. Confirmed: bin/svelte-viewport.ts:363-364 always parses the two constants together; docs/features.md:93 explicitly pairs them; the table row at line 158 has no TRUSTED_PROXY_HOPS sibling. Kept at minor.

Unique-to-B (verified): TRUST_PROXY description says "client-IP for the login rate-limit" but omits the SSE per-IP cap. Confirmed by bin/svelte-viewport.ts:356-362 (comment names the SSE cap as the primary motivation, login rate-limit is the secondary use at line 855) and docs/features.md:93. Omitting this means an operator reading only the table will not understand that without TRUST_PROXY=1 the per-IP cap degenerates to one global cap for all clients. Kept at minor.

Unique-to-A (verified): docs/deployment.md step-4 ini block at lines 149-151 omits TRUSTED_PROXY_HOPS=1. Confirmed by Read. A correctly notes the omission is operationally safe for the single-hop case but reduces discoverability. Kept at info.

No findings dropped.

Blast Radius

The diff is documentation-only, touching two markdown files in docs/. No source code, configuration schemas, or runtime behaviour is modified. The blast radius is limited to operators reading these docs and potentially misconfiguring their deployments.

BLAST_SCORE: 1/10

CI status (head 440d2baad095)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details
pr-reviewer CI / tsc --noEmit (pull_request) pending details

Findings (3)

[MINOR] TRUST_PROXY description omits the SSE per-IP cap — the primary motivation

docs/configuration.md:158

The new row says X-Forwarded-For is honoured "(client-IP for the login rate-limit)". This is factually incomplete: the SSE per-IP cap is the primary use case.

bin/svelte-viewport.ts:356-362 (source comment):

// #3601 — reverse-proxy trust policy for the SSE per-IP cap. Default OFF =
// key on socket.remoteAddress (spoof-proof; correct for a host-direct
// deployment). Set TRUST_PROXY=1 when the viewport is reached via a reverse
// proxy/tunnel so the cap keys on the REAL client (X-Forwarded-For honouring
// TRUSTED_PROXY_HOPS) instead of the shared proxy IP — otherwise the per-IP
// cap collapses to one global cap across all operators.

bin/svelte-viewport.ts:855 applies the same policy to the login route only as a secondary use. docs/features.md:93 also scopes it to the SSE cap first.

An operator reading only this table row to decide whether to set TRUST_PROXY=1 will not know that without it the per-IP cap collapses to one global cap (all traffic keyed on the proxy IP), which is a significant operational consequence.

Suggested fix:

-| `TRUST_PROXY` | `0` | When `1`, the viewport honours `X-Forwarded-For` (client-IP for the login rate-limit) and `X-Forwarded-Proto`/`X-Forwarded-Host` (per-request origin selection). Set ONLY when the viewport sits behind a trusted reverse proxy/tunnel that sets these headers — otherwise a client could spoof them. |
+| `TRUST_PROXY` | `0` | When `1`, the viewport honours `X-Forwarded-For` (real client-IP for the SSE per-IP cap and login rate-limit; without this, all traffic keys on the proxy's IP — one global cap) and `X-Forwarded-Proto`/`X-Forwarded-Host` (per-request origin selection). Set ONLY when the viewport sits behind a trusted reverse proxy/tunnel that sets these headers — otherwise a client could spoof them. Pair with `TRUSTED_PROXY_HOPS` for multi-hop chains (default `1`). |

[MINOR] TRUSTED_PROXY_HOPS companion variable missing from the new table

docs/configuration.md:158

configuration.md is the canonical env-var reference. The new TRUST_PROXY row has no sibling TRUSTED_PROXY_HOPS row, yet bin/svelte-viewport.ts:363-364 always parses them together:

const TRUST_PROXY = /^(1|true|yes)$/i.test((process.env.TRUST_PROXY ?? "").trim());
const TRUSTED_PROXY_HOPS = parseSsePositiveInt("TRUSTED_PROXY_HOPS", 1);

docs/features.md:93 explicitly pairs them: "TRUST_PROXY (+ TRUSTED_PROXY_HOPS) gates client-IP derivation for the SSE per-IP cap". .env.example documents them adjacent. An operator who consults only this table to deploy behind a two-hop stack (CDN → Caddy → WireGuard → NAS) would silently derive the wrong client IP from XFF with no hint the knob exists.

Suggested addition after the TRUST_PROXY row:

| `TRUSTED_PROXY_HOPS` | `1` | Number of trusted reverse-proxy hops when `TRUST_PROXY=1`. The viewport peels that many entries from the right of `X-Forwarded-For`; the next entry is the real client IP. For a single proxy (Caddy or nginx) the default `1` is correct. Increase for CDN → proxy → app chains. Only consulted when `TRUST_PROXY=1`. See `src/svelte-viewport/lib/client-ip.ts`. |

[INFO] Step 4 ini snippet omits TRUSTED_PROXY_HOPS=1

docs/deployment.md:150

The env block in step 4 (lines 149-151):

STATUS_PUBLIC_URLS=http://localhost:9723,https://reviewer.hemoglobina.store
TRUST_PROXY=1

does not include TRUSTED_PROXY_HOPS=1. The omission is safe for this single-hop Caddy topology (the default 1 is exactly right), but docs/features.md:93 and .env.example already pair these vars, and an operator copy-pasting this snippet gains no hint the companion knob exists. If they later add a CDN in front of Caddy they would need to set it.

Suggested change:

STATUS_PUBLIC_URLS=http://localhost:9723,https://reviewer.hemoglobina.store
TRUST_PROXY=1
TRUSTED_PROXY_HOPS=1   # 1 = single Caddy hop; increment for CDN+proxy stacks

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 1 • 3 findings (2m/1i) • 2026-06-03T15:55:12.679Z → 2026-06-03T16:00:45.374Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer round:1 --> ## hib-pr-reviewer review — PR #12 (hib/hib-pr-reviewer) **Round 1** — head `440d2baad095`, base `main`, trigger `opened` **TL;DR:** CONDITIONAL_APPROVE — 1 agreed finding kept (TRUSTED_PROXY_HOPS missing from table), 1 unique-to-B verified and kept (TRUST_PROXY description omits primary SSE per-IP-cap motivation), 1 unique-to-A verified and kept (deployment.md ini omits TRUSTED_PROXY_HOPS); all minor/info, no blocking issues. ### Summary ## Arbitration summary All three actionable concerns across A and B are verified against HEAD source and kept. **Agreed finding (A + B):** Both independently flag `TRUSTED_PROXY_HOPS` absent from the new `configuration.md` table. Confirmed: `bin/svelte-viewport.ts:363-364` always parses the two constants together; `docs/features.md:93` explicitly pairs them; the table row at line 158 has no `TRUSTED_PROXY_HOPS` sibling. Kept at **minor**. **Unique-to-B (verified):** `TRUST_PROXY` description says "client-IP for the login rate-limit" but omits the SSE per-IP cap. Confirmed by `bin/svelte-viewport.ts:356-362` (comment names the SSE cap as the *primary* motivation, login rate-limit is the secondary use at line 855) and `docs/features.md:93`. Omitting this means an operator reading only the table will not understand that without `TRUST_PROXY=1` the per-IP cap degenerates to one global cap for all clients. Kept at **minor**. **Unique-to-A (verified):** `docs/deployment.md` step-4 ini block at lines 149-151 omits `TRUSTED_PROXY_HOPS=1`. Confirmed by Read. A correctly notes the omission is operationally safe for the single-hop case but reduces discoverability. Kept at **info**. No findings dropped. ### Blast Radius The diff is documentation-only, touching two markdown files in docs/. No source code, configuration schemas, or runtime behaviour is modified. The blast radius is limited to operators reading these docs and potentially misconfiguring their deployments. **BLAST_SCORE: 1/10** ### CI status (head `440d2baad095`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/85/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/85/jobs/2) | | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/85/jobs/0) | ### Findings (3) #### **[MINOR]** TRUST_PROXY description omits the SSE per-IP cap — the primary motivation _docs/configuration.md:158_ The new row says `X-Forwarded-For` is honoured "(client-IP for the login rate-limit)". This is factually incomplete: the SSE per-IP cap is the *primary* use case. `bin/svelte-viewport.ts:356-362` (source comment): ```ts // #3601 — reverse-proxy trust policy for the SSE per-IP cap. Default OFF = // key on socket.remoteAddress (spoof-proof; correct for a host-direct // deployment). Set TRUST_PROXY=1 when the viewport is reached via a reverse // proxy/tunnel so the cap keys on the REAL client (X-Forwarded-For honouring // TRUSTED_PROXY_HOPS) instead of the shared proxy IP — otherwise the per-IP // cap collapses to one global cap across all operators. ``` `bin/svelte-viewport.ts:855` applies the same policy to the login route only as a secondary use. `docs/features.md:93` also scopes it to the SSE cap first. An operator reading only this table row to decide whether to set `TRUST_PROXY=1` will not know that without it the per-IP cap collapses to one global cap (all traffic keyed on the proxy IP), which is a significant operational consequence. Suggested fix: ```diff -| `TRUST_PROXY` | `0` | When `1`, the viewport honours `X-Forwarded-For` (client-IP for the login rate-limit) and `X-Forwarded-Proto`/`X-Forwarded-Host` (per-request origin selection). Set ONLY when the viewport sits behind a trusted reverse proxy/tunnel that sets these headers — otherwise a client could spoof them. | +| `TRUST_PROXY` | `0` | When `1`, the viewport honours `X-Forwarded-For` (real client-IP for the SSE per-IP cap and login rate-limit; without this, all traffic keys on the proxy's IP — one global cap) and `X-Forwarded-Proto`/`X-Forwarded-Host` (per-request origin selection). Set ONLY when the viewport sits behind a trusted reverse proxy/tunnel that sets these headers — otherwise a client could spoof them. Pair with `TRUSTED_PROXY_HOPS` for multi-hop chains (default `1`). | ``` #### **[MINOR]** TRUSTED_PROXY_HOPS companion variable missing from the new table _docs/configuration.md:158_ `configuration.md` is the canonical env-var reference. The new `TRUST_PROXY` row has no sibling `TRUSTED_PROXY_HOPS` row, yet `bin/svelte-viewport.ts:363-364` always parses them together: ```ts const TRUST_PROXY = /^(1|true|yes)$/i.test((process.env.TRUST_PROXY ?? "").trim()); const TRUSTED_PROXY_HOPS = parseSsePositiveInt("TRUSTED_PROXY_HOPS", 1); ``` `docs/features.md:93` explicitly pairs them: "`TRUST_PROXY` (+ `TRUSTED_PROXY_HOPS`) gates client-IP derivation for the SSE per-IP cap". `.env.example` documents them adjacent. An operator who consults only this table to deploy behind a two-hop stack (CDN → Caddy → WireGuard → NAS) would silently derive the wrong client IP from XFF with no hint the knob exists. Suggested addition after the `TRUST_PROXY` row: ```markdown | `TRUSTED_PROXY_HOPS` | `1` | Number of trusted reverse-proxy hops when `TRUST_PROXY=1`. The viewport peels that many entries from the right of `X-Forwarded-For`; the next entry is the real client IP. For a single proxy (Caddy or nginx) the default `1` is correct. Increase for CDN → proxy → app chains. Only consulted when `TRUST_PROXY=1`. See `src/svelte-viewport/lib/client-ip.ts`. | ``` #### **[INFO]** Step 4 ini snippet omits TRUSTED_PROXY_HOPS=1 _docs/deployment.md:150_ The env block in step 4 (lines 149-151): ```ini STATUS_PUBLIC_URLS=http://localhost:9723,https://reviewer.hemoglobina.store TRUST_PROXY=1 ``` does not include `TRUSTED_PROXY_HOPS=1`. The omission is safe for this single-hop Caddy topology (the default `1` is exactly right), but `docs/features.md:93` and `.env.example` already pair these vars, and an operator copy-pasting this snippet gains no hint the companion knob exists. If they later add a CDN in front of Caddy they would need to set it. Suggested change: ```ini STATUS_PUBLIC_URLS=http://localhost:9723,https://reviewer.hemoglobina.store TRUST_PROXY=1 TRUSTED_PROXY_HOPS=1 # 1 = single Caddy hop; increment for CDN+proxy stacks ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 1 • 3 findings (2m/1i) • 2026-06-03T15:55:12.679Z → 2026-06-03T16:00:45.374Z • posted-as: pr-reviewer-bot • model: auto</sub>
docs: address PR #12 review — document TRUSTED_PROXY_HOPS + TRUST_PROXY SSE motivation
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
pr-reviewer CI / tsc build (pull_request) Successful in 36s
79b32a4d8d
- configuration.md: add TRUSTED_PROXY_HOPS row; lead TRUST_PROXY with its primary
  motivation (the SSE per-IP cap collapses to one global cap behind a proxy
  without it), origin selection secondary.
- deployment.md: add TRUSTED_PROXY_HOPS to the env block + note the SSE-cap fix.
hibryda deleted branch docs/public-reverse-proxy-exposure 2026-06-03 18:04:16 +02:00

Review cancelled by operator at 2026-06-03T16:04:24.911Z.

Review cancelled by operator at 2026-06-03T16:04:24.911Z.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
hib/hib-pr-reviewer!12
No description provided.