Skip to content

fix(dashboard): serve per-request metadata to trusted-gateway peers#1766

Open
KennethWKZ wants to merge 1 commit into
headroomlabs-ai:mainfrom
KennethWKZ:fix/dashboard-trusted-gateway-loopback
Open

fix(dashboard): serve per-request metadata to trusted-gateway peers#1766
KennethWKZ wants to merge 1 commit into
headroomlabs-ai:mainfrom
KennethWKZ:fix/dashboard-trusted-gateway-loopback

Conversation

@KennethWKZ

Copy link
Copy Markdown
Contributor

Description

The dashboard's per-request metadata — the recent_requests / request_logs tail and the config block (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 loopback Host header.

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, so request.client.host is the gateway IP (e.g. 172.18.0.1, or 192.168.64.1 on macOS vmnet), not 127.0.0.1. include_sensitive is therefore False, and the "Recent Requests" table renders empty even though the operator is browsing locally at http://127.0.0.1:8787/dashboard.

curl from inside the container (real 127.0.0.1 peer) 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 by forwarded_headers.py to sanitize X-Forwarded-*) as loopback-equivalent, while retaining the loopback Host-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

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • headroom/proxy/server.py_request_is_loopback now: (1) always enforces the loopback Host-header gate first; (2) returns True for a genuine loopback peer; (3) additionally returns True for a peer inside HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS via the existing peer_is_trusted_gateway / load_trusted_gateway_cidrs helpers.
  • tests/test_proxy_loopback_gating.py — added test_stats_metadata_served_to_trusted_gateway_peer: gateway peer stripped without the allow-list, served with it, and DNS-rebinding (non-loopback Host) still rejected even for a trusted gateway peer.
  • CHANGELOG.md — Unreleased → Fixed entry.

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • New tests added for new functionality
  • Manual testing performed

Test Output

$ pytest tests/test_proxy_loopback_gating.py -q
14 passed, 1 warning in 3.56s

$ ruff check headroom/proxy/server.py tests/test_proxy_loopback_gating.py
All checks passed!

Real Behavior Proof

  • Environment: Headroom 0.29.0 in a mocker compose (Apple Containerization) bridge container on macOS; host browser at http://127.0.0.1:8787/dashboard.
  • Exact command / steps: before the fix, mocker compose exec headroom-proxy sh -c 'curl -s http://127.0.0.1:8787/stats' (peer = real 127.0.0.1) returned a populated recent_requests array, while the host browser saw an empty table. After adding HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS covering the container gateway and recreating, the host browser's dashboard shows the Recent Requests table again.
  • Observed result: dashboard per-request table restored for the host browser; aggregate-only view unchanged for untrusted network callers.
  • Not tested: IPv6 gateway CIDRs (the underlying peer_is_trusted_gateway supports them; not exercised in this environment).

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Additional Notes

Pure opt-in: HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS is empty by default, so _request_is_loopback behavior 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.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR follows the template and is marked ready for human review.

@github-actions github-actions Bot added the status: ready for review Pull request body is complete and the author marked it ready for human review label Jul 3, 2026
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.
@KennethWKZ KennethWKZ force-pushed the fix/dashboard-trusted-gateway-loopback branch from 465f480 to 19f69a0 Compare July 3, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: ready for review Pull request body is complete and the author marked it ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant