feat(dashboard): add HERMES_DASHBOARD_EXTRA_HOSTS for reverse-proxy / tunnel WS Origin acceptance - #75019
feat(dashboard): add HERMES_DASHBOARD_EXTRA_HOSTS for reverse-proxy / tunnel WS Origin acceptance#75019gering wants to merge 1 commit into
Conversation
…S Origin 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: HERMES_DASHBOARD_EXTRA_HOSTS, a comma-separated list of explicitly trusted hostnames accepted in addition to the bound host. Unset (the default) keeps current strict behaviour; unlisted origins and DNS-rebinding Hosts are still rejected. Closes NousResearch#70059
Related to #70059 and the existing reverse-proxy Origin implementations. This current-main rebase uses a separate multi-host environment allowlist; it is not a duplicate of the public-URL, config, or forwarded-host approaches. Maintainers need to choose the intended trust/config contract. |
|
Thanks for reproducing a real reverse-proxy WebSocket failure and for adding focused strictness coverage. This is an automated hermes-sweeper review.
Closing under the standing configuration policy rather than on the merits of the reproduction. Closed as not-planned per standing maintainer policy ( |
…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
Summary
Closes #70059. Related: #54072, #50365.
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 (it's a browser security header)._is_accepted_host()then refuses every WS upgrade:breaking chat, the events feed and the PTY, while the SPA itself loads fine.
Change
Adds the operator opt-in proposed in #70059:
_is_accepted_host()(covers both the HTTP Host middleware and the WS Host/Origin guard, which share this helper)._ws_auth_ok) or the loopback peer gate.Testing
tests/hermes_cli/test_web_server_host_header.py(TestExtraAcceptedHosts): accept-on-loopback-bind (incl. port + case), unlisted-host rejection, unset-env strictness, comma-list with whitespace, empty-entry handling.pytest tests/hermes_cli/test_web_server_host_header.py→ 11 passed (6 pre-existing + 5 new).127.0.0.1:9119, env set via systemd drop-in): dashboard chat / events feed / PTY connect again; foreign origins still refused.