Skip to content

feat(dashboard): trust reverse-proxy source and allow declared origins - #248

Open
AndrewMoryakov wants to merge 37 commits into
teamchong:mainfrom
AndrewMoryakov:feat/trusted-proxy-dashboard-origins
Open

feat(dashboard): trust reverse-proxy source and allow declared origins#248
AndrewMoryakov wants to merge 37 commits into
teamchong:mainfrom
AndrewMoryakov:feat/trusted-proxy-dashboard-origins

Conversation

@AndrewMoryakov

Copy link
Copy Markdown
Contributor

Problem

Running the dashboard behind a TLS + basic-auth reverse proxy currently requires two awkward header rewrites in the proxy config, and both trace back to a single spot: isAllowedDashboardClient() demands a loopback Host even for a request coming from the explicitly configured PXPIPE_TRUSTED_DASHBOARD_PROXY.

Because of that, the proxy has to:

  1. forge Host: localhost so the request is accepted, which in turn
  2. breaks the same-origin mutation check — the internal origin becomes http://localhost while the browser sends the real public origin — so the proxy must also strip Origin to make dashboard toggles work.

Two config hacks fighting one code assumption.

Change

Let the operator configure this directly instead of forging headers:

  • A request whose source address equals PXPIPE_TRUSTED_DASHBOARD_PROXY is trusted without a loopback Host. Host validation stays mandatory for every other source (loopback still requires a loopback Host + loopback address, exactly as before).
  • New PXPIPE_DASHBOARD_ORIGINS (comma-separated) declares the public origin(s) accepted for dashboard mutations behind the proxy.

Both are documented in --help.

Security

CSRF protection is unchanged. Cross-site writes are still rejected by the Sec-Fetch-Site: cross-site check, which this PR does not touch. The trusted-proxy bypass only affects a source address the operator explicitly configured as trusted; unconfigured/other sources still require loopback Host and loopback address.

Testing

Deployed behind Traefik (TLS + basic-auth), reaching pxpipe from the trusted proxy address with the real public Host:

  • GET / with public Host200 (previously 403: non-loopback Host)
  • same-origin mutation, real Origin in PXPIPE_DASHBOARD_ORIGINS200 {"compression_enabled":true}
  • cross-site mutation (Sec-Fetch-Site: cross-site) → 403 Forbidden (CSRF still blocked)

With the patch the reverse-proxy config collapses to just router + basicAuth — no Host rewrite, no Origin stripping.

…260713

# Conflicts:
#	src/dashboard/fragments.ts
#	tests/dashboard-api.test.ts
Bundles pre-existing local WIP (baseline/proxy/tracker/types/sessions/codex-usage
+ their tests) with the approved model-scope persistence feature (model-scope-store
+ node/fragments/dashboard chips). Checkpoint only — reorganize/split later as needed.
…e getter

Final-review Minor: DashboardState already had getCompressionEnabled(); Task 3
added a redundant isCompressionEnabled() twin. buildHealthState now consumes the
existing getter.
Enabling a model that reads pxpipe-imaged content measurably worse (Opus 4.7/4.8,
GPT-5.5, GPT-5.6 Sol, Grok 4.5 — per FINDINGS) now pops an hx-confirm with the
concrete numbers; a lit weak reader shows a ⚠ marker. Fable/Sonnet/Terra/Lun and
broad gpt-5.6 are unaffected. Disabling never prompts.
…/unmeasured)

Replace the ad-hoc weak-reader list with an objective MODEL_READINESS table keyed
to committed eval receipts. Severity follows evidence strength:
- validated (Fable) -> no warning
- below-bar (Opus 4.7/4.8, GPT-5.5, Sol, Grok, broad gpt-5.6->enables Sol) ->
  blocking confirm on enable + concrete numbers + persistent tooltip
- unmeasured (Sonnet, Terra, Lun, and any unlisted id by default) -> non-blocking
  ⚠ marker + 'unmeasured' tooltip
Disabling never prompts. Opus/Sol now sit together honestly (both 0/15 verbatim);
Terra/Lun no longer get a false clean pass.
Four new env vars, all default-preserving so existing configs are unchanged:

* PXPIPE_LOG_MAX_MB     rotate once the active file exceeds this size (default 100)

* PXPIPE_LOG_KEEP       number of rotated generations to retain on disk (.1, .2, ..., up to .N; default 1)

* PXPIPE_LOG_COMPRESS   set to 1 to gzip each rotated generation as .1.gz, .2.gz, ...

* PXPIPE_LOG_FSYNC_MS   periodic fsync interval in ms; 0 = fsync only on shutdown (default 0)

Why: the hardcoded 100 MB cap and single .1 rotation silently eat disk on heavy users (~660 MB/day at our load), and emit() never fsyncs, so any non-graceful kill loses the last few seconds of events. PXPIPE_LOG_FSYNC_MS=500 caps the loss to ~0.5s; raising KEEP gives multi-day history; COMPRESS trades ~10 ms of gzip at rotation for ~5x storage.

Defaults are byte-for-byte identical to the previous hardcoded behavior. Existing FileTracker / sessions / restart-restore tests pass.
Publishing the dashboard behind a TLS/basic-auth reverse proxy required two
awkward header rewrites, both stemming from isAllowedDashboardClient() demanding
a loopback Host even for the explicitly configured PXPIPE_TRUSTED_DASHBOARD_PROXY:

  * the proxy had to forge Host: localhost, which then
  * broke the same-origin mutation check (internal origin became http://localhost
    while the browser sends the real public origin), forcing the proxy to also
    strip Origin.

This lets the operator configure it directly instead:

  * a request whose source equals PXPIPE_TRUSTED_DASHBOARD_PROXY is trusted
    without a loopback Host (Host validation stays mandatory for every other
    source);
  * PXPIPE_DASHBOARD_ORIGINS (comma-separated) declares public origins accepted
    for dashboard mutations.

CSRF protection is unchanged: cross-site writes are still rejected via
Sec-Fetch-Site, which nothing here touches. Both env vars are documented in
--help.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant