dsh-auth-gate
August 30, 2026 · View on GitHub
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.
Built on dsh-plugin-framework
This plugin is developed on the engineering conventions of
dsh-plugin-framework, the
reference plugin framework for the dsh ecosystem. The src/ layout
(features/shared layers with barrel-only cross-slice imports), the engineering
gates (npm run verify, bundle/slice/no-emdash checks) and the decision-record
discipline all align with it - conventions that have held up across the dsh
codebase. Solid engineering worth building on.
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
401for API/script requests). -
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 <token>and skip the page entirely. -
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 small command-line tool for managing users:
dsh-auth user add admin --password-stdin # add a user dsh-auth user list # list users dsh-auth user disable admin # block a user's future logins dsh-auth user totp enable admin # generate a TOTP secret (prints an otpauth:// URI) dsh-auth user totp disable admin # remove the TOTP secretdsh-authis directly on your PATH when the package is installed globally. Afterdsh plugin addthe binary lives inside the profile and must be called through it — see Quick start.
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
Visitors without a session are sent to the login page:

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:

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.
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 (no code = no login) |
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 |
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 <name> 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).
Bundled configuration skill
The package ships a configuration quick-reference skill at
.agents/skills/dsh-auth-gate-config/ (this page). Install it into the
user-level dsh skill root so agents on the deployment side can answer
"what configuration does auth-gate support?" directly:
pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/<profile>" exec dsh-auth skill install [--force]
It copies the skill to $DSH_HOME/skills/dsh-auth-gate-config/, which
dsh's skill discovery picks up automatically. Re-running without
--force keeps any local edits to the skill; use --force to refresh it
from the package.
The skill is a user-only skill (disable-model-invocation: true in its
frontmatter): it stays out of the model's auto-invocable skill catalog so it
does not sit in every agent turn, and you open it explicitly from the skill
panel whenever you need the config reference (the UI marks it user-only).
If you prefer the agent to answer configuration questions automatically,
remove that frontmatter field after installation.
Troubleshooting
dsh-auth: command not found
dsh plugin --profile web add dsh-auth-gate installs the package into the
profile's node_modules ($DSH_HOME/profiles/web/node_modules/dsh-auth-gate,
default ~/.dsh/...), but nothing is added to your shell's PATH, so the CLI
binary is not callable by name. This only affects the CLI — the plugin itself
runs fine. Pick one:
-
Call it through the profile (recommended).
dsh pluginalready requires pnpm, so the CLI resolves from the same place the plugin lives:pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user add admin --password-stdin pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user listOptionally, once per shell session:
alias dsh-auth='pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth' -
Direct node invocation (no pnpm needed at runtime):
node "$DSH_HOME/profiles/web/node_modules/dsh-auth-gate/lib/cli.js" user add admin --password-stdin -
Install the package globally, then
dsh-authis on your PATH:npm install -g dsh-auth-gate dsh-auth user add admin --password-stdin
Whichever way you call it, the CLI manages the same shared user list
($DSH_HOME/auth/users.yaml, fallback ~/.dsh/auth/users.yaml) that the plugin
reads — the global copy is just a launcher.
Troubleshooting
dsh-auth: command not found
dsh plugin --profile web add dsh-auth-gate installs the package into the
profile's node_modules ($DSH_HOME/profiles/web/node_modules/dsh-auth-gate,
default ~/.dsh/...), but nothing is added to your shell's PATH, so the CLI
binary is not callable by name. This only affects the CLI — the plugin itself
runs fine. Pick one:
-
Call it through the profile (recommended).
dsh pluginalready requires pnpm, so the CLI resolves from the same place the plugin lives:pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user add admin --password-stdin pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user listOptionally, once per shell session:
alias dsh-auth='pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth' -
Direct node invocation (no pnpm needed at runtime):
node "$DSH_HOME/profiles/web/node_modules/dsh-auth-gate/lib/cli.js" user add admin --password-stdin -
Install the package globally, then
dsh-authis on your PATH:npm install -g dsh-auth-gate dsh-auth user add admin --password-stdin
Whichever way you call it, the CLI manages the same shared user list
($DSH_HOME/auth/users.yaml, fallback ~/.dsh/auth/users.yaml) that the plugin
reads — the global copy is just a launcher.
Deployment
- Reverse-proxy deployment guide — Caddy/nginx
setups, the browser-trust fence gotcha (Settings-page
403s behind a proxy, and why auth alone doesn't fix them), and the recommended semi-shell topology. docs/deployment.md— ops checklist, acceptance steps (A–I) and troubleshooting. Chinese version:docs/deployment_zh.md.
Authenticated local proxy (optional, dsh-auth-proxy)
⚠️ Known limitation (unaffected by any auth-gate release): dsh's settings pages ("Settings -> Models", etc.) are editable only when the page origin is loopback (
localhost/127.x). This is a dsh client-side boundary (isLoopback), orthogonal to authentication — on a domain page the settings dialog reports "settings are unavailable in this browser" and providers/credentials cannot be edited; upgrading dsh-auth-gate does not change that. To edit configuration, use this local proxy, or openhttp://127.0.0.1:3080on the server itself. Chatting and model selection on the domain page are unaffected.
After the semi-shell fixed the server-side
/apifence, dsh's client still requires "page origin must be loopback"; the local proxy provides a loopback page entry on the user's machine, composing with auth-gate for "remote config editing with authentication throughout", without touching dsh sources. Full design: docs/local-proxy.md (Chinese: docs/local-proxy_zh.md).
- Zero-dependency Node bin (
dsh-auth-proxy): strictly bound to127.0.0.1, stateless pass-through for pages/API,events.mux/events.hostWebSocket tunneling, and aSet-CookieSecure-attribute adaption (Safari fallback). - Authentication reuses auth-gate (password and token modes): the login page and session cookies pass through untouched.
- Security boundary (deny-list, Phase 2.1): combined with
--mark-proxy, the server-side guard answers403for marked requests hittinghost.pickDirectory/host.openPath/settings.openDocument/llm.discoverModels; unmarked traffic behaves exactly as if the proxy were not deployed.
dsh-auth-proxy --listen 127.0.0.1:8443 --target https://your-domain.example --mark-proxy
# Open http://127.0.0.1:8443 in the browser -> log in -> "Settings -> Models" is editable
systemd example: deploy/systemd/dsh-auth-proxy.service.example.
Requirements
- Node ≥ 22.19 and pnpm on the server.
- The dsh
webprofile running (dsh --profile web). - If
cookieSecureistrue, your site must be served over https (browsers refuse secure cookies on plain http).
License
Notes & limitations
- Disabling a user only stops new logins; already-signed-in sessions stay valid until they expire.
- Login rate limiting resets when the server restarts; so does the TOTP replay guard (a used code in the same 30s window becomes acceptable again after a restart — restart and code-stealing in the same window are both needed to exploit this).
- A TOTP challenge (the "password ok, code pending" state) lasts at most 5 minutes and survives restarts only as the browser cookie's own TTL; after a restart the code page still works if the cookie is fresh, and the code is validated against the user's configured secret at submit time.
- Behind a reverse proxy, rate limiting counts by the proxy's address.
- Sign out from the GUI: a prominent "Sign out / 退出登录" button sits in the
Settings panel (Settings → General, bottom) — client half, requires the
web app's client bundle (dsh 0.1.0-rc.6+); the direct
/auth/logout?next=/URL always works as a fallback. - The plugin only protects dsh's web surface. It is not a replacement for
server-level security: keep the server OS user locked down and the config
files private (
.credentials.yamlandauth/users.yamlare created with0600permissions).