Skip to content

feat(dashboard): dashboard.extra_hosts (config.yaml) for reverse-proxy / tunnel WS Origin acceptance - #75053

Open
gering wants to merge 2 commits into
NousResearch:mainfrom
gering:feat/dashboard-extra-hosts-config
Open

feat(dashboard): dashboard.extra_hosts (config.yaml) for reverse-proxy / tunnel WS Origin acceptance#75053
gering wants to merge 2 commits into
NousResearch:mainfrom
gering:feat/dashboard-extra-hosts-config

Conversation

@gering

@gering gering commented Jul 30, 2026

Copy link
Copy Markdown

Re-scope of #75019, which was closed under the standing env-var-for-config policy — this version puts the setting on the established dashboard config surface in config.yaml instead of a new HERMES_* 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 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, the events feed, and PTY. Closes #70059.

Mechanism

dashboard:
  extra_hosts:
    - atlas.example.com
  • Exact-match validation: each entry is a single bare hostname, compared case-insensitively against the request host with the port stripped. Wildcards, schemes, paths, and non-string entries are ignored with a single warning — a config mistake fails closed, it never partially widens the accept set.
  • No new env vars: the value is read per request via the mtime-cached 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.
  • Default unchanged: with extra_hosts unset, 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:

  • HTTP middleware (host_header_middleware): configured extra host passes (!= 400), unlisted host still rejected with 400.
  • WebSocket Host/Origin guard: WS upgrade with the extra host on Host + Origin succeeds end-to-end via TestClient.websocket_connect; an unlisted Origin still closes with 4403.
  • Unit coverage of the validation rules: case-insensitivity, port stripping (header and entry side), single-string tolerance, malformed-entry fail-closed, empty/unset strictness.

tests/hermes_cli/test_web_server_host_header.py: 18 passed.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_hosts is a new public configuration key, but the PR changes only code and tests. The existing dashboard configuration references are website/docs/user-guide/features/web-dashboard.md:608 and cli-config.yaml.example:1538; neither documents this setting.

Suggested changes

  • Document an exact-host dashboard.extra_hosts example and state that it is for explicitly trusted reverse-proxy/tunnel hostnames, not wildcards or public exposure.

Automated hermes-sweeper review.

Comment thread hermes_cli/web_server.py
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 {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/config Config system, migrations, profiles needs-decision Awaiting maintainer decision before any implementation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 30, 2026
@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@gering

gering commented Jul 30, 2026

Copy link
Copy Markdown
Author

Docs added per the review: a new "Reverse proxies and tunnels (dashboard.extra_hosts)" section in website/docs/user-guide/features/web-dashboard.md (exact-host example, the asymmetric Host-vs-Origin failure mode, and explicit scope limits: exact match only, no wildcards, not for public exposure, unset = unchanged strict behaviour) and a commented extra_hosts example in the Web Dashboard block of cli-config.yaml.example.

@gering

gering commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ready for another pass: the requested documentation landed (a3b96e68c — web-dashboard guide section + cli-config.yaml.example entry), and the branch is now rebased onto current main (clean rebase, no conflicts; tests/hermes_cli/test_web_server_host_header.py: 18 passed).

@gering
gering force-pushed the feat/dashboard-extra-hosts-config branch from 01e4fdf to a3b96e6 Compare August 3, 2026 17:32
gering and others added 2 commits August 17, 2026 12:44
…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
@gering
gering force-pushed the feat/dashboard-extra-hosts-config branch from a3b96e6 to f596bb4 Compare August 17, 2026 10:46
@gering

gering commented Aug 17, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (clean cherry-pick of the two commits, no conflicts). tests/hermes_cli/test_web_server_host_header.py: 18 passed.

The sweeper's docs request from 2026-07-30 is in the second commit. Happy to take another pass if anything else is needed.

Copy link
Copy Markdown

Independent real-world validation from a self-hosted Hermes v0.20.0 (2026.8.3) deployment behind Caddy, with the dashboard bound to 127.0.0.1:9119 and exposed only through a local HTTPS reverse proxy.

Reproduction before the allowlist:

  • direct dashboard /api/status: 200 / healthy
  • WebSocket with loopback Origin: 101
  • same WS handshake with X-Forwarded-For: 101
  • browser/public reverse-proxy Origin: 403
  • browser symptoms: Chat reconnect loop (1006) and Events/Tools feed disconnected

I locally backported the minimal exact-host acceptance behavior into _is_accepted_host() and configured one trusted reverse-proxy hostname. Post-fix validation:

  • loopback Origin: 101
  • configured reverse-proxy Origin: 101
  • arbitrary/unlisted Origin: 403
  • browser Chat and Events feed restored
  • gateway process was left untouched; Kanban dispatcher remained running

This strongly confirms the failure path and the exact-host allowlist approach in this PR. For upstream, the dashboard.extra_hosts config surface here is preferable to the environment-only workaround I used locally, and the fail-closed/unlisted-host behavior is important to preserve the DNS-rebinding boundary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard: add HERMES_DASHBOARD_EXTRA_HOSTS for reverse-proxy / Tailscale WS Origin acceptance

4 participants