feat(dashboard): configurable hostname allowlist for tunnel/proxy setups - #47560
feat(dashboard): configurable hostname allowlist for tunnel/proxy setups#47560bambalados wants to merge 1 commit into
Conversation
Adds dashboard.allowed_external_hosts config key (default []) and reads it in _is_accepted_host() so Cloudflare Tunnel and other reverse-proxy setups can route WS connections through loopback- bound dashboards without triggering the Host header check. The hostname set is read once at module load (restart to pick up config changes). Loopback names (localhost, 127.0.0.1, ::1) are always accepted regardless of this setting.
3e7db0f to
7287e2d
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the config-based reverse-proxy approach. The underlying Host/Origin rejection remains present on current main: loopback binds only accept loopback values in hermes_cli/web_server.py:450-453, and the same helper gates HTTP plus WebSocket Host/Origin validation (hermes_cli/web_server.py:476, 14391, 14408).
Problems
- The new allowlist would make a publicly reachable proxied hostname acceptable while loopback binds still bypass dashboard authentication (
hermes_cli/web_server.py:393-412) and inject_SESSION_TOKENinto the SPA (hermes_cli/web_server.py:357-360). A Cloudflare Tunnel without upstream authentication would therefore expose the dashboard remotely. The configured external-host path needs an auth/trusted-proxy security boundary. tests/hermes_cli/test_dashboard_host_allowlist.py:11-16does not configuredashboard.allowed_external_hosts, so it never tests the new behavior or the WebSocket Origin path.
Suggested changes
- Make external loopback aliases require dashboard authentication, or define and document an explicit authenticated trusted-proxy contract.
- Add config-loading and WebSocket Host/Origin tests for an allowlisted host, alongside the existing reject-by-default case.
Automated hermes-sweeper review.
| return True | ||
|
|
||
| # Loopback bind: accept the loopback names | ||
| # Loopback bind: accept the loopback names AND configured external hosts |
There was a problem hiding this comment.
Allowlisting a remote hostname here still leaves should_require_auth() false for the loopback bind, so the SPA receives its loopback session token without dashboard authentication. Please require an authenticated trusted-proxy path before accepting externally reachable aliases.
| def test_unknown_host_rejected(): | ||
| assert _is_accepted_host("evil.example.com:443", "127.0.0.1") is False | ||
|
|
||
|
|
There was a problem hiding this comment.
This test never configures dashboard.allowed_external_hosts, so the new allowlist path is untested. Add an isolated config fixture/reload that proves an allowlisted host is accepted and an unlisted one remains rejected.
Adds
dashboard.allowed_external_hostsconfig key (default[]) andreads it in
_is_accepted_host()so Cloudflare Tunnel and otherreverse-proxy setups can route WebSocket connections through loopback-
bound dashboards without triggering the Host header check.
Problem
When the dashboard binds to loopback (127.0.0.1 — the default), it
rejects any Host header that isn't localhost/::1. This breaks reverse-
proxy setups (nginx, caddy, Cloudflare Tunnel) where the proxy
forwards WS connections with the external hostname as the Host header.
Solution
A config key
dashboard.allowed_external_hosts(list of hostnames,case-insensitive, empty by default) that the Host header check also
accepts on loopback binds. Loopback names are always accepted
regardless.
Changes
hermes_cli/config.py: addedallowed_external_hosts: []defaulthermes_cli/web_server.py:_get_allowed_external_hosts()+ module-level memo, used in_is_accepted_hosttests/hermes_cli/test_dashboard_host_allowlist.py: 3 testsTesting
Usage