feat(dashboard): allow Tailscale Serve identity auth via dashboard.tailscale_allowlist#20515
Open
dima-hermes-bot wants to merge 2 commits into
Open
Conversation
538d68c to
596e05b
Compare
19 tasks
5f1edfd to
3a71dba
Compare
56c16c6 to
5dc31e4
Compare
19 tasks
…pies The 100.64.0.0/10 block (RFC 6598 "Shared Address Space", used by Tailscale and WireGuard) appeared three times across the codebase under two different names. Consolidate into agent.networks.CGNAT_NETWORK so future tailnet-related work has a single import. - agent/model_metadata.py: replaces _TAILSCALE_CGNAT - tools/url_safety.py: replaces _CGNAT_NETWORK - tools/browser_tool.py: replaces an inline ip_network() call that was rebuilt on every URL-safety check; now a module-level constant
…ilscale_allowlist Adds an opt-in identity-based auth path: when a request arrives from a trusted source (loopback or 100.64.0.0/10) carrying a Tailscale-User-Login header set by `tailscale serve`, the dashboard accepts it iff the login appears in dashboard.tailscale_allowlist. Empty allowlist fails closed. Refactors the existing token-based check into a single _dashboard_auth_decision used by both HTTP and WebSocket transports, and consolidates the four WS routes behind a new _ws_gate. Side effect: HTML and asset paths now require auth (the prior middleware only gated /api/). Local browsers hitting http://localhost still pass via the loopback carve-out; tailnet users without identity headers must include ?token= on first load. tests/hermes_cli/test_web_server_auth.py covers the policy matrix (33 cases) — loopback peer, CGNAT peer, public bind, identity present/absent, allowlist hit/miss, MagicDNS host, untrusted peer.
5dc31e4 to
ef76e67
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds an opt-in Tailscale identity-based auth path to the local dashboard.
When
dashboard.tailscale_allowlistlists aTailscale-User-Loginand atrusted peer (loopback or 100.64.0.0/10) presents that header, the
dashboard authenticates the request without requiring the page-embedded
session token. Empty allowlist fails closed.
This is the natural complement to running the dashboard behind
tailscale serve: tailnet users get authenticated by tailscaled'sidentity injection, and the page no longer has to be served
unauthenticated to bootstrap its embedded token.
Side effect: the previous
auth_middlewareonly gated/api/. HTML andasset paths are now gated too, with a loopback carve-out so local
browsers loading
http://localhoststill bootstrap normally. Tailnetusers without identity headers must include
?token=...on first load.The HTTP and WebSocket auth checks are now unified behind a single
_dashboard_auth_decisionpolicy, and the four WebSocket routes share anew
_ws_gateso a future route can't accidentally skip auth or theembedded-chat feature flag.
This PR also pulls a small refactor: the
100.64.0.0/10(RFC 6598"Shared Address Space")
IPv4Networkconstant existed in three placesunder two names. It now lives in
agent/networks.CGNAT_NETWORKand isimported by the four call-sites that need it.
Note on overlap with #19959
This PR overlaps in scope with #19959 ("feat: add OpenClaw-style dashboard
authentication"), which adds a full multi-mode
DashboardAuthManagerincluding a Tailscale identity mode. This one is intentionally narrower —
it adds only Tailscale Serve identity auth and consolidates the existing
HTTP/WS auth checks, without introducing a new auth subsystem, CLI flags,
or config namespace. If maintainers prefer the larger redesign, please
feel free to close this in favour of #19959; the test matrix and the
spoofable-loopback-header concerns explored here may still be useful as
review input there. The
CGNAT_NETWORKextraction is independent ofeither auth approach and could land on its own.
Type of Change
Changes Made
hermes_cli/config.py— addsdashboard.tailscale_allowlist(default empty list).hermes_cli/web_server.py— new_dashboard_auth_decisionshared by HTTP and WS;_ws_gateconsolidates the four WebSocket route preludes;auth_middlewarenow gates HTML/asset paths with a loopback carve-out;_is_accepted_hostrecognises*.ts.netHosts on loopback bind.tests/hermes_cli/test_web_server_auth.py— 33 tests covering the policy matrix (loopback peer, CGNAT peer, public bind, identity present/absent, allowlist hit/miss, MagicDNS host, untrusted peer).agent/networks.py(new) — exportsCGNAT_NETWORK.agent/model_metadata.py,tools/url_safety.py,tools/browser_tool.py— replace local CGNAT constants with the shared one.How to Test
pytest tests/hermes_cli/test_web_server_auth.py tests/hermes_cli/test_web_server_host_header.py tests/hermes_cli/test_web_server.py tests/agent/test_local_stream_timeout.py tests/tools/test_url_safety.py -q— all 297 should pass.hermes dashboardon loopback. Local browser athttp://localhost:9119loads as before; no token surface change.dashboard.tailscale_allowlist: ["you@example.com"]to~/.hermes/config.yaml. Behindtailscale serve, the tailnet user with that login passes; another login is rejected.?token=...exactly as before.Checklist
Code
pytest tests/ -q— failures onmainare pre-existing and unrelated (Bedrock 1M headers, run_agent test stubs, Dockerfile content) or parallel-execution flakes; the 297 tests touching files this PR changes all passDocumentation & Housekeeping
cli-config.yaml.example— N/A (default config is generated fromDEFAULT_CONFIG)CONTRIBUTING.md/AGENTS.mdipaddress, no OS-specific code