Back to home

SummerSec

dsh-web-auth

Transport-level authentication gate for the DeepSeek Harness Web GUI

Stars
0
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 16, 2026

Introduction

dsh-web-auth

npm Node.js License: MIT topic: dsh-plugin

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


Login page

DeepSeek Harness authentication page


Why this exists

DSH’s stock web host is convenient for local use, but it is not a product auth layer:

  • Binding to 0.0.0.0 or putting the port behind a reverse proxy can expose the full control surface.
  • A frontend-only “login page” does not protect /api, static plugin assets, SSE, or WebSocket upgrades.
  • Session and password handling need to live on the HTTP carrier itself.

@summersec/dsh-web-auth sits at the transport layer:

  1. Disable @deepseek-ai/dsh-host-webserver.
  2. Insert webserver-auth with the same ctx.webServer contract (register, registerUpgrade, registerFallback, tapIndex, host, port).
  3. Gate HTTP and upgrade traffic with a server-side session cookie.

Other plugins keep registering routes as usual; they do not need to know auth exists.


Features

AreaBehavior
CoverageHTTP routes and WebSocket / HTTP upgrade paths
Default modealways — login required even on 127.0.0.1
Optional modenon-loopback — skip auth only when bound to loopback
Passwordsscrypt hashes (scrypt$N$r$p$salt$key); plaintext env only for temporary use
Sessions32-byte random tokens, in-memory store, sliding TTL
CookiesHttpOnly, SameSite=Strict, optional Secure
Abuse controlPer-client-IP login attempt limiter with Retry-After
Login UXBuilt-in /auth/login page (light/dark), form + JSON body
HardeningOrigin check on login/logout, open-redirect sanitization, CSP and frame denial on auth responses

Requirements

  • Node.js >= 22
  • DeepSeek Harness with a web profile (peer: @deepseek-ai/cordis ^4.0.1)
  • A password hash in the process environment (recommended), or a temporary plaintext password

Quick start

# 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 .env if that file is shared or committed. Prefer the process environment, a secrets manager, or a private host-level env file outside the repo.


Install from source

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

Authentication modes

authMode / WEB_AUTH_MODEWhen auth runs
always (default)Always, including host: 127.0.0.1
non-loopbackOnly 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.


Environment variables

The bundle (cordis.patch.yml) wires these into plugin config:

VariableDefaultDescription
WEB_AUTH_MODEalwaysalways or non-loopback
WEB_AUTH_USERNAMEadminLogin 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.


Advanced configuration

The bundle:

  1. Sets the stock webserver row to disabled: true.
  2. Inserts 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

Config reference

FieldType / valuesDefaultNotes
host127.0.0.1 | 0.0.0.0127.0.0.1Listen address (from web startup)
port0655353080Listen port; 0 for ephemeral
authModealways | non-loopbackalwaysSee Authentication modes
usernamestringadminSingle shared account
passwordstringPlaintext; avoid in production
passwordHashscrypt$...Required format from the CLI
sessionTtlMinutes143200720 (12h)Sliding window on each authenticated request
maxAttempts110005Failed logins per IP per window
attemptWindowSeconds186400300Attempt window length
secureCookieauto | always | neverautoWhen to set the Secure flag
trustProxybooleanfalseTrust X-Forwarded-* only behind a locked-down proxy

secureCookie and trustProxy

ScenarioSuggested settings
Local HTTP on loopbacksecureCookie: auto, trustProxy: false
Direct TLS on the Node processsecureCookie: auto (sets Secure when the socket is encrypted)
HTTPS terminated at nginx / Caddy / CloudflaresecureCookie: 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.


Brute-force protection

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:

  • Counters are stored in process memory, so a restart clears them and multiple instances do not share state.
  • It limits IP addresses, not accounts. Attackers rotating source IPs can avoid a single-IP threshold.
  • With 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.


Auth HTTP API

MethodPathPurpose
GET / HEAD/auth/loginLogin HTML page; ?next=/path for post-login redirect
POST/auth/loginAuthenticate (application/x-www-form-urlencoded or application/json)
POST/auth/logoutClear session cookie and redirect to login
GET/auth/status{ authenticated, required, username? }200 or 401

Login body (JSON)

{
  "username": "admin",
  "password": "...",
  "next": "/"
}

Behavior notes

  • Successful form login responds with 303 + Set-Cookie (dsh_web_auth) and Location set to a sanitized relative path (blocks //evil, absolute URLs, and header-injection characters).
  • Failed login returns the login page with an error message (401) or rate-limit page (429 + Retry-After).
  • Login and logout require a matching Origin when the header is present (CSRF-oriented check).
  • WebSocket upgrades without a valid session are closed with 401 and a JSON error body.
  • Auth HTML responses set Cache-Control: no-store, a strict CSP, X-Frame-Options: DENY, and related headers.

How it fits into DSH

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 getters

CLI

Package 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

Password hashing algorithm

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:

  1. Generates a new 16-byte random salt with crypto.randomBytes.
  2. Derives a 64-byte key with N=16384, r=8, and p=1.
  3. Stores the algorithm name, parameters, salt, and derived key in one string. The salt and key use unpadded Base64URL encoding.
  4. During login, derives the key again with the stored parameters and compares it with 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.


Verification

npm run check          # syntax check + unit tests
npm pack --dry-run     # publish file set
dsh --profile web --dump-config

In the dump, confirm:

  • Stock webserver has disabled: true
  • A webserver-auth row exists with name @summersec/dsh-web-auth
  • Startup logs do not show FAILED

Manual smoke:

  1. Open the GUI without a cookie → redirect to /auth/login.
  2. Log in → land on the app; cookie dsh_web_auth present.
  3. GET /auth/status with cookie → authenticated: true.
  4. POST /auth/logout → session cleared.
  5. Exceed failed attempts → 429 until the window resets.

Publish to npm

Package name: @summersec/dsh-web-auth (public scope).

cd D:\ghproject\dsh-web-auth
npm login
npm whoami
npm run check
npm pack --dry-run
npm publish --access public
# with 2FA: npm publish --access public --otp=123456

Later releases:

npm version patch   # or minor / major
npm publish --access public
npm view @summersec/dsh-web-auth version

Limitations

  • In-memory sessions — process restart invalidates all logins; no multi-instance sticky session store.
  • Single shared account — one username/password boundary, not multi-user RBAC or audit roles.
  • Only the DSH web carrier — other ports or sidecars need their own protection.
  • Not a substitute for TLS — put HTTPS in front for any non-loopback or multi-user network.
  • trustProxy is dangerous if mis-scoped — only enable when the listen port is exclusive to a trusted reverse proxy.

Security notes

  • Prefer scrypt hashes over plaintext env passwords.
  • Default always mode avoids “I thought loopback was enough” surprises on shared machines.
  • Cookie flags and Origin checks reduce common session theft and CSRF patterns; they do not replace network isolation and HTTPS.
  • Report security issues privately if you find one; do not open a public issue with exploit details.

Project layout

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

Links


License

MIT