TiantianFlow/dsh-tailscale-gateway
Give selected Tailnet users private browser access to a local DeepSeek Harness (DSH) Web UI—without exposing DSH on your LAN or the public Internet. This small, dependency-free DSH Web-profile bundle keeps DSH and the gateway on loopback, then uses Tailscale Serve
catalog 简介 / catalog descriptioncatalog description:Private Tailscale access for DeepSeek Harness Web: exact user allowlists, loopback-only gateway, and guarded Serve setup
编辑导读Editor's Brief
## 核心特性
- 该插件将远程 DSH Web 访问限制为 trustedLogins 中列出的精确 Tailscale 用户登录名,其他 Tailnet 用户会收到 403 响应。
- 它将网关监听器固定为 127.0.0.1:3088,且仅转发到本地 DSH 源站 127.0.0.1:3080,避免使用局域网或公网监听器。
- 它通过 Tailscale Serve 代理 DSH UI、HTTP API 和 WebSocket 事件流,并将其作为唯一远程入口。
- 引导式设置会在确认后写入一个已启用的 Web-profile 条目,将节点所有者推断为第一个受信任登录名,并选择一个可用的 HTTPS 端口。
- ensure 模式仅会在缺失时创建指向回环网关的私有根路由,对其进行校验,拒绝冲突,并且从不使用 Funnel、reset 或 off。
## 使用场景
- 与选定的 Tailscale 身份共享本地 DSH Web 实例,而不是允许每个能访问 Tailnet 节点的设备。
- 通过 HTTPS 远程访问本地 DSH Web UI,而不在本地网络中暴露 3080 或 3088 端口。
- 在 DSH 前添加身份感知边界,同时将可选的 Tailnet ACL 或 grant 规则保留为独立的网络可达性控制。
## 技术细节
- 该 bundle 是一个面向 Node.js 20 或更高版本的 DSH Web-profile 插件,并通过 dsh plugin --profile web add -w 安装到 pnpm workspace 根目录。
- 对于非安全请求、API 请求和 WebSocket 升级,该网关要求必须恰好有一个由 Tailscale 注入的 Tailscale-User-Login 头,以及预期的外部 Host 和预期的外部 Origin。
- 在转发前,它会移除浏览器凭据、客户端提供的代理头和 Tailscale 头,以及逐跳头,然后将上游 Host 和 Origin 重写为回环值。
- 配置接受 publicOrigin、trustedLogins 和可选的 tailscaleServe,同时拒绝 listener、upstream、TLS、OAuth、secret 和未知键。
## 注意事项
- 仅安装插件不会启动监听器或更改 Tailscale,生成的配置只有在用户自己的 DSH Web 进程重启后才会生效。
- 标签设备和 Funnel 流量不会提供可用的用户登录身份,因此会被网关拒绝。
- 移除插件不会自动移除持久化的 Tailscale Serve 路由,包括由 ensure 创建的路由。
## Core Features
- The plugin restricts remote DSH Web access to exact Tailscale user logins listed in trustedLogins, while other Tailnet users receive a 403 response.
- It keeps the gateway listener fixed to 127.0.0.1:3088 and forwards only to the local DSH origin at 127.0.0.1:3080, avoiding LAN or public listeners.
- It proxies the DSH UI, HTTP API, and WebSocket event streams through Tailscale Serve as the only remote ingress.
- Guided setup writes an enabled Web-profile entry after confirmation, infers the node owner as the first trusted login, and selects an available HTTPS port.
- The ensure mode creates only an absent private root route to the loopback gateway, verifies it, refuses conflicts, and never uses Funnel, reset, or off.
## Use Cases
- Share a local DSH Web instance with selected Tailscale identities instead of every device that can reach the Tailnet node.
- Access a local DSH Web UI remotely through HTTPS without exposing port 3080 or port 3088 on the local network.
- Add an identity-aware boundary in front of DSH while keeping optional Tailnet ACL or grant rules as separate network-reachability controls.
## Technical Details
- The bundle is a DSH Web-profile plugin intended for Node.js 20 or later and is installed into a pnpm workspace root with dsh plugin --profile web add -w.
- The gateway requires exactly one Tailscale-injected Tailscale-User-Login header, the expected external Host, and the expected external Origin for unsafe requests, API requests, and WebSocket upgrades.
- Before forwarding, it strips browser credentials, client-provided proxy and Tailscale headers, and hop-by-hop headers, then rewrites upstream Host and Origin to loopback values.
- Configuration accepts publicOrigin, trustedLogins, and optional tailscaleServe, while listener, upstream, TLS, OAuth, secret, and unknown keys are rejected.
## Notes
- Installation alone does not start a listener or change Tailscale, and the generated configuration becomes active only after the user-owned DSH Web process is restarted.
- Tagged devices and Funnel traffic do not provide a usable user-login identity and are rejected by the gateway.
- Removing the plugin does not automatically remove a persistent Tailscale Serve route, including a route created by ensure.
安装Install
dsh plugin --profile web add -w dsh-tailscale-gateway
把 TiantianFlow/dsh-tailscale-gateway 加入你的 DSH 配置(web profile)即可启用。
READMEREADME
dsh-tailscale-gateway
Private DSH Web access for the people you choose—not your whole network.
Give selected Tailnet users private browser access to a local DeepSeek Harness (DSH) Web UI—without exposing DSH on your LAN or the public Internet. This small, dependency-free DSH Web-profile bundle keeps DSH and the gateway on loopback, then uses Tailscale Serve as the only remote ingress.
Allowlisted Tailnet browser ─ HTTPS ─> Tailscale Serve
│
└─ identity-aware gateway ─> local DSH
127.0.0.1:3088 127.0.0.1:3080
What it gives you: an exact Tailscale-login allowlist in front of DSH, a loopback-only gateway, and conflict-safe private Serve setup. Installing alone does nothing. Guided setup writes an enabled profile entry only after your confirmation; the next DSH Web start activates it. It never opens a LAN listener or configures Funnel.
Why this gateway?
Tailscale delivers an authenticated connection. This bundle turns that connection into a deliberately narrow, DSH-aware access boundary:
| What you need | What this bundle does |
|---|---|
| Share DSH with only specific people | Allows exact Tailscale-User-Login identities, not every Tailnet member who can reach the node. |
| Keep DSH local | Pins both DSH and the gateway to 127.0.0.1; there is no LAN listener or public Funnel mode. |
| Use the normal DSH web app remotely | Guards and proxies the UI, HTTP API, and WebSocket event streams, then forwards to DSH as its fixed loopback origin. |
| Avoid route accidents | Setup infers the node owner and a safe available HTTPS port; ensure creates only its one absent route and refuses conflicts or Funnel. |
| Preserve existing services | It never resets Serve, overwrites another handler, or removes a route automatically. |
The result is straightforward: install, approve the generated plan, restart
DSH, then open the Tailscale URL. There is no separate tailscale serve
command in the normal flow.
Set up in two commands, then restart DSH
You need a working local DSH Web profile and a non-tagged Tailscale node with MagicDNS and HTTPS enabled. Node.js 20+ is normally supplied by DSH.
Install the inert bundle. This alone neither starts a listener nor changes Tailscale.
dsh plugin --profile web add -w github:TiantianFlow/dsh-tailscale-gateway-wis required because DSH's Web profile is a pnpm workspace root.Run guided setup and confirm the displayed plan. It infers the current node owner's Tailscale login as the first allowlisted user; you can replace it at the prompt.
dsh plugin --profile web exec dsh-tailscale-gateway-setupYour confirmation writes an enabled profile entry with guarded
tailscaleServe.mode: ensure. It also picks the current node owner as the first trusted login and a safe available HTTPS port. Restart the DSH Web process or service you already own now. DSH loads newly installed bundles on startup; setup never guesses, kills, or restarts your supervisor. On that startup, the plugin may keep an exact private route or create and verify one absent root route to127.0.0.1:3088. It never runs Funnel, reset, or off, and it refuses a conflict.
After the restart, open the configured URL from an allowlisted Tailscale user.
That is the success criterion; port 3088 itself remains unreachable from the
LAN and tailnet. Use --print to preview without writing, or --yes for a
reviewed non-interactive run.
After setup
The saved configuration is intentionally not a claim that the URL is already live. Restart DSH Web using the process manager you already own. If the loopback port is occupied, Tailscale is unavailable, or the selected Serve route conflicts, the plugin fails closed at startup and leaves unrelated routes unchanged. Resolve the stated startup error, then restart DSH again.
Who can use it?
Setup automatically adds the current node owner to trustedLogins. That
exact Tailscale login is authorized to use DSH after the restart. It does not
add every person in the tailnet.
| Layer | What it decides | Default result for another tailnet user |
|---|---|---|
trustedLogins (required) |
May this authenticated Tailscale identity use DSH? | The gateway returns 403; DSH is not forwarded. |
| Tailnet ACL/grant (optional) | May this person's network connection reach the Serve endpoint? | They may reach the endpoint unless your tailnet policy blocks it. |
In other words, an optional Tailnet grant is defense in depth for network
reachability. It does not authorize DSH use. Without a grant (and absent
another restrictive Tailnet policy), an unlisted tailnet user can reach the
endpoint, but the gateway sees their Tailscale identity and returns 403.
Without a matching trustedLogins entry, they cannot use DSH.
To share DSH deliberately, edit the generated Web-profile entry and add each person's exact Tailscale login. Every allowlisted person is a full DSH administrator; use the login identity from the Tailscale admin console, not a display name. Restart the DSH Web process you own after changing the generated entry.
- insert:
- id: dsh-tailscale-gateway-user-instance
name: dsh-tailscale-gateway
config:
trustedLogins:
- 'owner@example.invalid'
- 'another-admin@example.invalid'
Optional: restrict Tailnet reachability too
The gateway allowlist is always required. If you also want the tailnet itself to prevent non-admins from reaching the endpoint, add a narrow Tailnet policy. This generic grant is an example; replace the group member, gateway Tailscale IP, and chosen HTTPS port.
{
"groups": {
"group:dsh-admins": ["admin@example.invalid"],
},
"hosts": {
"dsh-gateway": "100.64.0.10",
},
"grants": [
{
"src": ["group:dsh-admins"],
"dst": ["dsh-gateway"],
"ip": ["tcp:8443"],
},
],
}
Grants are additive: a broader existing rule can still permit network
delivery. Review overlapping rules if you want Tailnet policy to be restrictive.
Neither a grant nor an ACL replaces trustedLogins.
Advanced: manual configuration
Most people should use guided setup. Use this section only when you need a different owner/login, a chosen canonical URL, or to manage Serve yourself. The complete sanitized template is examples/web-profile.patch.yml. It is a boot-time, legacy-compatible baseline example; guided setup creates the same kind of safe enabled entry for you.
The only normal enabled configuration keys are publicOrigin,
trustedLogins, and optional tailscaleServe. Guided setup also writes an
opaque activationToken reserved for a loopback-only diagnostic protocol; it
is not remote authentication. Leave it in place and do not share the generated
profile. Listener, upstream, TLS, OAuth, secret, and unknown keys are
rejected. publicOrigin must be the exact origin-only HTTPS *.ts.net URL
users open; logins are exact and case-sensitive.
Installation alternatives
The Quick Start uses GitHub. For development from a source checkout:
git clone https://github.com/TiantianFlow/dsh-tailscale-gateway.git
cd dsh-tailscale-gateway
dsh plugin --profile web add -w "$PWD"
If a future npm release is published, the equivalent install is:
dsh plugin --profile web add -w dsh-tailscale-gateway
Let the plugin manage its one route
This is the mode generated by setup. The public HTTPS port is derived from
publicOrigin; do not configure it a second time.
- id: dsh-tailscale-gateway
config:
enabled: true
publicOrigin: 'https://your-device.your-tailnet.ts.net:8443'
trustedLogins:
- 'replace-with-an-exact-tailscale-login@example.invalid'
tailscaleServe:
mode: ensure
After DSH has started and the sidecar has bound, ensure uses an argv-based local
Tailscale command (never a shell) to inspect serve status --json. It creates
only an absent exact root route to http://127.0.0.1:3088, verifies it, and
will not overwrite another handler or port. It never runs funnel, reset,
or off.
Manage Serve yourself
Omit tailscaleServe or set mode: manual when you want full route ownership.
After DSH starts the loopback sidecar, create the matching private route
yourself. Here the external URL and command both use port 8443:
tailscaleServe:
mode: manual
tailscale serve --https=8443 --bg http://127.0.0.1:3088
tailscale serve status --json
Do not substitute tailscale funnel: Funnel is public and does not provide the
identity header this gateway requires. Avoid tailscale serve reset unless you
intend to remove every Serve route on the node.
What it protects—and what it does not
- The listener is fixed to
127.0.0.1:3088; its only upstream is fixed to127.0.0.1:3080. - It requires exactly one Tailscale-injected
Tailscale-User-Loginheader, the exact externalHost, and the exact externalOriginfor unsafe requests,/apirequests, and WebSocket upgrades. - It strips browser credentials, client-provided proxy/Tailscale headers, and
hop-by-hop headers before forwarding, then rewrites upstream
HostandOriginto loopback. - It has no TLS listener, cookie/session store, OAuth/OIDC flow, Cloudflare dependency, DSH core patch, direct LAN listener, or public listener.
Tailscale Serve replaces a caller-supplied identity header with the authenticated user's identity before sending a request to its local backend. The gateway must therefore remain loopback-only. A same-host process could forge a loopback request, but it is already in the same local trust boundary as DSH itself. Tagged devices and Funnel traffic do not provide a usable user-login identity and are rejected.
Operations
To stop remote access while keeping the bundle installed, set enabled: false
inside the generated user-instance profile entry, then restart the DSH Web
process you own. The private Serve route intentionally persists. Remove it
separately only if you want to discard that route:
tailscale serve --https=8443 off
Before uninstalling, remove the entire setup-generated top-level - insert:
block (or leave it disabled), restart DSH, then remove the package:
dsh plugin --profile web remove -w dsh-tailscale-gateway
Removing the bundle never removes a persistent Tailscale Serve route
automatically—not even one that ensure created. The shipped disabled baseline
remains for existing installations that use the older
id: dsh-tailscale-gateway configuration; setup deliberately does not replace
or migrate that entry. DSH may still
hide controls intended only for local interaction; this gateway transports the
normal DSH UI/API and does not change DSH product policy.
Development
pnpm install --frozen-lockfile
pnpm run check
pnpm test
pnpm audit --prod
npm pack --dry-run
See CONTRIBUTING.md for contribution guidance, SECURITY.md for private vulnerability reporting, and CODE_OF_CONDUCT.md for community expectations. Maintainers can use RELEASING.md for future-release guidance.
Community
Thanks to LINUX DO for providing a space for Chinese developers to exchange ideas and feedback. This acknowledgement does not imply affiliation or official endorsement.
amruthpillai/reactive-resume
tt-a1i/archify
zhu1090093659/dsh-web-ui
strukto-ai/mirage
liustack/modlens
omdsh-dev/DSH-better-sidebar
dsh-market/dsh-market