TecFancy/dsh-auth-gate

TecFancy/dsh-auth-gate

DSH 网页端登录门插件:账号口令或共享令牌认证、会话 cookie、登录限速,附用户管理 CLI(0.4.1 起声明 dsh.bundle manifest,`dsh plugin add` 一键挂载)。

安全与权限authenticationdeepseek-harnessdsh

dsh-auth-gate

English | 简体中文

A login door for your DeepSeek Harness (dsh) web instance. Put it in front of a public dsh deployment and nobody can reach your agents, your chat sessions, or your LLM credentials without signing in first.

Maintained until dsh ships authentication natively. dsh has no built-in login
yet, and this repository is how we close that gap: new dsh releases are tracked
(mount-point changes, supported version ranges, Linux/Windows CI), regressions are
fixed, and releases keep shipping. When official authentication lands we will publish
the migration path and keep supporting the dsh versions still in use, so nobody is left
on an abandoned fork.

Contents

  • What it does
  • What it does not do
  • The market's credentials badge
  • Quick start
  • See it in action
  • Configuration
  • Command-line tool
  • Admin tools: list users and reset a password
  • Bundled configuration skill
  • Troubleshooting
  • Deployment
  • Authenticated local proxy (optional)
  • Requirements
  • Notes & limitations
  • Development
  • License

What it does

  • Everything needs a login. Every page, API call, and WebSocket connection

is checked. Visitors without a valid session are sent to a simple login page (or rejected with 401 for API/script requests). The one exception is GET /manifest.webmanifest: browsers fetch the Web App Manifest without credentials, so that exact path is public (name / icons / display mode only).

  • Two ways to sign in (pick one in the configuration):
  • Password (recommended): each admin gets a username and password.
  • Token: one shared secret token for the whole instance.
  • Works for browsers and scripts. Browsers use the login page; scripts and

curl can pass Authorization: Bearer and skip the page entirely.

  • Admin tools. In password mode an administrator can list users and reset another user's

password, either from the Account security section of the Settings panel or over HTTP (GET /auth/users, POST /auth/users/password); a reset revokes that user's sessions and forces a new password at their next sign-in before they can reach anything else. Only an administrator on a full session sees the panel block, and the admin plane refuses to reset your own account (use the self-service form above it, or the CLI when you have forgotten your password).

  • Optional two-factor authentication (TOTP). In password mode, a user with

a TOTP secret added to their account signs in with password plus a 6-digit code from an authenticator app (RFC 6238, configurable off/optional/required).

  • Safe by default. Passwords are stored hashed, logins are rate-limited

(repeated wrong attempts temporarily lock the address), session cookies are secure, and any missing or broken configuration blocks access instead of silently opening the door. A wrong username or password makes the login card re-render with an inline Invalid username or password.: the username is kept, the password must be retyped. A lockout (HTTP 429 + retry-after) shows the same card with the retry seconds and a message saying the lockout applies to "this network". The number of remaining attempts is deliberately never shown, and with JavaScript a page refresh no longer consumes another failed attempt.

What it does not do

Honest boundaries, so you can gauge the risk before installing (the full list with mechanisms is in docs/deployed/known-limitations.md):

  • Not server-level security. Keep the OS user locked down and the config files

private (auth/users.yaml and .credentials.yaml are created 0600); this plugin guards dsh's web surface only.

  • Not every way of changing a password evicts sessions. dsh-auth user disable

blocks future logins and revokes the sessions that user was issued (within revokeSweepMs, 5 s by default); the Settings panel's self-service change revokes every session of that user, and so does an administrator's reset of someone else's password; the CLI's dsh-auth user passwd only rewrites the stored hash.

  • Not a replacement for HTTPS. With cookieSecure: true you must serve the site

over https.

  • Not a full identity provider. No OAuth/OIDC, no self-registration, no e-mail

reset; users are created and managed by an administrator through the CLI.

The market's credentials badge

Plugin-market listings scan the artifact you would install and print what it touches. For this plugin the scan reports credentials ("uses your API keys"), network, fs-read, fs-write and env, plus one red line: _reads credentials/secrets AND has network access_. That is a disclosure, not a verdict, and for this plugin it is accurate. Here is what it corresponds to in the code.

  • One credential read, in token mode only. apply() builds the token resolver only

when mode: "token" (src/index.ts); password mode never touches the credentials service. Token mode resolves the shared token with credentials.resolve(config.tokenRef) in src/token-resolver.ts, and tokenRef (default DSH_AUTH_TOKEN) is the only reference the plugin ever asks for. Nothing lists, enumerates, or reads any other credential.

  • The credential service belongs to the harness, and it resolves in this order:

$DSH_HOME/.credentials.yaml (created 0600), the process environment, then a .env file. When the service is missing or the reference does not resolve, the gate denies every request instead of letting traffic through.

  • The network capability is the plugin's own traffic: the browser calling the

plugin's endpoints (sign-in, user management, password reset) and the optional authenticated local proxy. Nothing is sent anywhere else.

  • What a static scan cannot say. It can prove a capability is present; it cannot

prove that one is absent, and it does not read intent.

If you would rather run an instance that never reads a credential, use password mode.

Quick start

# 1. Install the plugin from npm into your dsh profile.
#    Since 0.4.1 the package declares a `dsh.bundle` manifest, so `dsh plugin add`
#    also registers the mount (dsh.profile.bundles) automatically:
dsh plugin --profile web add dsh-auth-gate

# 2. Create an admin account.
#    `dsh plugin add` installs the plugin into the profile's node_modules
#    ($DSH_HOME/profiles/web, default ~/.dsh/...) — the CLI is NOT added to your
#    PATH, so call it through the profile. `dsh plugin` already requires pnpm:
printf '%s\n' 'choose-a-strong-password' | \
  pnpm --dir "$DSH_HOME/profiles/web" exec dsh-auth user add admin --password-stdin

# 3. Turn on password login: override the plugin config in $DSH_HOME/cordis.patch.yml
#    (a ready-to-use config-override template ships in deploy/cordis.patch.yml;
#    see Configuration below — the mount itself needs no manual patch row)

# 4. Restart dsh. Open your site — you will be asked to sign in.

See it in action

Every screenshot below uses the English UI, and both READMEs share the same set of images (the plugin's own panels follow the GUI language; the server-rendered pages are English in every locale).

Visitors without a session are sent to the login page (the card is rendered by the plugin server-side in English, so this shot is the same in every locale):

When TOTP is enabled for your account, signing in continues with a second step — a 6-digit code from your authenticator app (password first, then the code):

After signing in, they land on your instance:

On dsh 0.1.2-alpha+ (which guards pages with a launch token), signing in auto-bridges the token gate: the login redirect takes a short relative /?token=… hop that sets the dsh cookie, then lands on / (details in docs/implemented/impl-launch-token-bridge.md).

A prominent Sign out / 退出登录 button sits inside the Settings panel (the Settings → General page, below the last preference row). It's a centered, danger-styled filled button (16px door icon + localized label, theme tokens for light/dark), and its label follows the GUI language through the same locale mechanism the Settings language switch uses. Clicking it runs the same native POST /auth/logout?next=/ flow as before.

A signed-in user can also change their own password from the Account security section of the Settings panel (password mode only): current password, the new password typed twice, and a TOTP code whenever the account has a secret. The panel title carries the plugin's own mark (a shield + keyhole outlined at 16px).

The mark next to the section name in the nav is the plugin's too: the host has no per-section icon option yet (settings.section carries only id/order/label, and nav icons come from the host's hard-coded navIcon(id), which falls back to the default gear for third-party sections), so a temporary DOM stopgap replaces our own row and nothing else (if it cannot find the row it silently falls back to the gear). Once dsh offers an icon option, the stopgap and its code are removed (see ADR D24.1 for the migration conditions).

The panel posts to POST /auth/password (current / password / code, form-urlencoded) and, on success, every session of that user is revoked, including the one making the change. The client immediately sends the device back to the login page with a reason, and the card explains why (the success panel only stays on screen when the environment refuses to navigate; its "Sign in again" button is then the fallback way out):

Passwords must be at least 14 characters, contain four character classes and differ from the current one. With TOTP on, a code already spent in the current 30-second window is rejected as a replay: wait for the next code.

An administrator on a full session also gets a user management block below that form (the panel screenshot above shows it): a read-only list (name, role, a mutually exclusive disabled / must-change-password / normal badge and a two-factor badge, with "You" on their own row) plus a reset form for another user. A reset forces the target to pick a new password at the next sign-in and revokes every session that user had, while the acting administrator's own session is untouched. When the administrator's own account has TOTP enabled, the reset asks for a current code as well. The block renders only for role === "admin" on a full (not forced-change) session, and a non-admin client never requests the user list at all. Resetting your own account is not possible: the server refuses it and the dropdown excludes you, so an administrator who has forgotten their own password must reset it on the server with dsh-auth user passwd .

Configuration

The bundle mount (id dsh-auth-gate, inserted by dsh plugin add) uses the default config: mode: "token" backed by the DSH_AUTH_TOKEN environment variable. To change it, override the config in $DSH_HOME/cordis.patch.yml (or the profile's cordis.patch.yml — a ready-to-use override template ships in deploy/cordis.patch.yml). The override targets the mounted row by id (no insert — adding one would double-mount the plugin):

- id: dsh-auth-gate
  config:
    mode: "password" # "password" (recommended) or "token"
    totp: "optional" # "off" (default), "optional", or "required"
    cookieSecure: true # keep true when you use https

Option · Default · What it does mode · "token" · "password" = username/password login; "token" = one shared secret totp · "off" · Password mode only. "optional": users with a TOTP secret sign in with password + code; "required": all users must have a secret (users without one get the uniform 401 at the password stage, same body as a wrong password — anti-enumeration) sessionTtl · 604800 · How long a login lasts (seconds) before you must sign in again cookieName · dsh_auth · Name of the session cookie (rarely needs changing) tokenRef · "DSH_AUTH_TOKEN" · Token mode only: which environment variable holds the shared secret cookieSecure · true · Set to false only if you are testing over plain http usersFile · "" · Password mode: where your user list lives. Defaults to $DSH_HOME/auth/users.yaml publicHost · "" · Host rendered in the login page identity block (the anti-phishing "which instance is this" line). Empty = use the request Host header. Set it when a reverse proxy rewrites Host (e.g. Caddy header_up Host 127.0.0.1:3080), otherwise the card shows a loopback address instead of your public domain. With D25 it also lets the admin and password-change endpoints validate an Origin header: with publicHost empty they accept only Sec-Fetch-Site: same-origin, so production deployments should set it, and write it with the scheme (https://host) when TLS terminates at a reverse proxy revokeSweepMs · 5000 · Password mode: how fast (ms) a user disabled with dsh-auth user disable loses already issued sessions. 0 = never sweep (disabling only blocks new logins) clientIpHeader · "" · Header carrying the real client address for rate limiting (x-forwarded-for, or cf-connecting-ip behind Cloudflare). Empty = read no header at all. Only read when the request's peer is inside trustedProxyCidrs; without it a same-host reverse proxy makes every client share one lockout bucket (issue #74) trustedProxyCidrs · ["127.0.0.0/8", "::1/128"] · Which peers may supply clientIpHeader (default: loopback only; a peer without an address, i.e. a Unix socket, counts as local). Invalid entries are dropped with an error log and trust narrows to loopback, an explicit [] means "trust nobody", and 0.0.0.0/0 / ::/0 are always rejected logoutOrder · 1000 · Slot order of the "Sign out" button in Settings → General (higher = lower on the page). Raise it if another plugin registers a bigger order

To enable TOTP for a user, run dsh-auth user totp enable and add the printed secret (or scan the otpauth:// URI) into an authenticator app (Google Authenticator, 1Password, etc.). The code changes every 30 seconds; a code from the previous or next window is also accepted (drift tolerance).

Command-line tool

dsh-auth manages users from the shell:

dsh-auth user add admin --password-stdin   # add a user (--admin creates an administrator)
dsh-auth user list                          # list users
dsh-auth user passwd admin                  # change a password (read twice; there is no --password flag)
dsh-auth user role admin user               # grant/revoke the admin role: user role <name> <admin|user>
dsh-auth user disable admin                 # block future logins + revoke that user's live sessions
dsh-auth user enable admin                  # re-enable a disabled user (a reset alone does not let them in)
dsh-auth user totp enable admin             # generate a TOTP secret (prints an otpauth:// URI)
dsh-auth user totp disable admin            # remove the TOTP secret

dsh-auth is directly on your PATH when the package is installed globally. After dsh plugin add the binary lives inside the profile and must be called through it - see Quick start.

dsh-auth user passwd only rewrites the stored hash: it does not revoke that user's live sessions. Use the Settings panel change when sessions must be evicted right away; dsh-auth user disable also blocks future logins, but the sessions already issued are dropped by the periodic sweep (revokeSweepMs, ~5 s by default).

Admin tools: list users and