feat(dashboard): dashboard.extra_hosts (config.yaml) for reverse-proxy / tunnel WS Origin acceptance - #75053
feat(dashboard): dashboard.extra_hosts (config.yaml) for reverse-proxy / tunnel WS Origin acceptance#75053gering wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for re-scoping this onto config.yaml. The current-main premise is confirmed: hermes_cli/web_server.py:14371 sends a browser Origin authority through _is_accepted_host, whose loopback path accepts only loopback aliases at hermes_cli/web_server.py:499-502. The proposed pre-loopback allowlist therefore addresses the reported path and preserves the unlisted-host rejection tests.
Problems
dashboard.extra_hostsis a new public configuration key, but the PR changes only code and tests. The existing dashboard configuration references arewebsite/docs/user-guide/features/web-dashboard.md:608andcli-config.yaml.example:1538; neither documents this setting.
Suggested changes
- Document an exact-host
dashboard.extra_hostsexample and state that it is for explicitly trusted reverse-proxy/tunnel hostnames, not wildcards or public exposure.
Automated hermes-sweeper review.
| disk I/O on the hot path) so a running dashboard picks up config | ||
| edits without a restart — the same live-edit behaviour tests rely on. | ||
| """ | ||
| dashboard_cfg = load_config_readonly().get("dashboard") or {} |
There was a problem hiding this comment.
Please add a corresponding dashboard.extra_hosts example and its exact-host security semantics to website/docs/user-guide/features/web-dashboard.md and cli-config.yaml.example. This is a new operator-facing config key, but this PR currently changes only implementation and tests.
|
Docs added per the review: a new "Reverse proxies and tunnels ( |
|
Ready for another pass: the requested documentation landed ( |
01e4fdf to
a3b96e6
Compare
…n acceptance Behind a reverse proxy or tunnel (nginx, Tailscale Serve, Cloudflare Tunnel) fronting a loopback-bound dashboard, HTTP works because the proxy rewrites the Host header — but the browser-set WebSocket Origin header carries the public hostname and cannot be rewritten by any proxy. _is_accepted_host() then refuses every WS upgrade (origin_mismatch), breaking chat, events feed and PTY. Add an operator opt-in on the established dashboard config surface: dashboard.extra_hosts in config.yaml, a list of explicitly trusted hostnames accepted in addition to the bound host. - Exact-match only: bare hostnames, compared case-insensitively with the port stripped; wildcards/schemes/paths are ignored with a single warning (fail closed, never partially honoured). - Read per request via the mtime-cached load_config_readonly() (no disk I/O on the hot path), so a running dashboard picks up config edits without a restart. - Unset (the default) keeps current strict behaviour; unlisted hosts and DNS-rebinding Hosts are still rejected. - Positive + negative regression coverage through BOTH real request paths: the HTTP host_header_middleware and the WebSocket Host/Origin guard, plus unit coverage of the validation rules. Re-scope of NousResearch#75019 (closed under the env-var-for-config policy): behavioural settings belong in config.yaml, not HERMES_* env vars. Closes NousResearch#70059 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwuEkr3qrADt5ctc5mXZmn
…ample) Address the hermes-sweeper review on NousResearch#75053: the new public config key was code+tests only. Add the two established documentation surfaces: - website/docs/user-guide/features/web-dashboard.md: new 'Reverse proxies and tunnels' section — the asymmetric Host-vs-Origin failure, an exact-host example, and the explicit scope limits (exact match only, no wildcards, not a public-exposure switch, unset = unchanged strict behaviour) - cli-config.yaml.example: commented extra_hosts example in the Web Dashboard block, same rules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwuEkr3qrADt5ctc5mXZmn
a3b96e6 to
f596bb4
Compare
|
Rebased onto current The sweeper's docs request from 2026-07-30 is in the second commit. Happy to take another pass if anything else is needed. |
|
Independent real-world validation from a self-hosted Hermes v0.20.0 (2026.8.3) deployment behind Caddy, with the dashboard bound to Reproduction before the allowlist:
I locally backported the minimal exact-host acceptance behavior into
This strongly confirms the failure path and the exact-host allowlist approach in this PR. For upstream, the |
Re-scope of #75019, which was closed under the standing
env-var-for-configpolicy — this version puts the setting on the establisheddashboardconfig surface inconfig.yamlinstead of a newHERMES_*env var, as requested in the closing review (and in the same direction required by the #70064 review).Problem (unchanged from #75019, reproduced live)
Behind a reverse proxy or tunnel (nginx, Tailscale Serve, Cloudflare Tunnel) fronting a loopback-bound dashboard, HTTP works because the proxy rewrites the
Hostheader — but the browser-set WebSocketOriginheader carries the public hostname and cannot be rewritten by any proxy._is_accepted_host()then refuses every WS upgrade (origin_mismatch), breaking chat, the events feed, and PTY. Closes #70059.Mechanism
load_config_readonly()(no disk I/O on the hot path, ~µs cache hit), so a running dashboard picks up config edits without a restart.extra_hostsunset, behaviour is byte-for-byte the current strict behaviour; unlisted hosts and DNS-rebinding Hosts are still rejected (GHSA-ppp5-vxwm-4cf7 defence intact).Coverage (both request paths, positive + negative)
As asked for in the #75019 closing review:
host_header_middleware): configured extra host passes (!= 400), unlisted host still rejected with 400.Host+Originsucceeds end-to-end viaTestClient.websocket_connect; an unlistedOriginstill closes with 4403.tests/hermes_cli/test_web_server_host_header.py: 18 passed.