Skip to content

feat(dashboard): allow Tailscale Serve identity auth via dashboard.tailscale_allowlist#20515

Open
dima-hermes-bot wants to merge 2 commits into
NousResearch:mainfrom
dima-hermes-bot:feat/dashboard-tailscale-allowlist
Open

feat(dashboard): allow Tailscale Serve identity auth via dashboard.tailscale_allowlist#20515
dima-hermes-bot wants to merge 2 commits into
NousResearch:mainfrom
dima-hermes-bot:feat/dashboard-tailscale-allowlist

Conversation

@dima-hermes-bot

Copy link
Copy Markdown

What does this PR do?

Adds an opt-in Tailscale identity-based auth path to the local dashboard.
When dashboard.tailscale_allowlist lists a Tailscale-User-Login and a
trusted 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's
identity injection, and the page no longer has to be served
unauthenticated to bootstrap its embedded token.

Side effect: the previous auth_middleware only gated /api/. HTML and
asset paths are now gated too, with a loopback carve-out so local
browsers loading http://localhost still bootstrap normally. Tailnet
users without identity headers must include ?token=... on first load.

The HTTP and WebSocket auth checks are now unified behind a single
_dashboard_auth_decision policy, and the four WebSocket routes share a
new _ws_gate so a future route can't accidentally skip auth or the
embedded-chat feature flag.

This PR also pulls a small refactor: the 100.64.0.0/10 (RFC 6598
"Shared Address Space") IPv4Network constant existed in three places
under two names. It now lives in agent/networks.CGNAT_NETWORK and is
imported 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 DashboardAuthManager
including 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_NETWORK extraction is independent of
either auth approach and could land on its own.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix (HTML/asset paths previously served without auth)
  • ♻️ Refactor (CGNAT_NETWORK extraction)

Changes Made

  • hermes_cli/config.py — adds dashboard.tailscale_allowlist (default empty list).
  • hermes_cli/web_server.py — new _dashboard_auth_decision shared by HTTP and WS; _ws_gate consolidates the four WebSocket route preludes; auth_middleware now gates HTML/asset paths with a loopback carve-out; _is_accepted_host recognises *.ts.net Hosts 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) — exports CGNAT_NETWORK.
  • agent/model_metadata.py, tools/url_safety.py, tools/browser_tool.py — replace local CGNAT constants with the shared one.

How to Test

  1. 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.
  2. Manual:
    • Run hermes dashboard on loopback. Local browser at http://localhost:9119 loads as before; no token surface change.
    • Add dashboard.tailscale_allowlist: ["you@example.com"] to ~/.hermes/config.yaml. Behind tailscale serve, the tailnet user with that login passes; another login is rejected.
    • With an empty allowlist (default), tailnet identity headers are ignored and tailnet users continue to authenticate via ?token=... exactly as before.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate (feat: add OpenClaw-style dashboard authentication #19959 noted above)
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've run pytest tests/ -q — failures on main are 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 pass
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu / Linux 6.17

Documentation & Housekeeping

  • N/A docs (no user-facing flag beyond the config key, which is self-describing)
  • cli-config.yaml.example — N/A (default config is generated from DEFAULT_CONFIG)
  • N/A CONTRIBUTING.md / AGENTS.md
  • Cross-platform impact considered — pure Python ipaddress, no OS-specific code
  • N/A tool descriptions/schemas

@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 area/auth Authentication, OAuth, credential pools labels May 6, 2026
@dima-hermes-bot dima-hermes-bot force-pushed the feat/dashboard-tailscale-allowlist branch from 538d68c to 596e05b Compare May 8, 2026 05:58
@dima-hermes-bot dima-hermes-bot force-pushed the feat/dashboard-tailscale-allowlist branch 3 times, most recently from 5f1edfd to 3a71dba Compare May 14, 2026 09:39
@dima-hermes-bot dima-hermes-bot force-pushed the feat/dashboard-tailscale-allowlist branch 3 times, most recently from 56c16c6 to 5dc31e4 Compare May 28, 2026 23:41
…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.
@dima-hermes-bot dima-hermes-bot force-pushed the feat/dashboard-tailscale-allowlist branch from 5dc31e4 to ef76e67 Compare June 4, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants