Problem Statement
The /stats endpoint gates its sensitive sub-payloads — recent_requests, request_logs, and the config block — behind a hard-coded loopback-only check, with no way to opt in for a trusted deployment. include_sensitive = _request_is_loopback(request) requires both the TCP peer (request.client.host) to be loopback and the Host: header to name a loopback address; otherwise those keys are popped. uvicorn also runs with proxy_headers=False, so X-Forwarded-For is intentionally ignored and the guard always sees the real socket peer.
The consequence: when headroom runs as a long-lived service behind a trusted reverse proxy addressed by hostname, both gates fail by construction (peer = the proxy's socket, Host = the public service name). The aggregate counters render fine, but the useful live panel — Recent Requests across all projects — is permanently blank. There's currently no supported way to say "this front proxy is trusted."
I want to be clear I understand why the gate exists and am not asking to weaken the default: the two-gate model is a deliberate DNS-rebinding defense (a peer-IP check alone is insufficient because a malicious page can make a browser hit 127.0.0.1:<port>, and it's the loopback-Host requirement that unmasks the rebind), and the payload is genuinely sensitive (prompt/tool-path snippets, model names, upstream URLs). The ask is an explicit, operator-defined extension of that boundary, off by default.
Proposed Solution
An opt-in, default-off trusted-proxy configuration that preserves the existing two-gate model but lets the operator define the boundary:
HEADROOM_DASHBOARD_TRUSTED_PROXIES — CIDR/IP allowlist. A forwarded origin is considered only when the real peer is in this set.
HEADROOM_DASHBOARD_TRUSTED_HOSTS — a Host: allowlist that extends the loopback set (e.g. the service hostname), so the anti-rebinding gate remains a real allowlist rather than being disabled.
Both unset ⇒ byte-for-byte current behavior. When set, include_sensitive is true only if peer ∈ trusted_proxies and forwarded_host ∈ trusted_hosts — same two gates, operator-scoped. This mirrors the established trusted-proxy pattern (Django ALLOWED_HOSTS + SECURE_PROXY_SSL_HEADER, Grafana [security], Jupyter trust_xheaders), and there's in-repo precedent for a configured gateway-trust concept in HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS (forwarded_headers.py).
Use Case
- What I'm building: headroom running as a shared, always-on proxy service (the kind the persistent-service preset / Docker image target), fronted by a reverse proxy on a trusted host/network, with several projects routed through it via distinct base URLs.
- How it helps: the dashboard is my primary operability surface. The per-project savings tiles and, above all, "Recent Requests" across all projects are how I see the proxy actually working end-to-end. Today, reached by its hostname, the dashboard is aggregate-only — the live request feed is exactly the part that's stripped. This feature turns it back into the cross-project view it's designed to be, without exposing per-request data on the open network.
- Tokens/cost saved: none directly — this is an observability/operability feature, not a compression path. The indirect value is real, though: making the per-project savings and request feed visible in a hosted deployment is what lets me spot low-savings projects and tune compression, rather than flying blind or shelling into the host to curl 127.0.0.1.
Alternatives Considered
- Browse 127.0.0.1: on the host / SSH-tunnel a loopback origin — both work and are what I do now, but they don't fit a headless service reached by hostname, and don't scale to a shared dashboard.
- A separate loopback-only admin port — lets a co-located proxy target it, but doesn't solve remote viewing and adds surface.
- Trusting X-Forwarded-For globally — rejected; that's the spoofing vector proxy_headers=False exists to prevent. Trust must be explicit and peer-gated.
Example API
# Off by default — current loopback-only behavior is unchanged.
# Operator explicitly declares the trusted front proxy + the hostname it serves:
export HEADROOM_DASHBOARD_TRUSTED_PROXIES="10.0.0.0/8" # peer must match
export HEADROOM_DASHBOARD_TRUSTED_HOSTS="headroom.internal.example" # Host: must match
Effective gate (unchanged shape, operator-scoped boundary):
include_sensitive = (
is_loopback(peer) and is_loopback_host(host_header) # existing path
) or (
peer in TRUSTED_PROXIES and host_header in TRUSTED_HOSTS # new, opt-in
)
Additional Context
- Environment: headroom-ai 0.30.0, persistent raw_exec-style deployment behind a reverse proxy on a trusted network.
Problem Statement
The
/statsendpoint gates its sensitive sub-payloads — recent_requests, request_logs, and the config block — behind a hard-coded loopback-only check, with no way to opt in for a trusted deployment.include_sensitive = _request_is_loopback(request)requires both the TCP peer (request.client.host) to be loopback and the Host: header to name a loopback address; otherwise those keys are popped. uvicorn also runs withproxy_headers=False, soX-Forwarded-Foris intentionally ignored and the guard always sees the real socket peer.The consequence: when headroom runs as a long-lived service behind a trusted reverse proxy addressed by hostname, both gates fail by construction (peer = the proxy's socket, Host = the public service name). The aggregate counters render fine, but the useful live panel — Recent Requests across all projects — is permanently blank. There's currently no supported way to say "this front proxy is trusted."
I want to be clear I understand why the gate exists and am not asking to weaken the default: the two-gate model is a deliberate DNS-rebinding defense (a peer-IP check alone is insufficient because a malicious page can make a browser hit
127.0.0.1:<port>, and it's the loopback-Host requirement that unmasks the rebind), and the payload is genuinely sensitive (prompt/tool-path snippets, model names, upstream URLs). The ask is an explicit, operator-defined extension of that boundary, off by default.Proposed Solution
An opt-in, default-off trusted-proxy configuration that preserves the existing two-gate model but lets the operator define the boundary:
HEADROOM_DASHBOARD_TRUSTED_PROXIES— CIDR/IP allowlist. A forwarded origin is considered only when the real peer is in this set.HEADROOM_DASHBOARD_TRUSTED_HOSTS— a Host: allowlist that extends the loopback set (e.g. the service hostname), so the anti-rebinding gate remains a real allowlist rather than being disabled.Both unset ⇒ byte-for-byte current behavior. When set, include_sensitive is true only if
peer ∈ trusted_proxiesandforwarded_host ∈ trusted_hosts— same two gates, operator-scoped. This mirrors the established trusted-proxy pattern (Django ALLOWED_HOSTS + SECURE_PROXY_SSL_HEADER, Grafana [security], Jupyter trust_xheaders), and there's in-repo precedent for a configured gateway-trust concept inHEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS(forwarded_headers.py).Use Case
Alternatives Considered
Example API
Effective gate (unchanged shape, operator-scoped boundary):
Additional Context