feat(dashboard): allow extra hostnames in Host validator via env var#29195
feat(dashboard): allow extra hostnames in Host validator via env var#29195StartupBros wants to merge 1 commit into
Conversation
The dashboard's anti-DNS-rebinding Host validator (`hermes_cli/web_server.py::_is_accepted_host`) accepts only the bound interface — plus loopback aliases when bound to loopback. That's correct for the default deployment but blocks operators who run the dashboard behind a reverse proxy or `tailscale serve` mapping a public tailnet hostname back to `127.0.0.1:9119`: in those setups the proxy forwards with the public hostname in the Host header, which the validator rejects. `HERMES_DASHBOARD_ADDITIONAL_HOSTS=host1.example,host2.example` adds specific hostnames to the loopback accept list. Hosts NOT in the list still get rejected, so a DNS-rebinding attacker host that TTL-flips to 127.0.0.1 is still blocked. The opt-in is per-operator (env var), so the default stays safe. Tests in tests/hermes_cli/test_web_server_host_header.py cover: listed hosts accepted on every loopback bind, attacker hosts still rejected with env set (incl. suffix/subdomain variants), case-insensitive matching, comma-separated parsing with whitespace, empty/unset env is a no-op, env var ignored on explicit non-loopback bind, and 0.0.0.0 unaffected.
561d6e9 to
0fc48ec
Compare
|
Status re: @alt-glitch's duplicate note above: |
|
Correction to my earlier status comment: I said "Reviewed all 5 competing PRs" — but that was the set the duplicate-detector bot enumerated. A wider search turned up more PRs in the dashboard-host space that the bot didn't flag:
Posted a brief acknowledgment comment on #20871 since that's the closest direct comparator. For the maintainer's triage: this PR remains the narrowest host-allowlist-only diff with the most focused test coverage, but #20871 has equivalent placement logic if a single host-allowlist + voice multi-fix is preferred. |
Problem
The dashboard's anti-DNS-rebinding Host validator (
hermes_cli/web_server.py::_is_accepted_host) currently accepts only the bound interface — plus loopback aliases when bound to loopback. That's correct for the default deployment but blocks operators who run the dashboard behind:tailscale servemapping a public tailnet hostname back to127.0.0.1:9119In those setups the proxy forwards with the public hostname in the
Hostheader, which the validator rejects with400 "Invalid Host header. Dashboard requests must use the hostname the server was bound to."Bypassing this today requires
--insecure, which binds to all interfaces and exposes API keys on the network (the CLI help calls this out explicitly). That's a much larger blast radius than the operator needs — they just want loopback bind + one or two known proxy hostnames accepted.Solution
A minimal env-var-gated extension:
When set, the loopback-bound dashboard also accepts requests whose Host header matches one of the listed hostnames. The DNS-rebinding defence is preserved for any host NOT in the list, so an attacker hostname that TTL-flips to 127.0.0.1 still gets rejected.
The opt-in is per-operator (env var), not a config-file default — so the safe behavior remains the default and operators consciously add hostnames they trust.
Use case (concrete)
I run the dashboard loopback-bound on a Debian VM, then
sudo tailscale serve --bg --https=443 http://127.0.0.1:9119to make it reachable from my other tailnet machines. Tailscale ACLs gate access to the tailnet; the dashboard's loopback bind keeps the API-key blast radius small; the env var bridges the gap so the validator doesn't reject the tailscale-MagicDNS Host header.Without this patch the choice is
--insecure(defeats the security model) or a local fork (drift risk on every upgrade).Test plan
A new
TestHostHeaderAdditionalHostsclass intests/hermes_cli/test_web_server_host_header.py(the file annotated for GHSA-ppp5-vxwm-4cf7) covers:127.0.0.1,localhost,::1), with and without port suffixmagic.tailnet.ts.net.attacker.test) and subdomain attacks (attacker.magic.tailnet.ts.net)0.0.0.0(--insecure) behavior unchangedAll 15 tests in the file pass (5 existing + 7 new + 3 middleware).
Notes
HERMES_DASHBOARD_ADDITIONAL_HOSTSfollows the existingHERMES_*env-var convention (HERMES_QWEN_BASE_URL,HERMES_DOCKER_BINARY,HERMES_HUMAN_DELAY_MODE, etc.)