toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
通过 Caddy forward_auth 为 DeepSeek Harness Web 提供管理员登录,含 Argon2id 口令、可撤销会话、中英界面和侧栏原生退出。
English | 简体中文
Unofficial community plugin for DeepSeek Harness. Add a secure administrator login to the DeepSeek Harness Web app. dsh-auth keeps Harness on loopback and installs a project-owned Caddy forward_auth edge for pages, APIs, downloads, SSE, and WebSockets.
Version 0.2.0 is a breaking upgrade from legacy v1 deployments. Previous installer flags, Nginx-managed installations, and old sessions are not migrated. Uninstall the previous installation, then run setup again.
Install the published CLI, then start from an existing DSH Web systemd service whose upstream listens only on loopback:
sudo npm install -g dsh-auth
sudo dsh-auth setupnpm install -g dsh-auth installs the current stable CLI, and the installer pins that same version in the selected DSH profile. For controlled production rollout, install the exact version approved by your supply-chain policy:
sudo npm install -g dsh-auth@0.3.0dsh plugin --profile web add dsh-auth (or a local tarball) only adds the bundle to the Web profile. With both core environment variables absent the bundle stays dormant: Web keeps booting with its native authentication, and no dsh-auth routes or settings UI appear. A partially supplied configuration produces an activation error and does not enable dsh-auth; Harness may keep running with its native authentication. Enabling authentication always requires the globally installed CLI and sudo dsh-auth setup; the plugin command never creates secrets, installs Caddy, or protects anything.
When setup finds a pre-installed bundle whose package name, version, and build content exactly match the running CLI, it adopts it: no package is reinstalled, and the bundle keeps external ownership. Any other pre-installed build is refused before the host changes. Rollback and uninstall leave an adopted bundle in place; without managed configuration it simply returns to dormancy.
The interactive installer asks for the exact DSH service, administrator initialization method, HTTPS hostname, and TLS mode; shows a secret-free plan; and changes the system only after you type the exact confirmation. It installs the pinned bundle into the selected DSH profile, copies a checksum-verified Caddy binary bundled in the same package, writes permission-restricted authentication state, and enables an independent dsh-auth-caddy.service. It never stores a plaintext password and never downloads Caddy at setup time.
Normal deployment requires Linux x64 or ARM64, systemd, Node.js 24.7 or newer, and DSH Web 0.2.0-rc.2. Automatic TLS is the HTTPS default. Manual TLS requires an existing certificate and key. --server-name accepts either a DNS name or a public literal IP address.
The standalone installer explicitly enables DSH_AUTH_NATIVE_AUTH_BRIDGE=true: a valid, revocable dsh-auth session can then act as the native Harness operator while native Host and Origin checks remain enforced. The plugin defaults this option to false. Multi-user gateways such as Chatroom must leave it disabled and enforce their own resource authorization. Revoking a dsh-auth session does not revoke separately issued native Harness credentials.
$ sudo dsh-auth setup
Existing DSH Web systemd unit: dsh-web.service
Administrator initialization (password/login-token): password
Login tokens (enabled/disabled) [disabled]: enabled
Administrator username: operator
Edge mode (https/http) [https]:
TLS (automatic/manual) [automatic]:
Public HTTPS hostname: harness.example.com
...
Type install to apply this exact plan: install
Password:
Confirm password:
dsh-auth setup completed successfully.Rerunning the same command is idempotent. An existing managed installation with identical non-secret settings is reported unchanged; different settings or files without an ownership record are rejected instead of overwritten.
Use plan before setup to inspect the same typed plan without reading a password or changing the filesystem:
sudo dsh-auth planNon-interactive mode requires stable flags and an explicit administrator initialization method. For password initialization, mount the plaintext password as a temporary 0600 secret file supplied by the platform; dsh-auth reads it once to create an Argon2id hash and does not copy the plaintext.
These command names, flag names, --name value or --name=value syntax, JSON schema version 2, and exit codes are the public automation contract. Global flags may precede the command. New flags and diagnostic codes may be added. Renaming, removing, or changing the meaning of an existing flag, JSON field, or exit code is a breaking change.
Print the frozen usage text:
dsh-auth --help
dsh-auth --version-h is an alias for --help. dsh-auth setup --help prints the same usage text. The example below is a complete HTTPS system install with password initialization and automatic TLS.
Prompts run only when stdin and stdout are both TTYs and --non-interactive is not set. --json is output format only and does not disable prompts.
sudo dsh-auth setup \
--non-interactive \
--json \
--dsh-service dsh-web.service \
--dsh-home /var/lib/dsh \
--dsh-executable /usr/local/bin/dsh \
--profile web \
--admin-bootstrap password \
--admin-username operator \
--login-token enabled \
--password-file /run/secrets/dsh-auth-password \
--mode https \
--tls automatic \
--upstream 127.0.0.1:3080 \
--listen-address 0.0.0.0 \
--server-name harness.example.comToken initialization omits the password and username. The first authorized user sets them in the browser, or chooses Later:
sudo dsh-auth setup \
--non-interactive \
--json \
--dsh-service dsh-web.service \
--admin-bootstrap login-token \
--login-token enabled \
--mode https \
--tls automatic \
--server-name harness.example.comFlag · Required · Default · Description --help, -h · no · · Print usage and exit. --version · no · · Print the CLI version and exit. --non-interactive · on a TTY · · Disable prompts. --json · no · · Emit one JSON document. Does not disable prompts. --mode · no · https · https or http. --behind-tls-proxy · no · disabled · Keep the managed HTTP edge on loopback, require trusted HTTPS forwarding headers, and issue Secure cookies. --admin-bootstrap · when not prompting · · password or login-token. --admin-username · password setup · · Initial administrator login name. --login-token · when not prompting · · enabled or disabled. Token initialization requires enabled. --login-token-error-message-zh · no · built-in Chinese copy · Optional 1–500 character Chinese token-failure page text. Requires --login-token enabled. --login-token-error-message-en · no · built-in English copy · Optional 1–500 character English token-failure page text. Requires --login-token enabled. --listen-address · HTTP · 0.0.0.0 for HTTPS · Literal IP bind address. HTTP still requires an explicit private or loopback address. --dsh-service · system setup · · Exact existing DSH Web systemd unit. Omit only with --output-dir. --password-file or --password-stdin · ready password setup · · Password source. Not used by plan or token initialization. Unchanged reruns skip it. --server-name · --mode https · · Public HTTPS hostname. --tls · HTTPS · automatic · automatic or manual. --certificate · --tls manual · · Absolute TLS certificate path. --certificate-key · --tls manual · · Absolute TLS private-key path. --dry-run · no · · On setup, alias for plan. On uninstall, list owned removals without changing the host. --dsh-home · no · discovered · Harness home when the unit does not infer it. --dsh-executable · no · discovered · DSH executable file when the unit does not infer it. Not a directory. --profile · no · web · DSH profile name. --upstream · no · 127.0.0.1:3080 · Loopback DSH listener (127.0.0.1 or [::1]). --package · no · dsh-auth@ · Pinned registry spec or absolute .tgz. --http-port · no · 80 (8080 for HTTP) · HTTP or HTTPS-redirect port. --https-port · no · 443 · HTTPS listen port. --output-dir · no · · Offline or container render directory. Skips systemd.
When --server-name is a publicly routable IPv4 or IPv6 address and --tls automatic is selected, the managed Caddy requests a free Let’s Encrypt IP address certificate with the shortlived profile and renews it automatically. Let’s Encrypt requires these certificates to be valid for about six days, so the host must keep Caddy’s persistent state and outbound ACME access available. The HTTP challenge port must be reachable from the Internet (normally TCP 80); the certificate authenticates the IP, not a port, so the HTTPS listener may use another port after issuance.
For the public address 9.135.102.192 on the normal HTTPS port:
sudo dsh-auth setup \
--non-interactive \
--dsh-service dsh-web.service \
--admin-bootstrap login-token \
--login-token enabled \
--mode https \
--tls automatic \
--server-name 9.135.102.192 \
--http-port 80 \
--https-port 443If ACME validation cannot reach the machine, setup does not turn that into a trusted certificate: use a DNS name, restore public access to the challenge port, or provide an existing certificate with --tls manual. tls internal remains an explicit local/evaluation fallback and is not a publicly trusted certificate.
Removed without aliases: --nginx, --authorize-nginx-install, --user-id, --username, --roles, and --dsh-bin.
Other commands accept a smaller frozen flag set:
Command · Required when not prompting · Optional plan · Same setup flags, without a password source · --json, --non-interactive doctor · · --json upgrade · --authorize-upgrade · --package, --json, --non-interactive, --dry-run reset-password · --password-file or --password-stdin; --authorize-password-reset · --json, --non-interactive uninstall · --authorize-uninstall · --json, --non-interactive, --dry-run issue-login-token · --authorize-login-token-issue when not prompting · --ttl-seconds, --public-origin, --auth-state-file with --public-origin, --json hash · · --password-stdin secret · ·
Passwords are accepted only through hidden interactive input, --password-stdin, or --password-file. There is no inline password flag. Command output, JSON, plans, subprocess argv, and installer errors never contain password or session-secret values. issue-login-token is the only command whose successful stdout or JSON may contain a bearer login token.
When setup enabled login tokens, a cloud control plane or operator can mint a single-use URL. The raw token appears only in the successful human URL line or the JSON success document:
sudo dsh-auth issue-login-token --non-interactive --authorize-login-token-issueThe URL uses a fragment (/auth/token#token=…). Opening it establishes the same 72-hour rolling session as a password login. If the administrator password has not been set, the browser first offers a setup page; Later skips only that login.
Container and image layouts pass explicit paths instead of reading the systemd ownership record:
dsh-auth issue-login-token \
--non-interactive \
--authorize-login-token-issue \
--json \
--auth-state-file /export/dsh-auth/state/auth-state.json \
--public-origin https://harness.example.comA system installation created with --behind-tls-proxy also takes the current public HTTPS origin at issue time. The value is not stored by setup, so changing an outer proxy address or port does not require reinstalling dsh-auth:
sudo dsh-auth issue-login-token \
--non-interactive \
--authorize-login-token-issue \
--public-origin https://203.0.113.10:49152Setup can replace the built-in failure page text. Configure Chinese and English independently; an omitted language keeps its built-in copy. Each value is 1–500 Unicode characters of plain text. Control characters are rejected, and HTML is shown as text rather than markup. The installer refuses these flags when --login-token is disabled.
Malformed, expired, already-used, and unknown tokens all return the same HTTP 401 page with that text. The page does not identify which of those cases occurred. A token POST that fails the Origin or CSRF check returns a different HTTP 403 page: it asks the user to reopen the latest console link or check the public access address, does not use the 401 text, and does not consume the token.
sudo dsh-auth setup \
--login-token enabled \
--login-token-error-message-zh '登录链接不可用,请向管理员重新申请。' \
--login-token-error-message-en 'This sign-in link is unavailable. Request a new one from your administrator.'dsh-auth exposes a provider-neutral authorization-code interface. The built-in ioa provider adapts Tencent IOA/Taihu's signed AccessToken exchange while the session, CSRF, state, and authorization policy remain provider-independent.
Enable it through the Cordis bundle configuration (the provider is disabled by default):
externalIdentity:
enabled: true
paasId: ${TAIHU_PAAS_ID}
tokenFile: /run/secrets/taihu-token
baseUrl: https://api.woa.com
callbackUrl: https://lightpilot.woa.com/auth/callback
allowedUsers: [masonxhuang, yuehuali]
allowedDepartmentIds: []
allowedDepartmentPrefixes: []Users start the flow at /auth/login/ioa. The callback validates a short-lived state value, exchanges the one-time code server-side, applies the configured user/department allowlist, and creates the same revocable opaque session used by password login. The Taihu token is read only from the permission-restricted tokenFile; it is never accepted in a URL or persisted authentication state.
An authenticated GET or HEAD request to /auth/verify still returns 204 and the legacy X-Dsh-Auth-User-Id: admin, username, and edge-role headers. For an external (IOA) session it also returns URL-encoded, validated profile headers: X-Dsh-Auth-Subject, X-Dsh-Auth-Username, X-Dsh-Auth-Display-Name, and optional X-Dsh-Auth-Picture. Subject is the stable external account key; X-Dsh-Auth-Roles describes only the dsh-auth edge and must not be used as an application role. The managed Caddy removes client-supplied values before forward_auth and copies only verifier output to the upstream. Profile fields are bounded to 512 UTF-8 bytes and reject control characters; picture URLs must be HTTPS without credentials or fragments.
Signed-in administrators can open Settings → General → Reset password, enter the current password, and set a new one. That updates the stored hash and signs out other browser sessions; it does not rotate the session secret.
If the current password is unavailable, operators with root on an installation created by setup can run the interactive reset:
sudo dsh-auth reset-passwordAfter exact confirmation, the command reads and confirms the new password without echo. It atomically replaces the managed Argon2id hash, rotates the session secret, revokes all existing sessions, and restarts the recorded DSH service only when it is active. A failed restart restores both previous credential files.
Automation must provide the password through stdin or a temporary 0600 file and explicitly authorize the operation:
sudo dsh-auth reset-password \
--non-interactive \
--json \
--authorize-password-reset \
--password-file /run/secrets/dsh-auth-new-passwordThe command never accepts a password value in argv and does not print the password, hash, or session secret.
Plain HTTP remains authenticated but exposes credentials and sessions to network interception. It is accepted only with an explicit --mode http and a literal loopback, RFC1918, or ULA listen address:
sudo dsh-auth setup \
--admin-bootstrap password \
--admin-username operator \
--login-token disabled \
--mode http \
--listen-address 10.0.0.20 \
--http-port 8080Do not use this mode on an untrusted network. HTTPS is the production default.
Operators may keep certificates and public TLS in a same-host or same-network-namespace ingress, load balancer, or reverse proxy while retaining the managed dsh-auth Caddy as the only authentication edge that can reach DSH:
sudo dsh-auth setup \
--admin-bootstrap login-token \
--login-token enabled \
--mode http \
--listen-address 127.0.0.1 \
--http-port 8080 \
--behind-tls-proxyThis mode accepts only a loopback listener. The outer proxy must connect to that listener from loopback and must overwrite X-Forwarded-Host, X-Forwarded-Proto, and X-Real-IP; the forwarded protocol must be https. Missing forwarding metadata is rejected. dsh-auth preserves the public authority for exact Origin checks, uses relative login redirects, and emits Secure, __Host- cookies even though its inner hop is HTTP.
The outer proxy, its certificates, public address, and port remain operator-owned. Setup does not discover, reload, or modify them, and their changing public origin is not part of the setup fingerprint. Do not expose the inner listener, use a path prefix as an authentication secret, or let the outer proxy append client-supplied forwarding headers.
TLS termination and authentication enforcement remain separate ownership boundaries in this topology. The outer proxy does not need to know Harness routes or dsh-auth session semantics, and it must never proxy any path directly to Harness. dsh-auth deliberately retains its managed Caddy so operators do not have to reproduce complete forward_auth coverage for pages, APIs, downloads, SSE, and WebSockets in an existing gateway. Using an operator-managed Caddy, Nginx, ingress, or load balancer as the authentication edge is not a supported deployment mode.
doctor checks the ownership record, file permissions, the exact DSH service, root-executable safety, Caddy version and checksum, caddy validate, and service state:
sudo dsh-auth doctor
sudo dsh-auth doctor --jsonRuntime authentication events use the Cordis logger name dsh-auth. The managed Caddy service writes security-sensitive access events to /var/lib/dsh-auth-caddy/access.log. Collect the application journal, restricted access-log files, and read-only health report: