dsh-web-guard: Adds password protection to the DSH Web UI and fixes the /api 403 issue caused by reverse proxying.

Introduction

DSH’s Web UI has long relied only on a process-level ?token=: it changes with every restart, requires copying it out from the terminal, and /api is still prone to direct 403s after HTTP reverse proxying. dsh-web-guard (repo lvxianchao/dsh-web-guard, maintainer lvxianchao, MIT license) packages these deployment issues into a plugin: an access password, revocable sessions, a native settings page, and TLS. Below, I introduce its positioning, installation, configuration, and operational considerations.

Ecosystem background: DSH’s philosophy is “everything is a plugin”; this article’s index page comes from the third-party community site SkillHub, with no affiliation to DeepSeek. Star 1, category admin-security is carried by this index page, and as of writing, there is no other independent evidence.

What it solves

The README contrasts native behavior with plugin behavior in a table. The four problems are as follows:

Problem Native behavior dsh-web-guard
No human login Only the ?token= printed at startup, changes on restart One password + revocable sessions
Uniform 403 on reverse proxy The /api fence requires Host to be reachable as-is; NPM/Caddy rewrite it The proxy rewrites Host/Origin into a loopback authority
Token needs to be dug from logs Need journalctl \| grep token to gain access Exchange happens inside the process; the browser cannot see the token
Plaintext HTTP errors crypto.randomUUID is not a function Injects a polyfill

Zero runtime dependencies, using only the Node standard library; the npm package contains pre-built JavaScript, so dsh plugin add requires no toolchain.

How it works

browser ──HTTP(S)──> dsh-web-guard :3081 ──> DSH listener (loopback)
                          │
                          ├─ not signed in → /login
                          ├─ signed in → forward, Host/Origin rewritten to loopback
                          └─ injects the harness cookie (never exposed to the browser)

DSH’s /api fence accepts loopback authority, so making every upstream request appear to come from the loopback eliminates the 403. Authentication happens inside the proxy, not a frontend mask: requests without a session cookie cannot reach DSH. The browser half (dsh.client) is only responsible for mounting the management page into DSH’s settings panel; the gate itself is completely on the server side, so a browser half load failure doesn’t affect usage.

Installation and enabling

Three installation methods, commands from the README:

# npm
dsh plugin --profile web add @lvxianchao/dsh-web-guard

# from GitHub
dsh plugin --profile web add "github:lvxianchao/dsh-web-guard"

# from a local checkout
dsh plugin --profile web add /path/to/dsh-web-guard

The main branch only contains published code (development is on dev branch), so this command installs the latest stable version. The package declares dsh.bundle, and the guard line is automatically stacked; restart dsh after installation.

First startup automatically generates a password, printed in the terminal, and stored as a hash in <profile>/data/dsh-web-guard.json (0600). To pin the instance, modify ~/.dsh/profiles/web/cordis.patch.yml:

- id: guard
  config:
    listenHost: '0.0.0.0'
    listenPort: 3081

Note that the profile patch completely replaces config, so any keys to be retained must be rewritten.

Upgrading from v2: The floating shield in the bottom right corner and /__guard/admin have been removed; the management entry is changed to “Settings → DSH Web Guard”. Reinstall (or upgrade to 3.0.0) and restart DSH; password hash, session cache, and settings file format remain unchanged, and upgrading doesn’t kick users out.

Prerequisite: DSH itself must bind to loopback (default is 127.0.0.1). If bound to a LAN IP, clients can bypass the gate and connect directly—the plugin detects and warns when starting.

Typical usage

Let the plugin terminate TLS (no reverse proxy)

config:
  listenHost: '0.0.0.0'
  listenPort: 8443
  tls:
    certFile: /path/fullchain.pem
    keyFile: /path/privkey.pem

Browsers access https://<host>:8443, and the session cookie automatically includes Secure.

Behind a reverse proxy

The reverse proxy only does TLS. Upstream fills in http://<dsh-host>:3081 and enables WebSocket. No Host rewrite configuration is needed—this is exactly the part the plugin eliminates.

Pure LAN

Plaintext HTTP + password; simplest but least secure, and the session cookie can be sniffed.

Common configuration keys

Key Default Description
listenHost 0.0.0.0 Gate listen address
listenPort 3081 Gate port (cannot be the same as DSH port)
password — Plaintext password (hashed at startup)
passwordHash — scrypt$N$r$p$salt$key, takes precedence over password
sessionDays 7 Session duration; 0 = invalid when browser closes
maxAttempts 5 Consecutive failure count, temporary lockout if exceeded
lockMinutes 5 Lockout duration
polyfill true Inject crypto.randomUUID fallback
title DSH Web Guard Login page title
tls null {certFile, keyFile}, enables HTTPS

Generate scrypt hash:

node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv[1])))" 'your-password'

What it is, and isn’t

This is an “entry point”, not an optional plugin. Once installed, it listens on 3081 and forwards requests to DSH (3080), which only listens on loopback. Once the plugin fails to load, 3081 stops listening, and both internal and external networks cannot access it—while the DSH main body is still running, sessions and data are present, just out of reach. If this address has already been exposed to the public network, this requires special attention.

Security model boundaries must be clear: one password, one permission level; it is not a multi-user system. DSH’s agent itself can execute code, so any logged-in user possesses the execution permissions of that machine—this is a product design, not a gate defect.

The work done in the table above—attributing /api 403s to Host being rewritten by the reverse proxy, and “allowing via proxy by rewriting Host/Origin into a loopback authority”—all comes from the project README’s description. If this model is inconsistent with the actual behavior of the fence in your environment, check DSH’s official documentation before changing configuration.

Applicable scenarios and notes

Suitable for: people who want to expose DSH Web UI to the internal or public network but don’t want to keep copying tokens; people who have already set up NPM/Caddy/Traefik and are stuck on /api 403s; people who want to manage passwords and sessions using a native settings page.

Must note:

  1. Do not uninstall or disable it. Do not run dsh plugin --profile web remove @lvxianchao/dsh-web-guard; do not delete the entry from dsh.profile.bundles in the profile’s package.json; do not add disabled: true in cordis.patch.yml. DSH’s “Settings → Plugins” page is a read-only list with status tags, no switches. Mis-closing usually happens on the command line.
  2. Requires DeepSeek Harness 0.2.2-rc.1 or later (engines.dsh: >=0.2.2-rc.1 <0.3.0), Node >=22.19, license MIT. When the host version mismatches, the entire bundle will be skipped—in this case, the gate doesn’t start, so you should upgrade DSH, not delete the plugin.
  3. The plugin runs with the current dsh process permissions; check the source code and license before installing.
  4. The session table is persisted to <profile>/data/dsh-web-guard-sessions-<port>.json (0600, temp file + rename atomic write); restarting DSH won’t kick already logged-in browsers offline—but restarting doesn’t equal revocation. To revoke all, use “Sign out all devices” in the settings page.

If accidentally closed, the recovery command doesn’t depend on the plugin itself:

# 1) Reinstall (use file: if installed from local directory)
dsh plugin --profile web add file:/path/to/dsh-web-guard
# or: dsh plugin --profile web add github:lvxianchao/dsh-web-guard

# 2) Restart, and 3081 will listen again
sudo systemctl restart dsh-web

If disabled: true was added originally, delete that line and restart; the password hash is in <profile>/data/dsh-web-guard.json, not lost.

The real pitfall isn’t in these two commands, but that you have lost access to the machine right now. The gate only proxies HTTP entry points, not SSH; but if remote access is consolidated under one domain, once the plugin is gone, there’s only one path left—a back door that doesn’t go through it. At least prepare one: use the host console (PVE / ESXi / Cloud provider VNC) to enter the container or VM and execute the two commands above, or an independent SSH channel (LAN direct connection, jump box, VPN). And verify it works today, right now, then copy the recovery command to a place you can open from the outside.

Conclusion

The value of dsh-web-guard lies in converging these four tasks—copying tokens, modifying Host, digging logs, plaintext HTTP errors—into one plugin. The cost is that it becomes the only entry point—benefits and risks both stem from this. Confirm DSH version is new enough before installing, and leave a back door that doesn’t go through the gate after installation.

Index page: https://www.skillhub.cn/plugins/lvxianchao/dsh-web-guard
GitHub: https://github.com/lvxianchao/dsh-web-guard
npm: https://www.npmjs.com/package/@lvxianchao/dsh-web-guard

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye