dshbase

插件目录 / Security / dsh-one-gateway

dsh-one-gateway

已验证 · 实测可装 TiantianFlow

✓ 持续维护

查看 GitHub ↗ ← 返回插件目录

13Stars
1Forks
0未关闭 issue
JavaScript语言
2026-08-26最近推送
跨平台平台

功能简介

Private DSH One Gateway — loopback, identity-first ingress for DeepSeek Harness

✅
我们的评价
可用 — 实测通过,早期项目

Private DSH One Gateway — loopback, identity-first ingress for DeepSeek Harness 实测能干净安装、正常启动。早期项目,但功能可用。

「已验证」表示我们的自动化 CI 在干净 profile 里实际执行了 dsh plugin add 并启动成功——仅此而已。功能描述与版本兼容性均为作者声明。这不是安全审计,也不代表对第三方代码的背书。

README

DSH One Gateway

MIT license DSH Web-profile bundle Node.js 20 or later

English · 简体中文

Share DSH Web with the people you choose — not your whole network.

A DeepSeek Harness (DSH) plugin
that puts a private, zero-trust gateway in front of DSH Web. One allowlist.
No user-chosen password. Being on the same Wi-Fi, tailnet, or mesh is never
enough to get in.

Getting started

You need a working local DSH Web profile and Node.js 20+ (normally supplied by
DSH). Installing the plugin does nothing until you run setup. Nothing is
exposed.

Install

dsh plugin --profile web add github:TiantianFlow/dsh-one-gateway

Set up

dsh plugin --profile web exec dsh-gateway -- setup

Setup opens a menu, previews a plan, and waits for you to confirm. It refuses
public or anonymous defaults. Operators on Tailscale.com are steered to
identity-aware Tailscale Serve.

Then restart the DSH Web process you already own, and open the configured HTTPS
origin as an allowlisted principal. Port 3088 itself remains unreachable from
the LAN and from the provider network.

The full command is dsh-one-gateway; dsh-gateway is a shorter alias.
Cloudflare Access, Headscale, local checkouts, and unattended flags are in
Setup in detail.

What you get

An exact principal allowlist in front of DSH, a loopback-only HTTP/WebSocket
proxy, and a single onboarding command that previews a plan and refuses public
or anonymous defaults.

The gateway and DSH stay on loopback. Tailscale Serve, Cloudflare Tunnel with
Cloudflare Access, or Headscale via Tailscale TCP Serve is only the private
ingress. Joining that private network is never an authorization decision.
Every request must resolve one unambiguous, allowlisted principal before
anything is forwarded to DSH. That is self-hosted access control for a zero
trust homelab: reachability is not permission.

Allowlisted browser ─ HTTPS ─> provider ingress (Tailscale Serve, Cloudflare Access,
                                      │           or Headscale TCP Serve)
                                      └─ loopback gateway ─> local DSH
                                         127.0.0.1:3088   127.0.0.1:3080

Callers authenticate through Tailscale Serve, Cloudflare Access, or — on
Headscale — a generated gateway credential in front of private TCP Serve.

How this is different

Other DSH gateways may bind off loopback, patch DSH internals so a gate stays
exhaustive after upgrades, or run a reverse proxy in front of DSH. Those
designs can cover /api and WebSockets too; the difference is not who covers
more of the surface. This plugin is a different contract: DSH itself never
leaves loopback.

  1. Private network membership is never authorization. Binding 0.0.0.0 or
    treating RFC1918 as an allow is out of scope. The listener stays on loopback.
    Being on the same Wi-Fi, tailnet, or mesh does not get you in.
  2. Fail-closed DSH origin. DSH stays on loopback; the gateway is the only
    listener in front of it. A DSH upgrade cannot silently add a route that
    becomes reachable off-host — there is no gate route table to keep
    exhaustive, because DSH was never reachable off-host to begin with. A missed
    route in a full-coverage gate is a silent bypass; a missed route in this
    bridge just breaks that one proxied path. It does not expose DSH.
  3. No DSH-core or client-library patches. Some gates stay exhaustive by
    patching DSH HTTP and upgrade entry points, then re-applying those patches
    after every upgrade — because an upstream change can silently undo them.
    This gateway is an external process. DSH's own code is never modified.
  4. For Tailscale Serve and Cloudflare Access, identity comes from the
    provider — not a login page, password, or shared token.

    Password forms, shared tokens, and session-cookie doors are a large auth
    surface and a common source of bugs. Those two shipped modes use Serve's
    injected Tailscale-User-Login, or a locally verified Cloudflare Access
    JWT. We check an allowlist. We do not ask you to invent a password.
    gateway-credential is a smaller, purpose-built login for transports with
    no native identity: a generated per-principal credential (not a user-chosen
    password), verifier-only storage, a bounded
    HttpOnly/Secure/SameSite=Strict session, individual revocation, and
    rate limiting without permanent lockout. Compared with a typical
    user-chosen or shared password, that is stronger on guessability, storage
    disclosure, and revocation; it is not "passwordless" and not a claim of
    superiority over every password or passkey. Headscale TCP Serve is the
    shipped transport that uses this mode. For any transport-only provider with
    no native identity, the contract is a product-owned bridge from the private
    overlay to the unchanged loopback gateway, authenticated with
    gateway-credential — never a fabricated identity header.
  5. One plugin, one onboarding command, one allowlist. Instead of a different
    bespoke setup per provider, Tailscale Serve, Cloudflare Tunnel with Access,
    and Headscale TCP Serve share one loopback gateway. A new provider is
    another adapter, not another product.

Setup in detail

In a terminal, omit --provider to choose from a menu — that is the command
in Getting started. Detection of a local executable is a
hint and, when exactly one provider is found, a default — not a configuration
check. Pass --provider to skip the menu. Non-interactive setup still
auto-selects when exactly one provider executable is detected, and otherwise
requires --provider. Headscale TCP Serve is listed when the live node is on
Headscale.

From a local checkout instead of GitHub:

dsh plugin --profile web add -w /path/to/dsh-one-gateway

Tailscale Serve:

dsh plugin --profile web exec dsh-gateway -- setup --provider tailscale-serve

Cloudflare Access (you configure Access yourself; the gateway only verifies
the token locally). You must already have an Access application forwarding
only to 127.0.0.1:3088:

dsh plugin --profile web exec dsh-gateway -- setup --provider cloudflare-access \
  --external-origin 'https://dsh.example.invalid' \
  --team-origin 'https://team.example.invalid' \
  --application-audience 'replace-with-access-application-audience' \
  --trusted-principal 'email:operator@example.invalid'

In a TTY, omitted Cloudflare values are collected interactively in this
order: existing Access origin, team origin, application audience, trusted
email. Unattended --yes still requires all four flags. Setup never creates
a tunnel, DNS record, or Access application.

Headscale TCP Serve (private reachability plus a generated gateway
credential; you supply the certificate). Setup on Tailscale.com will not
offer this as an equal menu choice:

dsh plugin --profile web exec dsh-gateway -- setup --provider headscale-tcp-serve \
  --tls-cert /path/to/dsh-one-gateway/cert.pem \
  --tls-key /path/to/dsh-one-gateway/key.pem \
  --credential-store /path/to/dsh-one-gateway/credentials.json \
  --trusted-principal operator-1

TCP Serve does not terminate HTTPS and does not prove identity. The gateway
terminates TLS on 127.0.0.1:3088 with that operator-supplied certificate.
Clients must trust the certificate; this pass does not generate a private
CA. After confirmation, setup issues one credential, prints the raw secret
once, and never writes it to the profile. --print issues nothing.

Confirmation writes an enabled profile entry. Setup never guesses, kills, or
restarts your supervisor. Restart the DSH Web process you already own.

Use --print to preview without writing. In a TTY, --print may still prompt
for a provider and missing values, but it never writes a profile, provider
resource, or credential. Non-interactive --yes requires every
security-sensitive value to be supplied explicitly. --yes skips only the
final write confirmation; it does not invent a provider or Cloudflare values.

Supported providers

Provider Auth mode What identity it proves What setup does
Tailscale Serve trusted-header — Serve injects a login header Exact Tailscale-User-Login injected by Serve after it overwrites a caller-supplied value. Not “anyone on the tailnet”. Can create one missing private Serve route for you (routeManagement: ensure), or only check that the route already exists (verify-only).
Cloudflare Tunnel with Access signed-jwt — locally validates an Access identity token A locally validated Access identity JWT (Cf-Access-Jwt-Assertion, RS256, issuer, audience, email, non-empty sub). Not a convenience email header, not a service token, not “the hostname is private”. You configure the Access application yourself and point it only at the gateway. Setup verifies local JWT settings (routeManagement: verify-only); it cannot independently prove Access stays attached to the tunnel.
Headscale via Tailscale TCP Serve gateway-credential — possession of a gateway secret Possession of a distinct high-entropy gateway credential issued per operator. TCP Serve supplies private reachability only; it has no HTTP identity header and does not prove who you are. Can create one missing private TCP Serve forward to 127.0.0.1:3088 (ensure), or only check that it exists (verify-only). You supply the TLS certificate and key. Setup on Tailscale.com steers you to identity-aware Tailscale Serve instead.
EasyTier gateway-credential — possession of a gateway secret Possession of a distinct high-entropy gateway credential. EasyTier is transport only. Not shipped.

Private reachability is not authorization. A tailnet member, a Cloudflare
hostname that is internet-routable, or a mesh peer can reach an endpoint and
still receive 403 unless the gateway allowlist matches.

Cloudflare nuance: Access-gated applications are often reachable from the
Internet. Packets can arrive unauthenticated. The supported product shape is an
identity-gated application plus mandatory local JWT validation, never an
anonymous public tunnel. Local token validation is solid. The gateway cannot
machine-prove that Access remains attached to the tunnel without broad account
credentials; setup says so, and it still refuses a missing or invalid JWT.

What this plugin does not do

  • Make DSH itself multi-tenant, or reduce the privileges of an allowlisted user
    (every allowlisted principal is a full DSH administrator).
  • Treat device, node, or mesh membership as human identity.
  • Expose a configurable generic reverse proxy or an arbitrary trusted-header
    name.
  • Support public anonymous tunnels, Funnel, or Cloudflare quick tunnels.
  • Manage provider-wide ACLs, DNS zones, or account policies.
  • Auto-remove persistent provider routes on uninstall.
  • Accept user-chosen passwords.
  • Run more than one ingress provider in one gateway instance.
  • Protect you from a malicious same-host administrator or any process that can
    already read DSH memory/configuration or connect directly to DSH loopback.

What each auth mode proves

These auth.mode values are the literal YAML keys. Each one is paired with a
fixed provider; you cannot mix them.

  • trusted-header (Tailscale only). Serve injected exactly one
    Tailscale-User-Login and the value is on the allowlist as
    login:<exact-login>. The header name is fixed in code. You cannot configure
    a generic header.
  • signed-jwt (Cloudflare Access only). The request carried exactly one
    Cf-Access-Jwt-Assertion that verifies against the team JWKS, with the
    configured issuer and application audience, required exp/iat/nbf,
    identity type, scalar email, and non-empty sub. The allowlist uses
    email:<exact-email>. The CF_Authorization cookie is never trusted.
  • gateway-credential (Headscale TCP Serve).
    Possession of a distinct ≥256-bit credential issued per operator (CLI-
    generated, not a user-chosen password), submitted in a POST body from the
    JSON API or a same-origin login form — never a URL query parameter — and
    exchanged for a short-lived __Host- session cookie (HttpOnly, Secure,
    SameSite=Strict). The gateway stores only a verifier hash; sessions are
    individually revocable and attempts are rate-limited without permanent
    lockout. TCP Serve does not contribute identity: being able to reach the
    node is not authorization. Tailscale Serve and Cloudflare Access cannot
    select this mode.

After setup

dsh-gateway doctor
dsh-gateway credential issue --store /path/to/dsh-one-gateway/credentials.json --name operator-1
dsh-gateway credential list --store /path/to/dsh-one-gateway/credentials.json
dsh-gateway credential revoke --store /path/to/dsh-one-gateway/credentials.json --name operator-1

Disable by setting enabled: false on the generated profile entry and
restarting DSH. Uninstall does not remove Tailscale Serve routes, Cloudflare
tunnels, Access applications, or credential files. Remove those yourself.

Threat model and local-host trust boundary

The gateway defends against spoofed identity headers, public-mode provider
configuration, Host/Origin/request-target smuggling, provider tokens leaking
into DSH, stale JWT keys, and config typos that would broaden exposure. See
SECURITY.md.

It does not defend against a process on the same host that can connect to
127.0.0.1:3080 or 127.0.0.1:3088, read the DSH profile, or act as a local
root. Loopback TCP cannot prove which local executable opened it. Same-host
compromise is out of scope.

TLS, keys, and credentials

  • Profile YAML never contains private keys, JWTs, or issued credential secrets.
  • Cloudflare signing keys are fetched from the team origin JWKS path with
    bounded HTTPS; they are not written to the profile.
  • Headscale TCP Serve requires an operator-supplied certificate and private
    key (absolute paths, restrictive key permissions, matching pair, unexpired,
    SAN covering externalOrigin). The gateway does not generate a CA or
    self-signed certificate. Clients must enroll trust for that certificate.
  • Gateway credentials (when used) store only a verifier at an operator-supplied
    absolute path with restrictive permissions. The raw secret is shown once.
  • Backup the credential store as you would any other secret file; revocation is
    per principal. Sessions are in-memory and drop when the gateway process
    restarts.

Troubleshooting

Do not disable auth, Origin checks, TLS, or provider verification to “just
get it working”.

Symptom What to check
Gateway never becomes ready dsh-gateway doctor; Tailscale Serve conflict/Funnel; TCP Serve conflict/Funnel; TLS cert/key; Cloudflare JWKS fetch; missing allowlist
403 for an expected user Exact, case-sensitive principal (login: / email:); duplicate identity headers; missing Origin on POST/API/WebSocket
Setup refuses to write Existing dsh-gateway or legacy dsh-tailscale-gateway entry; non-list YAML; missing --yes values
Cloudflare still 403 with Access Identity token missing/expired; wrong audience; service token (no email); Access not attached (probe may report unprotected)

Not supported yet

These may map onto the same contracts later. “It is a VPN” is not enough.

  • EasyTier / ZeroTier / WireGuard-only — no application-level identity
    (the mapping is gateway-credential). All three stay out today for one
    shared reason: this codebase cannot yet prove a listener is bound exclusively
    to the private overlay interface, not merely that it reports the right local
    address. Linux and macOS have SO_BINDTODEVICE / IP_BOUND_IF for that;
    Node's net.Server.listen() exposes neither. That is a specific engineering
    gap, not a claim that these transports cannot work. There is no shipped
    adapter for them, and no committed schedule.
  • Headscale HTTPS Serve — still blocked. Headscale does not provide
    Tailscale's identity-aware HTTPS Serve. The shipped Headscale path is raw
    TCP Serve plus gateway-credential and an operator-supplied certificate,
    not a fabricated identity header.
  • NetBird — claimed identity headers are unsupported until a cited overwrite
    profile and integration test exist.
  • Twingate / Pangolin — no frozen JWT/header validation profile.
  • Generic reverse proxy / arbitrary trusted-header — too easy to configure
    with a spoofable header.
  • Raw LAN, SSH tunnel, public tunnel — outside the private-ingress contract.

The older dsh-tailscale-gateway package remains a Tailscale-only reference
product. The two gateway processes cannot bind the same fixed gateway port at
once. Setup detects a legacy profile entry and refuses to append another.

Configuration

Only the exact fields shown below are accepted. Unknown keys are errors. There
are no listenHost, listenPort, upstream, headerName, jwksUrl,
allowAnonymous, trustPrivateNetwork, public, or funnel keys.

Tailscale — trusted-header means Serve injects the login; routeManagement: ensure means setup will create one missing private Serve route:

enabled: true
externalOrigin: 'https://gateway.example-tailnet.ts.net:8443'
provider:
  type: tailscale-serve
  routeManagement: ensure
auth:
  mode: trusted-header
  trustedPrincipals:
    - 'login:operator@example.invalid'

Headscale TCP Serve — gateway-credential means possession of a generated
secret; TCP Serve is private reachability only. tls is required:

enabled: true
externalOrigin: 'https://gateway.example.invalid:8443'
provider:
  type: headscale-tcp-serve
  routeManagement: ensure
tls:
  certPath: '/path/to/dsh-one-gateway/cert.pem'
  keyPath: '/path/to/dsh-one-gateway/key.pem'
auth:
  mode: gateway-credential
  trustedPrincipals:
    - 'credential:operator-1'
  credentialStorePath: '/path/to/dsh-one-gateway/credentials.json'

Cloudflare — signed-jwt means the gateway locally validates the Access
identity JWT; routeManagement: verify-only means you attach Access yourself:

enabled: true
externalOrigin: 'https://dsh.example.invalid'
provider:
  type: cloudflare-access
  routeManagement: verify-only
  teamOrigin: 'https://team.example.invalid'
  applicationAudience: 'replace-with-access-application-audience'
auth:
  mode: signed-jwt
  trustedPrincipals:
    - 'email:operator@example.invalid'

License

MIT. See LICENSE.

安装

🧩 让 Agent 自动装(推荐)

装一次目录插件,之后本站所有插件都能让 DeepSeek Harness 自动找、自动装:

dsh plugin add dshbase-catalog

然后对 agent 说「帮我装 dsh-one-gateway」,它会在目录里找到并自动安装。文档:dshbase-catalog · 已验证场景包。

该插件是 GitHub 源码(未发 npm)——直接从仓库装:

Web profile:

dsh plugin --profile web add github:TiantianFlow/dsh-one-gateway

Headless(CLI)profile:

dsh plugin --profile headless add github:TiantianFlow/dsh-one-gateway

实测报告

验证通过:从 GitHub 源码完成 L1 安装 + L2 加载 + L3 运行(dsh 0.1.0-rc.6)。

安全:尚未扫描——我们的每日静态扫描将很快覆盖它。

分享徽章

Security 里更多

浏览全部 7819 个插件 →