Dashboard: accept extra Host values so Tailscale MagicDNS names work - #62301
Dashboard: accept extra Host values so Tailscale MagicDNS names work#62301Zeus-Deus wants to merge 3 commits into
Conversation
The dashboard rejects any Host header that does not match the bound address, which is good DNS rebinding protection but makes it impossible to reach a gateway through a name that differs from the bind, like a Tailscale MagicDNS hostname or a tailscale serve proxy in front of a loopback bind. This adds an opt in allowlist of extra accepted Host values, configurable three ways that merge as a union: a repeatable --allowed-host flag on hermes dashboard and hermes serve, a HERMES_DASHBOARD_ALLOWED_HOSTS env var (comma separated), and dashboard.allowed_hosts in config.yaml. Entries are normalized (case, port, IPv6 brackets, URL forms, trailing dot) to match the header parsing, and the check applies to the HTTP middleware and both WebSocket host and origin guards through the same helper. Setting a non empty allowlist forces the auth gate regardless of bind address, since an allowlisted external name means requests are expected from beyond the local machine; the existing fail closed provider check then applies, with the error text extended to name the allowlist as the reason.
Related: this remains a competing reverse-proxy/MagicDNS Host-allowlist implementation alongside feature anchor #34390 and open work #37119 (durable-service consolidation) and #20515 (Tailscale identity auth). The live branch supports only repeatable |
|
Thanks for addressing the real reverse-proxy/MagicDNS limitation: current main rejects a non-bound Host in both HTTP ( Problems
Suggested changes
This is an automated hermes-sweeper review. |
…e check with the setup preflight Review feedback on the allowed hosts allowlist. The HERMES_DASHBOARD_ALLOWED_HOSTS env var is gone since .env is for secrets only, so the allowlist is now set through the repeatable --allowed-host flag or dashboard.allowed_hosts in config.yaml and the two sources merge as a union. Docs and .env.example updated to match. The interactive setup preflight used to skip every loopback bind, but a non empty allowlist forces the auth gate on even there, so an interactive user could skip setup and then hit the fail closed error at startup. Both paths now share a single effective_auth_required helper, and the preflight prompt explains that the allowlist is what engaged the gate when the bind itself is loopback. Adds a loopback plus allowlist regression test along with tests for the helper.
|
Both points addressed in b9cac28. Env var removed. The allowlist is now set only through the repeatable Preflight and start_server now share one condition. Added Tests: the new regression test runs the preflight with a loopback bind plus an allowlist and asserts it prompts instead of returning early, with a control case for the empty allowlist and unit tests for the helper. The union test now covers CLI plus config. 26 pass in the host header suite and 59 across that plus the auth gate suites, ruff clean. I also reran the live checks from the PR description: an allowlisted name is accepted with and without a port and with different casing, a config sourced entry works alongside the flag, an unknown host still gets 400, and a non TTY start with an allowlist but no provider still refuses to boot with the message naming the allowlist as the reason. |
|
@teknium1 Bumping this one too since it's been quiet for a bit. Both review points are addressed in b9cac28 and the branch is still conflict free and mergeable against current main. Quick recap of where it stands: the HERMES_DASHBOARD_ALLOWED_HOSTS env var is gone entirely, so the allowlist is set only through the repeatable Ready for another look whenever you get a chance. |
|
One small reviewability cleanup remains: the PR description still says the allowlist has three sources and lists Could you update the description and test summary to match the current head so the intended config/CLI behavior is unambiguous for final review? |
|
Good catch, the description was still describing the pre review version. Updated it to match the current head: two sources only, the repeatable --allowed-host flag and dashboard.allowed_hosts in config.yaml, and the Testing section now reflects the current suites (26 in the host header suite, 142 total with the auth gate and WebSocket auth suites) plus the setup preflight coverage that was added during review. No .env.example changes anymore either since the env var path is gone. |
# Conflicts: # hermes_cli/main.py # hermes_cli/web_server.py
What this does
The dashboard rejects any request whose Host header does not exactly match the address the server was bound to. That is good DNS rebinding protection, but it makes some very normal setups impossible. My case: the gateway runs on my home server inside my tailnet, and I want to reach it by its Tailscale MagicDNS name. If I bind to the Tailscale IP, connecting via the hostname gets a 400 Invalid Host header. Same story for tailscale serve, which proxies to a loopback bind but forwards the external hostname.
This PR adds an opt in allowlist of extra Host values the server will accept. You can set it two ways, and they merge together:
a repeatable flag:
hermes serve --host 100.x.y.z --allowed-host myserver.tailnet.ts.netconfig:
dashboard.allowed_hostsin config.yamlEntries are normalized the same way the Host header is parsed (case, port, IPv6 brackets, full URLs, trailing dot), and the check goes through the same helper used by the HTTP middleware and both WebSocket host and origin guards, so all three surfaces behave the same.
The security part
Setting a non empty allowlist forces the auth gate on, regardless of the bind address. Allowlisting an external hostname means you expect requests from beyond the local machine, and without this rule a loopback bind behind tailscale serve would hand the unauthenticated dashboard, including its injected session token, to everyone who can reach the proxy. The existing fail closed check applies too: if the allowlist is set and no auth provider is configured, the server refuses to start, and the error message now names the allowlist as the reason. Starting it interactively in that state offers the auth setup prompt first instead of failing right away, through the same effective_auth_required condition start_server uses. Nothing changes when the allowlist is empty, which is the default.
Testing
New unit tests cover the validator with allowed hosts on loopback and non loopback binds, the entry normalization, the config plus flag merging, the auth forcing rule, the setup preflight on a loopback bind with an allowlist, the HTTP middleware accept and reject paths, and WebSocket accept and 4403 reject. The full host header suite passes (26 tests), along with the auth gate and WebSocket auth suites (142 passed total), and ruff is clean.
I also verified it against a live server: an allowlisted MagicDNS name is accepted with and without a port and with different casing, an unknown host still gets 400,
/api/statusreports the forced auth gate, starting with an allowlist but no auth provider refuses to boot with a helpful message, and a server without an allowlist behaves exactly as before.Docs updated in the web dashboard guide (flag table, DNS rebinding note, a new MagicDNS section under Tailscale), SECURITY.md, and the security user guide. I left the zh-Hans translation mirror untouched.
Related to #62293, which adds a connect to server option to the desktop app first launch. Together they make the home server setup work end to end over Tailscale.