fix(dashboard): serve per-request metadata to trusted-gateway peers#1766
Open
KennethWKZ wants to merge 1 commit into
Open
fix(dashboard): serve per-request metadata to trusted-gateway peers#1766KennethWKZ wants to merge 1 commit into
KennethWKZ wants to merge 1 commit into
Conversation
Contributor
PR governanceThis PR follows the template and is marked ready for human review. |
KennethWKZ
pushed a commit
to KennethWKZ/headroom
that referenced
this pull request
Jul 3, 2026
…d table Set HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS=192.168.64.0/24,172.16.0.0/12 on the headroom-proxy env anchor and the bedrock overlay. In a bridge-network container the host browser's peer IP is the gateway (macOS vmnet 192.168.64.1 / docker 172.16/12), not 127.0.0.1, so the dashboard's per-request table was stripped by the loopback gate. Pairs with fb7dec9 (the _request_is_loopback fix upstreamed as PR headroomlabs-ai#1766). Loopback Host-header gate still enforced.
The dashboard's recent-requests table (and the config block with upstream URLs) is gated to loopback callers via _request_is_loopback. When Headroom runs in a bridge-network container (docker/mocker compose), a browser on the host reaches it through the container gateway, so request.client.host is the gateway IP, not 127.0.0.1 — include_sensitive is False and the table renders empty even though the operator is local. curl from inside the container (real 127.0.0.1 peer) confirmed the data is present; only the host-browser path was stripped. Treat a peer inside an operator-configured trusted-gateway CIDR (HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS, already used to sanitize X-Forwarded-*) as loopback-equivalent, while keeping the loopback Host-header gate as the DNS-rebinding defence. Opt-in and empty by default, so no behavior change unless the operator allow-lists their container gateway.
465f480 to
19f69a0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The dashboard's per-request metadata — the
recent_requests/request_logstail and theconfigblock (which echoes upstream API URLs + backend settings) — is gated to loopback callers via_request_is_loopback. It requires both a loopback peer IP (request.client.host == 127.0.0.1) and a loopbackHostheader.When Headroom runs in a bridge-network container (Docker/podman, or Apple Containerization /
mocker), a browser on the host reaches the proxy through the container gateway, sorequest.client.hostis the gateway IP (e.g.172.18.0.1, or192.168.64.1on macOS vmnet), not127.0.0.1.include_sensitiveis thereforeFalse, and the "Recent Requests" table renders empty even though the operator is browsing locally athttp://127.0.0.1:8787/dashboard.curlfrom inside the container (real127.0.0.1peer) confirmed the data is present and populated — only the host-browser path was being stripped.The fix treats a peer inside an operator-configured trusted-gateway CIDR (
HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS— the same allow-list already used byforwarded_headers.pyto sanitizeX-Forwarded-*) as loopback-equivalent, while retaining the loopbackHost-header gate as the DNS-rebinding defence. It is opt-in and empty by default, so there is no behavior change unless the operator explicitly allow-lists their container gateway.Closes #
Type of Change
Changes Made
headroom/proxy/server.py—_request_is_loopbacknow: (1) always enforces the loopbackHost-header gate first; (2) returnsTruefor a genuine loopback peer; (3) additionally returnsTruefor a peer insideHEADROOM_PROXY_TRUSTED_GATEWAY_CIDRSvia the existingpeer_is_trusted_gateway/load_trusted_gateway_cidrshelpers.tests/test_proxy_loopback_gating.py— addedtest_stats_metadata_served_to_trusted_gateway_peer: gateway peer stripped without the allow-list, served with it, and DNS-rebinding (non-loopbackHost) still rejected even for a trusted gateway peer.CHANGELOG.md— Unreleased → Fixed entry.Testing
pytest)ruff check .)Test Output
Real Behavior Proof
mocker compose(Apple Containerization) bridge container on macOS; host browser athttp://127.0.0.1:8787/dashboard.mocker compose exec headroom-proxy sh -c 'curl -s http://127.0.0.1:8787/stats'(peer = real127.0.0.1) returned a populatedrecent_requestsarray, while the host browser saw an empty table. After addingHEADROOM_PROXY_TRUSTED_GATEWAY_CIDRScovering the container gateway and recreating, the host browser's dashboard shows the Recent Requests table again.peer_is_trusted_gatewaysupports them; not exercised in this environment).Review Readiness
Additional Notes
Pure opt-in:
HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRSis empty by default, so_request_is_loopbackbehavior is byte-identical to today unless an operator allow-lists a gateway CIDR. Reuses the existing trusted-gateway machinery rather than introducing a new config surface. Docs/compose examples intentionally omitted — deployment-specific.