deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
Transport-level authentication for the DeepSeek Harness (DSH) Web GUI.
Official DSH webserver serves the GUI, plugin bundles, /api, SSE, and WebSocket traffic without a login boundary. This plugin disables that unauthenticated carrier and replaces it with a drop-in webServer service that authenticates every request before it reaches application routes.
中文文档:README.zh-CN.md

DSH’s stock web host is convenient for local use, but it is not a product auth layer:
0.0.0.0 or putting the port behind a reverse proxy can expose the full control surface./api, static plugin assets, SSE, or WebSocket upgrades.@summersec/dsh-web-auth sits at the transport layer:
@deepseek-ai/dsh-host-webserver.webserver-auth with the same ctx.webServer contract (register, registerUpgrade, registerFallback, tapIndex, host, port).Other plugins keep registering routes as usual; they do not need to know auth exists.
| Area | Behavior |
|---|---|
| Coverage | HTTP routes and WebSocket / HTTP upgrade paths |
| Default mode | always — login required even on 127.0.0.1 |
| Optional mode | non-loopback — skip auth only when bound to loopback |
| Passwords | scrypt hashes (scrypt$N$r$p$salt$key); plaintext env only for temporary use |
| Sessions | 32-byte random tokens, in-memory store, sliding TTL |
| Cookies | HttpOnly, SameSite=Strict, optional Secure |
| Abuse control | Per-client-IP login attempt limiter with Retry-After |
| Login UX | Built-in /auth/login page (light/dark), form + JSON body |
| Hardening | Origin check on login/logout, open-redirect sanitization, CSP and frame denial on auth responses |
>= 22web profile (peer: @deepseek-ai/cordis ^4.0.1)# 1) Generate a random password + scrypt hash (save the password offline)
npx --yes @summersec/dsh-web-auth generate
# 2) Export the hash for this shell session (do not commit it)
$env:WEB_AUTH_PASSWORD_HASH = 'scrypt$...'
$env:WEB_AUTH_USERNAME = 'admin'
# 3) Install into the web profile
dsh plugin --profile web add @summersec/dsh-web-auth
# 4) Start the GUI
dsh web
Open the usual DSH URL. Unauthenticated browser navigations redirect to /auth/login. API and other non-HTML clients receive 401 JSON:
{ "error": "authentication_required" }
After login you get a session cookie and continue to the original path. The injected browser bootstrap makes same-origin API, SSE, and plugin requests use that session cookie explicitly. If an in-memory session expires or the service restarts, a JSON authentication_required response sends the browser back to the login page instead of leaving the plugin in a silent transport-failure state.
Do not put the password or hash into the project
.envif that file is shared or committed. Prefer the process environment, a secrets manager, or a private host-level env file outside the repo.
git clone https://github.com/SummerSec/dsh-web-auth.git
cd dsh-web-auth
npm install
node .\bin\dsh-web-auth.js generate
$env:WEB_AUTH_PASSWORD_HASH = 'scrypt$...'
# From the parent directory that hosts your DSH workspace, or via local path:
dsh plugin --profile web add <path-to-dsh-web-auth>
dsh web
Hash an existing password (minimum 12 characters):
$env:WEB_AUTH_PASSWORD = 'your-long-passphrase'
node .\bin\dsh-web-auth.js hash-password
Remove-Item Env:WEB_AUTH_PASSWORD
Or pipe stdin (the CLI never accepts the password as a command-line argument):
'your-long-passphrase' | node .\bin\dsh-web-auth.js hash-password
authMode / WEB_AUTH_MODE |
When auth runs |
|---|---|
always (default) |
Always, including host: 127.0.0.1 |
non-loopback |
Only when host is not 127.0.0.1 (e.g. 0.0.0.0) |
# Default: always require login
$env:WEB_AUTH_MODE = 'always'
dsh web
# Loopback without login; enable gate when binding non-loopback
$env:WEB_AUTH_MODE = 'non-loopback'
dsh web --host 0.0.0.0
If authentication is active and neither passwordHash nor password is configured, the plugin throws at startup so you never ship an open server by accident.
The bundle (cordis.patch.yml) wires these into plugin config:
| Variable | Default | Description |
|---|---|---|
WEB_AUTH_MODE |
always |
always or non-loopback |
WEB_AUTH_USERNAME |
admin |
Login username |
WEB_AUTH_PASSWORD_HASH |
(none) | Preferred scrypt hash from generate / hash-password |
WEB_AUTH_PASSWORD |
(none) | Plaintext password for temporary / lab use only |
Prefer WEB_AUTH_PASSWORD_HASH. Keep WEB_AUTH_PASSWORD for short-lived local experiments.
The bundle:
webserver row to disabled: true.webserver-auth with name @summersec/dsh-web-auth.DSH patches replace config as a whole. To override advanced fields, restate the full webserver-auth block in the profile patch (e.g. profile cordis.patch.yml):
- id: webserver-auth
name: '@summersec/dsh-web-auth'
inject: [webStartup]
config:
host: !!js ctx.webStartup.host ?? '127.0.0.1'
port: !!js ctx.webStartup.port ?? 3080
authMode: always
username: admin
passwordHash: !!js process.env.WEB_AUTH_PASSWORD_HASH
sessionTtlMinutes: 720
maxAttempts: 5
attemptWindowSeconds: 300
secureCookie: auto
trustProxy: false
| Field | Type / values | Default | Notes |
|---|---|---|---|
host |
127.0.0.1 | 0.0.0.0 |
127.0.0.1 |
Listen address (from web startup) |
port |
0–65535 |
3080 |
Listen port; 0 for ephemeral |
authMode |
always | non-loopback |
always |
See Authentication modes |
username |
string | admin |
Single shared account |
password |
string | — | Plaintext; avoid in production |
passwordHash |
scrypt$... |
— | Required format from the CLI |
sessionTtlMinutes |
1–43200 |
720 (12h) |
Sliding window on each authenticated request |
maxAttempts |
1–1000 |
5 |
Failed logins per IP per window |
attemptWindowSeconds |
1–86400 |
300 |
Attempt window length |
secureCookie |
auto | always | never |
auto |
When to set the Secure flag |
trustProxy |
boolean | false |
Trust X-Forwarded-* only behind a locked-down proxy |
secureCookie and trustProxy| Scenario | Suggested settings |
|---|---|
| Local HTTP on loopback | secureCookie: auto, trustProxy: false |
| Direct TLS on the Node process | secureCookie: auto (sets Secure when the socket is encrypted) |
| HTTPS terminated at nginx / Caddy / Cloudflare | secureCookie: auto or always, trustProxy: true, and only the proxy may reach DSH’s port |
If trustProxy is true while the port is reachable by untrusted clients, attackers can spoof X-Forwarded-For / X-Forwarded-Proto and weaken IP limits or cookie security. Lock network access first.
Failed logins are limited by client IP. With the default configuration, an IP may fail 5 times within 300 seconds. Further attempts receive 429 Too Many Requests and a Retry-After header until the window expires. A successful login clears that IP's failure count.
Configure the threshold with:
maxAttempts: 5
attemptWindowSeconds: 300
The limiter is intentionally small and local:
trustProxy: false, the socket address is used. With trustProxy: true, the first X-Forwarded-For value is trusted, so the DSH port must only accept traffic from the configured proxy.For an Internet-facing deployment, keep this limiter enabled and add rate limiting at the reverse proxy or firewall. It is not a replacement for HTTPS, network isolation, or a strong password.
| Method | Path | Purpose |
|---|---|---|
GET / HEAD |
/auth/login |
Login HTML page; ?next=/path for post-login redirect |
POST |
/auth/login |
Authenticate (application/x-www-form-urlencoded or application/json) |
POST |
/auth/logout |
Clear session cookie and redirect to login |
GET |
/auth/status |
{ authenticated, required, username? } — 200 or 401 |
{
"username": "admin",
"password": "...",
"next": "/"
}
303 + Set-Cookie (dsh_web_auth) and Location set to a sanitized relative path (blocks //evil, absolute URLs, and header-injection characters).401) or rate-limit page (429 + Retry-After).Origin when the header is present (CSRF-oriented check).401 and a JSON error body.Cache-Control: no-store, a strict CSP, X-Frame-Options: DENY, and related headers.Browser / client
│
▼
┌──────────────────────┐
│ dsh-web-auth │ ← session cookie / login routes
│ (Authenticated │
│ WebServer service) │
└──────────┬───────────┘
│ authenticated only
▼
GUI · plugin bundles · /api · SSE · WS
(registered via ctx.webServer.*)
Compatible surface with the stock web server service:
register({ kind, path, handler })registerUpgrade({ path, handler })registerFallback(handler)tapIndex(transform)host / port gettersPackage binary: dsh-web-auth
dsh-web-auth generate
Print WEB_AUTH_PASSWORD=... and WEB_AUTH_PASSWORD_HASH=...
dsh-web-auth hash-password
Read password from WEB_AUTH_PASSWORD or stdin; print scrypt hash only
The CLI uses Node.js crypto.scryptSync, an RFC 7914 scrypt password-based key derivation function. It is designed to make large-scale password guessing more expensive in both CPU time and memory than a fast general-purpose hash.
For each password, the plugin:
crypto.randomBytes.N=16384, r=8, and p=1.crypto.timingSafeEqual.The password itself is not stored, and the encoded value is not encryption that can be decrypted. Passwords passed to the hashing CLI must contain at least 12 characters.
Stored format:
scrypt$N$r$p$<salt-base64url>$<key-base64url>
Default parameters: N=16384 (CPU/memory cost), r=8 (block size), p=1 (parallelization), a 64-byte derived key, and a 16-byte salt. The Node.js scrypt memory ceiling is set to at least 64 MiB for these operations.
npm run check # syntax check + unit tests
npm pack --dry-run # publish file set
dsh --profile web --dump-config
In the dump, confirm:
webserver has disabled: truewebserver-auth row exists with name @summersec/dsh-web-authFAILEDManual smoke:
/auth/login.dsh_web_auth present.GET /auth/status with cookie → authenticated: true.POST /auth/logout → session cleared.429 until the window resets.Package name: @summersec/dsh-web-auth (public scope).
Publishing is performed only by the repository's GitHub Actions workflow. Do not use local npm publish as a release path.
Before the first release, add a repository Actions secret named NPM_TOKEN. It must be an npm token with permission to publish @summersec packages, and the npm organization’s 2FA and CI-publishing policy must permit GitHub Actions to use that token.
Release through one of these workflow entry points:
vX.Y.Z tag that exactly matches package.json's X.Y.Z version.workflow_dispatch and provide the exact package version.The workflow validates the version, runs checks, then publishes to npm and GitHub Packages. Pushes and pull requests run the verification job only; they cannot publish packages.
If GitHub Packages has already published but npm fails, open that workflow run and choose Re-run failed jobs. Do not re-run the entire workflow, because that would try to publish the same GitHub Packages version again.
trustProxy is dangerous if mis-scoped — only enable when the listen port is exclusive to a trusted reverse proxy.always mode avoids “I thought loopback was enough” surprises on shared machines.dsh-web-auth/
├── bin/dsh-web-auth.js # generate / hash-password CLI
├── cordis.patch.yml # DSH bundle: disable stock webserver, insert webserver-auth
├── src/
│ ├── auth.js # scrypt, sessions, attempt limiter, cookie helpers
│ └── index.js # AuthenticatedWebServer service + login UI
├── test/ # node:test unit tests
├── package.json
├── README.md
└── README.zh-CN.md
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。