DeepSeek Harness plugin

dsh-auth-gate

DSH Web UI 的认证门禁插件,提供 SVG 图形验证码与防暴力破解保护

Jump to install

Source facts

Repository
jiang539/dsh-auth-gate
Latest update
Aug 15, 2026
Category
Security & Permissions
GitHub stars
0
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/jiang539/dsh-auth-gate
Plugin: dsh-auth-gate
Author: jiang539

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.en.mdSource · read only
README language

dsh-auth-gate

A security authentication plugin for DeepSeek Harness (DSH). It puts a login gate in front of the DSH Web UI and provides an /auth API that Nginx auth_request can enforce, giving LAN or public deployments authentication and brute-force protection without modifying DSH itself.

> Implemented against the real DSH plugin API (ctx.webServer.register, the Cordis slot system, and the dsh.client bundling contract).

How It Works

Internet user → Nginx (HTTPS + rate limiting)
              → auth_request (Nginx-layer auth check, sub-request to /auth/verify)
              → DSH Web UI (plugin login gate + login page)

Inside DSH: the dsh-auth-gate plugin registers /auth/* routes
           - GET  /auth/captcha  → graphical captcha { svg, uuid }   (one-time)
           - POST /auth/login    → validates captcha + account + password → { token }
           - GET  /auth/verify   → validates Token (for Nginx auth_request)
           - POST /auth/logout   → destroys the session
           - POST /auth/password → change own password (requires login, validates old password)

Two mutually independent enforcement layers:

LayerMechanismNotes
Nginx layerauth_request /_auth → sub-request GET /auth/verifyRequests without a valid Token are rejected with 401 before ever reaching DSH
Plugin layerClient login gate + server-side sessionToken is checked when the browser opens the page; the whole UI is covered by the login page when not logged in, and the server issues no session

Features

  • SVG graphical captcha — one-time use, expires automatically, excludes confusable characters such as 0o1i
  • Brute-force protection — consecutive failures from the same IP (captcha errors and password errors both count) within the blockDuration window lock the IP for blockDuration seconds once maxLoginAttempts is reached; Nginx rate limiting acts as a backstop
  • Session management — server-side in-memory Token storage with sliding expiration (each /auth/verify call extends the session by sessionTimeout)
  • Single-session login — one account is only ever logged in at one place at a time (singleSessionPerUser, on by default): logging in elsewhere immediately invalidates every previous session of that account (the old Token stops validating — 401 — on its next check, i.e. the earlier login is kicked), and the new login's response carries a kickedPrevious flag
  • Password security — bcrypt hashes stored as username:bcrypt_hash (mode 0600), plaintext is never stored
  • Dual-end integration — the Host side registers the /auth/* routes; the Client side registers the login page through DSH's official Slot mechanism (root slot priority -1 overrides the layout, automatically released after a successful login)
  • Trusted proxy — supports reading the real client IP from X-Forwarded-For; the header is only trusted when the direct peer is a loopback address (same-machine Nginx), and the last entry appended by the proxy is used, so forged prefixes from the client cannot bypass the lockout

Installation

> Out of the box (default credentials): if the password file ~/.dsh/auth.passwd contains > no accounts at startup, the plugin automatically creates the initial account admin with > the default password admin123 and prints it once in the DSH startup log. The default > credentials are a public value: the first login is forced to rename the account and change > the password before the UI releases, and the name admin is then reserved (no account can > rename to it; the old name stops working the moment it is renamed away). > To disable this, set autoProvisionAdmin: false in the config (see below).

# 1. Add the plugin to a profile (installed as a profile dependency)
dsh plugin --profile web add dsh-auth-gate

# 2. (Optional, recommended) Create a password file with bcrypt hashes (one user per line)
#    — skip this and the default account above is used instead
mkdir -p ~/.dsh
npx dsh-auth-passwd set admin            # interactive password entry, mode 0600
#   or generate manually (⚠️ the plaintext appears in shell history and process lists — one-time use only):
node -e "console.log(require('bcryptjs').hashSync('YOUR_PASSWORD', 10))" > ~/.dsh/auth.passwd

# 3. Restart DSH
dsh web

When DSH starts, the plugin's cordis.patch.yml writes the dsh-auth-gate entry into the profile, and the client part is loaded automatically by the web client registry (declared via dsh.client). Open the Web UI to see the login gate.

> Local development install: dsh plugin --profile web add ./path/to/dsh-auth-gate.

Verifying It Works

# Get a captcha
curl http://127.0.0.1:3080/auth/captcha

# Log in (replace the captcha answer and uuid with the values from the previous step)
curl -X POST http://127.0.0.1:3080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"your-password","captcha":"abcd","uuid":"<uuid>"}'

# Validate a Token (this is what Nginx auth_request calls)
curl -H "Authorization: Bearer <token>" http://127.0.0.1:3080/auth/verify   # → 200 + X-Auth-User

# Brute-force protection: 5 consecutive failures → 429, with blockedUntil returned

Configuration

All options are set in the plugin entry's config (modifiable in the profile's cordis.patch.yml or in a --patch overlay):

KeyDefaultNotes
passwordFile~/.dsh/auth.passwdPassword file path (~ expands to the OS user's home directory)
sessionTimeout3600Session lifetime (seconds), renewed on a sliding window
captchaExpires300Captcha validity (seconds)
maxLoginAttempts5How many consecutive failures from the same IP before lockout
blockDuration300Lockout duration (seconds)
singleSessionPerUsertrueSingle-session login: a new login for the same account immediately invalidates all of its previous sessions (the old Token fails the next check with 401 — the earlier login is kicked); set to false to allow concurrent logins
trustProxyfalseWhether to trust X-Forwarded-For (only effective when Nginx and DSH are on the same machine and the direct peer is a loopback address; takes the last entry appended by the proxy)
devCaptchaTextfalseDevelopment only — echoes the captcha answer in /auth/captcha for curl debugging; never enable in production
defaultAdminUseradminUsername of the initial account (auto-created only when the password file is empty and autoProvisionAdmin is on). This name is reserved: a login under it forces a rename, and no account may rename to it
defaultPasswordadmin123Default password of the initial account (public value; first login is forced to change it; at least 8 characters)
autoProvisionAdmintrueWhen the password file holds no accounts at startup, whether to create the initial account and print its default password in the log

Profile override example:

# ~/.dsh/profiles/web/cordis.patch.yml
- id: dsh-auth-gate
  config:
    sessionTimeout: 7200
    maxLoginAttempts: 10
    blockDuration: 600

Except for passwordFile and devCaptchaText, every option (sessionTimeout, captchaExpires, maxLoginAttempts, blockDuration, trustProxy, singleSessionPerUser) can also be changed online after login under Settings → Personal config → Security settings: changes apply to the running process immediately and are persisted to auth-gate.security.json next to the password file.

Password File

Format: one username:bcrypt_hash per line (lines starting with # are comments), mode 0600.

npx dsh-auth-passwd hash            # print a hash for manual use
npx dsh-auth-passwd set <user>      # add/update a user (interactive password entry)
npx dsh-auth-passwd list            # list users
npx dsh-auth-passwd delete <user>   # delete a user

> Initial-account auto-provisioning: with autoProvisionAdmin on (default) and no accounts > in the password file, startup creates defaultAdminUser (default admin) with a bcrypt hash of > defaultPassword (default admin123), mode 0600, and prints the default password once in the > startup log; the first login is forced to rename the account and change the password > (after the rename the old name admin stops working and no account may rename to it). > Password files that already contain accounts are never modified.

Changing Your Password After Login

A logged-in user can change the password or log out directly from the floating control in the bottom-right corner of the Web UI (mounted via the official shell.overlay slot, shown only when logged in and the real interface is rendered). The UI is usable without a page refresh after login; logging out calls /auth/logout to destroy the server-side session and clears the local Token.

The change-password dialog asks for the old and new passwords.

Server-side enforced rules:

  • Must be logged in (Authorization: Bearer <token>); an expired session returns 401.
  • The old password must match the stored bcrypt hash (otherwise 403; failures count toward the same per-IP lockout as login).
  • The new password must be at least 8 characters and at most 72 bytes (the bcrypt limit).
  • After a successful change, all other login sessions of that user are invalidated; the current session stays valid.

The password file is rewritten atomically (temp file + rename, mode 0600), and the read–verify–write cycle runs in a single serial queue; comments and other users' entries are preserved. Admins can still reset any user's password directly on the server:

npx dsh-auth-passwd set <user>   # overwrite any user's password

Nginx Reverse Proxy (Public / LAN Deployment)

DSH deliberately listens only on 127.0.0.1. Put Nginx from the same machine in front of it (auth_request + rate limiting) to expose it safely — public HTTPS: [docs/nginx.conf.example](./docs/nginx.conf.example), LAN HTTP: [docs/nginx.conf.lan.example](./docs/nginx.conf.lan.example).

nginx -V 2>&1 | grep -- 'http_auth_request_module'   # check that the module is available
sudo nginx -t && sudo systemctl restart nginx

Key points:

  • location /auth_request /_auth; the sub-request forwards the browser's Authorization header to /auth/verify.

2xx allows, 401/403 rejects.

  • /auth/login and /auth/captcha pass through, but are rate-limited per IP.
  • The limit_req zone provides coarse-grained transport-level rate limiting; the plugin's

failure-counter store provides fine-grained account lockout.

  • HTTPS with Let's Encrypt: apt install certbot python3-certbot-nginx && certbot --nginx -d your-domain.com.

LAN HTTP (Direct IP Access)

When HTTPS is not needed on the LAN, access directly via IP on port 80. Swap the listen 443 ssl in the public config for listen 80 and drop the ssl_* directives — the auth_request auth flow is independent of transport-layer encryption and behaves identically. Full example: [docs/nginx.conf.lan.example](./docs/nginx.conf.lan.example):

# Just change the server block to:
listen 80;
# leave server_name empty or use the machine's IP, e.g. server_name 192.168.1.10;

> ⚠️ HTTP is plaintext: account passwords and session Tokens can be sniffed on the LAN. > Only recommended on trusted internal networks; any public-facing deployment should use the HTTPS config above.

Security Notes

  • The default credentials are public: the out-of-the-box admin / admin123 is printed in

the startup log and documented — anyone who sees it can log in before it is changed. The forced first-login rename + password change only shortens that window: complete both immediately after deployment (the name admin is then reserved), or set autoProvisionAdmin: false and create your own accounts with dsh-auth-passwd set. Note that the CLI is a server-admin tool: it can still create a user named admin — what the Web UI forbids, a server admin can always do.

  • Plugin trust: third-party DSH plugins can rewrite the entire config tree at startup

(this gate uses exactly that to start itself). Only install plugins from trusted sources, pin versions, and review their cordis.patch.yml.

  • In-memory storage: sessions, captchas, and failure counters live in memory.

Restarting the process logs out all users. For multi-instance deployments, swap these Maps for shared storage (e.g. Redis) — handlers are isolated behind small functions and easy to replace.

  • Nginx is the authoritative enforcement point: the plugin's login gate only hides the UI;

for public deployments the authoritative check on /api traffic is Nginx's auth_request layer. Without Nginx in front, DSH only serves on the loopback address.

  • Password file: keep it at ~/.dsh/auth.passwd, mode 0600; rotate hashes with

dsh-auth-passwd set (bcrypt cost = 10).

Development

npm install
npm run build          # tsc builds host → lib/host, esbuild builds client → lib/client.js
npm run typecheck
npm test              # builds host, then runs integration tests (captcha/lockout/password change/trusted proxy/concurrent writes)

Directory layout:

dsh-auth-gate/
├── package.json            # dsh.bundle.patch + dsh.client(platform: web)
├── cordis.patch.yml        # inserts the host entry into the profile
├── src/
│   ├── host/index.ts       # /auth/* services (captcha, login, verify, logout)
│   ├── client/index.tsx    # login gate (root slot, priority -1)
│   ├── client/login.css    # login gate styles
│   └── shared/types.ts     # online types shared by both ends
├── bin/dsh-auth-passwd.mjs # password file CLI
├── scripts/build-client.mjs# wraps the esbuild output into __ModuleLoader__.load()
└── lib/                    # build output
    ├── host/               # host ESM (tsc)
    └── client.js           # client bundle (esbuild, CJS-in-loader wrapper)

The client bundle is served by DSH's own module system at /plugins/dsh-auth-gate/client.js and injects window.__DSH_BOOT__ automatically — no extra wiring needed.