toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
DeepSeek Harness Web UI 认证网关:登录门禁覆盖页面、/api、/plugins 与 WebSocket 升级;PBKDF2 口令哈希(每用户独立盐)、HttpOnly SameSite 会话 Cookie(名字按 DSH_HOME 唯一)、按源 IP 登录锁定;邀请码注册;两步验证可用 TOTP 动态码或通行密钥(WebAuthn:一个账号可绑定多个密钥,手机可扫码添加,支持免用户名登录,改动登录因子前必须二次验证);设置面板内置用户管理(普通用户可修改本人资料与密码并管理自己的 TOTP 与通行密钥,管理员可新增/删除用户、重置密码、管理邀请码、清除丢失设备的通行密钥);模型与 API Key 按用户隔离管理,私有配置用登录口令派生的密钥加密、任何管理员都无法读取,管理员可把自己的模型分享给指定用户(可用、可看余额、看不到 Key、撤销立即生效),未配置的用户被阻断而不是回退到部署级配置;REST/列表接口按用户隔离,WebSocket 主通道逐帧按归属过滤;会话重启后免登录恢复;JSONL 审计日志;存储故障时保持 fail-closed。仅支持 DSH 0.2.0 线(0.1.1-rc.2 传输线已在 0.7.0 移除);因浏览器要求安全上下文,通行密钥需通过 localhost 或 HTTPS 访问。
**面向:**首次接触本插件的用户。
v0.7.0 起本插件只跟随 DSH 的新版本开发(目标版本 0.2.0-rc.2),
legacy 传输线(dotted /api/<a>.<b> + apiProxy)的代码已完全移除:
宿主未提供现代传输能力时插件会 fail-closed(认证网关不建立、面板不可访问并输出明确错误),
不会退回旧传输线,更不会降级成无门放行。
若 DSH 后续架构有大变动,我会提前声明该版本的支持结束时间,不会突然中断。
仍在使用 0.1.1-rc.2 的部署请在此之前按 DSH 版本兼容性 一节规划升级。
给 DeepSeek Harness(DSH)的 Web UI 加一道用户名 / 密码登录门:未登录时无法访问任何页面、 API 或 WebSocket 通道;登录后可管理用户、邀请码、两步验证(TOTP)与通行密钥(Passkey), 并按登录用户隔离会话数据。
适用场景:把 DSH 面板暴露到内网或公网时,需要一个前置认证层,并希望不同使用者之间互不可见。
本 README 只讲怎么装、怎么登录、怎么用。更深的主题按你的角色分流(完整清单见 docs/INDEX.md):
| 你的角色 | 去哪读 |
|---|---|
| 第一次用,只想跑起来 | 本文往下读即可 |
| 负责部署与运行 | 运维手册(环境变量、备份、升级、排障) |
| 做安全审计 | 安全模型 + 验证证据 |
| 要升级 DSH / 做集成 | 0.2.0 兼容性 |
| 想读代码 / 提 PR | 架构 + 贡献指南 |
/api/*、/plugins/*、HMR、
SPA fallback 与 WebSocket 升级通道,没有旁路。未登录时页面请求 302 跳登录页、API 返回 401、
WS 升级直接断开;登录后原请求原样透传。Cache-Control: no-store;
通行密钥使用一次性短时挑战、严格的来源/RP ID 校验与签名计数器回退检测。locale/change,
服务端渲染页按 Accept-Language),详见多语言。dsh-ui-auth-audit.jsonl(JSONL 审计文件)。| 项 | 要求 |
|---|---|
| DSH | >=0.2.0-rc.2 <0.3.0(web profile)。v0.7.0 起只支持 0.2.0 线——legacy 的 0.1.1-rc.2 传输线已移除,失去现代传输能力时插件 fail-closed(面板不可访问并打印明确错误),详见兼容性矩阵 |
| Node.js | `^22.19.0 |
| 运行时依赖 | qrcode(生成 TOTP 二维码)、ws(流式通道)与 @simplewebauthn/server(通行密钥服务端校验),安装时自动获取,无需手动构建 |
| 浏览器 | 通行密钥需要 Chrome / Edge / Safari / Firefox 等支持 WebAuthn 的现代浏览器;不支持时其余功能不受影响 |
通行密钥对访问地址有硬性要求(浏览器规则,插件无法绕过):
| 访问地址 | 通行密钥 | 说明 |
|---|---|---|
https://你的域名 | ✅ 可用 | 推荐的生产部署方式(反代终止 TLS) |
http://localhost:3080 | ✅ 可用 | 本机使用请用 localhost,不是 127.0.0.1 |
http://127.0.0.1:3080 | ❌ 不可用 | Chrome 拒绝 IP 地址作为通行密钥域(RP ID);面板会提示改用 http://localhost:3080 |
http://内网IP:3080 | ❌ 不可用 | 明文 HTTP 不是安全上下文,且 IP 不能作为 RP ID;需域名 + HTTPS |
本包是标准的 DSH profile bundle(声明了 dsh.bundle.patch 与 dsh.client),
dsh plugin 会自动把它加入 profile 的 bundle 名单:
# 从 npm 安装
dsh plugin --profile web add dsh-ui-auth
# 或固定到某个 GitHub 发行版本
dsh plugin --profile web add github:0QwQ0/dsh-ui-auth#v0.7.0
# 或从本地目录安装(离线 / 二次开发)
dsh plugin --profile web add <本插件目录的绝对路径>
安装完成后重启一次面板生效(bundle 层在启动时应用)。
用户表为空时,插件会自动创建管理员 admin,随机密码同时输出到两处:
[dsh-ui-auth] 开头);dsh-ui-auth-bootstrap.txt。打开面板会跳转到 /auth/login,用 admin 与上述随机密码登录。
登录后请立即在【设置】→【用户管理】中修改密码;任意用户改密成功后,引导文件会自动删除。
【用户管理】→ 修改密码:先输入当前密码,离开该输入框时自动校验;通过则边框变绿(2px 向外荧光) 并解锁下面两个输入框,失败则变红且保持锁定。新密码实时评估强度:不满足=红、刚满足=黄、很强=绿; 只有在至少黄色时才解锁「确认新密码」,并实时比对(不匹配红、匹配绿)。注册页采用同一套规则。
注册入口在登录页的「注册账号」。新用户需要管理员事先在【用户管理】→【邀请码管理】中生成的有效邀请码; 每个码可设置 1–100 次的注册次数,可随时撤销。邮箱目前只做格式填写、不校验真实性,注册后可在用户管理中修改。
两步验证可以用两种因子满足:TOTP 动态码或通行密钥。绑定任一种后即可在 【用户管理】→「两步验证」用开关启用;开关打开后,登录方式如下(通行密钥始终可直接登录, 与两步验证开关无关):
| 账号状态 | 可用的登录方式 |
|---|---|
| 两步验证关闭 | 用户名 + 密码;通行密钥 |
| 两步验证开启,已绑定 TOTP | 用户名 + 密码 + 动态码;通行密钥 |
| 两步验证开启,只绑定了通行密钥 | 用户名 + 密码 + 通行密钥;通行密钥(可不填用户名) |
0.6.4 起移除了「免密 + 动态码」登录:只有动态码、没有密码的登录请求一律被拒绝。 免密入口统一收敛到通行密钥。
TOTP:在【用户管理】→「两步验证(TOTP)」生成密钥,用验证器 App 扫码(或手动输入密钥 / otpauth 链接)后输入 6 位动态码完成绑定。动态码错误同样计入失败锁定;移除令牌需要当前动态码, 管理员可移除任意用户的令牌。
通行密钥:在【用户管理】→「通行密钥(Passkey)」中添加,同一个账号可以绑定多个:
安全约定:通行密钥的私钥永不离开设备,服务器只保存公钥与名称等公开信息;添加、重命名、删除 通行密钥都需要先确认当前密码(两步验证开启时还需动态码或一次已有通行密钥的确认); 为避免账号被锁死,两步验证开启且只剩一个通行密钥时不允许删除它——请先绑定 TOTP 或先关闭两步验证。 设备丢失时,管理员可在【用户管理】中「清除通行密钥」救援。
同一套权限策略在服务端逐端点生效,界面只是它的呈现:
| 设置页 | 普通用户 |
|---|---|
| 用户管理 | ✅ 完整可用(改自己的资料/密码、TOTP、通行密钥、两步验证;管理员额外有用户与邀请码管理) |
| Agent 预设 | ✅ 可查看预设清单、查看本会话所用预设、为自己会话选择预设;创建/删除预设仅管理员 |
| 插件 | ✅ 可查看本部署已安装插件的清单;安装/卸载/启停插件、单个插件的设置与密钥仅管理员 |
| 通用设置 | 可查看;写入部署级设置仅管理员 |
| 模型 | ✅ 管理自己的模型与 API Key(私有、服务端不可解密);管理员分享的可选用并看余额,看不到 Key |
需要放开某项的部署,可由宿主插件通过 uiAuth.registerPolicy() 逐条登记,见
docs/DSH-0.2.0-COMPATIBILITY.md。
DSH 本身按单用户设计(会话、工作区是机器级数据)。本插件按登录用户隔离:
session/list 只回自己的,直连他人 403);baseline.items / upsert.workspace / order.workspaceIds / sessionId):他人的数据丢弃或裁剪,
自己的原样转发,管理员不受限。实测对比见已知边界;DSH_AUTH_MUX_PROXY=0 可临时退避为直通(会重现泄漏,仅排障用);普通用户在【设置】→【模型】里管理只属于自己的模型与 API Key:
前4...后4)/ 默认 / 余额;行可点选。查余额(全部配置)(一次请求内顺序查询全部配置,整批只节流一次)、
设为默认、选用分享、删除、添加;另有 校验有效性 在保存前探活当前填写的 Key。管理员在【分享管理】里创建分享、按用户名授予/撤销,并查看每条分享的授权用户列表与用量; 所有分享 Key 收进下拉菜单,选中后才展开该 Key 的授权列表。
| 登录页(含通行密钥入口) | 注册页 |
|---|---|
![]() | ![]() |
| 通行密钥(Passkey)卡片 | 用户管理页 |
|---|---|
![]() | ![]() |
| 注册成功引导页(绑定第二个因子) | 模型页(按用户,v0.7.0 表格化) |
|---|---|
![]() | ![]() |
| 分享管理(管理员,v0.7.0) |
|---|
![]() |
截图由
node test/shot.mjs(基础页)与node test/shot-features.mjs(模型页 / 分享管理)在一次性实例上生成(见脚本头部的环境变量说明), 因此图中不含任何真实账号数据。通行密钥相关界面必须在localhost或域名 + HTTPS 下才会完整渲染。
dsh plugin --profile web remove dsh-ui-auth
# 重启面板后网关、设置面板与客户端模块全部消失;profile 的依赖与 bundle 名单自动还原
卸载不会删除账号数据(防误删);如需一并清空,见上一节。
dsh_auth_<DSH_HOME 短哈希>,
因为浏览器 Cookie 不区分端口,固定名会让同机多实例互相踢下线);在 TLS 直连或(仅在信任反代时)
X-Forwarded-Proto: https 的通道下自动追加 Secure。127.0.0.1 或内网。反代场景请按上文开启 DSH_AUTH_TRUST_PROXY=1,否则限流会按代理 IP 聚合。dsh-ui-auth-sessions.json。/api 的 DNS-rebinding 信任栅栏明确不是认证;
本插件才是前置认证层,两者叠加使用。v0.7.0 的 mux 边界(已在 v0.7.1 修复):DSH 0.2.0 的客户端把
/api/remote.mux(WebSocket)当作主连接通道, 普通用户也需要它,而宿主把这条通道当单用户用——它会把全部工作区与会话推给每个连接, 于是普通用户能看到管理员的会话/工作区(Issue #6)。v0.7.1 起网关在本插件内透明代理这条升级并逐帧按登录归属裁决, 不再把升级直接交给宿主;退避开关DSH_AUTH_MUX_PROXY=0仅用于排障(退避后泄漏会重现)。
v0.7.0 实测发现并修复的问题(均已在真实实例上验证): 凭据键语法错误会让宿主起不来、R1-ii 三处接线问题、余额 fetcher 从未注入、 WebSocket 升级被自研 mux 接管导致设置页一直「重新连接中」、跨实例会话 Cookie 同名互相踢下线。 详见
CHANGELOG.md的 0.7.0 条目。
uiAuth.registerPolicy({stream}) 在 0.7.1 及更早版本不生效(已在 main 修复,下一个发行版生效):
自 0.7.0 起 handleUpgrade 一直不可达,v0.7.1 删除自研 mux 后 stream 规则的唯一调用点也彻底消失——
因此部署方为流式端点登记的放行与逐帧裁剪会被静默忽略(http/rpc/remote/upgrade 四个 section 不受影响,
这也是它难以被发现的原因)。main 上已把 stream 规则接回透明代理的两个方向(打开授权 + 逐帧投影),
并加了「五个 section 都必须有查询点」的契约回归;在下一个发行版发布前,请勿依赖 stream 登记——
流式端点的归属过滤由本插件内置策略负责(会话/工作区/事件归属,见下)。
mux 跨用户泄漏(Issue #6,已修复):网关现在代理 /api/remote.mux 升级,并在两个方向都按登录归属把关:
policy.frame()——他人的 baseline.items/upsert.workspace 丢弃、
order.workspaceIds 与 archivedSessionIds 裁剪、session/control 基线按属主投影、api-session/* 等事件按会话归属、
无归属字段的帧(如 ready 握手)放行、管理员不受限。三种形态都是真实浏览器抓包发现的(baseline 最易漏,
它没有顶层 sessionId);被隐藏的 waterfall 接收方仍会代其释放投递,属主的交互不会挂住。open 先按归属授权,未授权就不转发(对端只看到 auth/forbidden)。
宿主的 mux 是"单用户"语义,会照单服务任何 sessionId,所以只过滤下行是不够的。
回归护栏:npm run test:mux:leak(双身份真实浏览器 A/B:普通用户可见工作区必须为 0、且对他人会话的 open 不得到达宿主,
管理员侧为阳性对照;NO-LEAK 退出 0)+ 安全套件的 WS-ISO 分类(17 项,驱动策略层与代理接线)。
实测(同一构建、只改 DSH_AUTH_MUX_PROXY):直通时普通用户可见 20 个管理员工作区、且他人会话 open 被放行到宿主;
代理下为 0,且 open 被我方拒绝(管理员 20 个不受限、open 正常到达宿主)。
排障退避:DSH_AUTH_MUX_PROXY=0(退避后上述泄漏会重现,仅用于定位问题)。旧自研 mux 已彻底移除:typertGateway.wireStream 自建流那套(含 MAX_STREAMS、StreamWork)是 0.7.0 的死代码,
已删除;升级入口收敛为网关的 handleUpgrade(mux → 代理;其他升级路径 → upgrade 策略,默认仅管理员,
可用 uiAuth.registerPolicy() 逐条放开,并监视登录态失效后主动断开)。
会话 Cookie 按实例唯一:以 DSH_HOME 短哈希为后缀(实例内稳定、实例间互不干扰)。
注意:该修复在 v0.7.0 生效;同机上仍在跑 0.6.x 的实例其 Cookie 名仍旧式,跨版本仍可能互相踢。
查余额需要解锁:私有 Key 为口令派生密钥加密(E2E),服务端在未解锁时无法解密,故按设计提示「需先解锁」。
界面文案支持中文 / English,跟随 DSH 的语言设置自动切换(无需重启面板):
| 层 | 取语言的方式 |
|---|---|
| 客户端界面 | 用 DSH 的 locale 服务:ctx.locale.register/bind 注册并绑定词典,订阅 locale/change 即时重渲染 |
| 服务端渲染页(登录 / 注册 / 注册成功) | 用户偏好缺省时按 DSH 的规则交给浏览器,即读取请求的 Accept-Language |
| 服务端消息(RPC 错误等) | 与页面同一套词典,在统一出口处翻译 |
实现要点:
src/i18n.ts,用于逐点 t())与短语表(src/i18n-phrases.ts,约 320 条),
后者按「中文原文 → 英文」做子串替换,因此能覆盖拼装式文案(如 当前登录: + 用户名);.dshua 面板与设置对话框),不触碰宿主其它界面,且记住原文可双向还原;t() 为准(最权威),快照与 documentElement.lang 仅作兜底,且每次替换前即时探测:
DSH 的 locale 同步可能晚于插件加载,缓存会导致用错语言。自检:node test/i18n.test.mjs(词典完整性 / 替换行为 / Accept-Language 解析)与
DSH020_URL=... node test/live-i18n-check.mjs(真实实例上切 en 再切回 zh 的端到端检查)。
环境变量、数据落盘位置、备份恢复、升级回滚与排障,全部收在 运维手册(面向部署者)。
这里只强调两点:首次启动生成的 dsh-ui-auth-bootstrap.txt 用完即删;
宿主缺少现代传输能力时插件会 fail-closed(面板不可访问),这是有意设计而非故障。
v0.7.0 起只支持 modern 传输线(DSH 0.2.0-rc.2 线),legacy 传输线已完全移除。
下表按"目标版本 / 中间版本 / legacy"三档给出实测结论(表格随每次实测更新):
| 档位 | DSH 版本 | 结论 | 说明 |
|---|---|---|---|
| 目标 | 0.2.0-rc.2 | ✅ compatible | 唯一的目标版本;启动即硬前置校验,端点面与页面均按此版本实测 |
| 中间 | 全部已实测:0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1(✅ 38/38);0.1.6-alpha.2(37/38)、0.1.5-rc.3、0.1.5-rc.2、0.1.5-rc.1(34/38,缺 0.2.0 独有端点,属能力收窄);0.1.6-alpha.1 ❌ 宿主自身无法启动(上游缺陷) | 见左 | 中间版本不会因为缺能力而放行:缺失现代传输能力即 fail-closed;端点缺失只表现为该页不可用(404) |
| legacy | 0.1.1-rc.2 | ❌ 已移除 | 缺失 connection.authorizeIndex 时fail-closed:认证网关不建立、面板不可访问并输出明确错误,绝不降级为无门放行 |
注意:宿主未提供现代传输能力时,插件不会退回旧传输线,而是以"明确拒绝"的方式停摆 (宁可不可访问,也不能无门暴露)。这也意味着升级到 v0.7.0 前请先确认 DSH 版本。
普通用户在 modern 线上的可达面同样按用户裁剪:模型与 API Key 各自独立(见
按用户隔离的模型与 API Key),不能创建 workspace(属部署方能力),
也不能写入任何设置命名空间;需要放开时可由宿主插件通过 uiAuth 接口登记策略。
端点清单、收紧项与验证证据见 docs/DSH-0.2.0-COMPATIBILITY.md。
忘记管理员密码怎么办?
删除 ~/.dsh/.credentials.yaml 中 dsh-auth/admin(及其对应的哈希记录),重启面板后会重新生成 admin
与新的随机密码(原账号的 TOTP、通行密钥与资料会丢失)。其他用户的密码可由管理员在用户管理中重置。
删掉的用户想用同一个名字重建,提示「用户名已存在」? 这是有意设计:被删除的用户名会写入永久墓碑,同名账号不能重建,避免新账号继承旧账号留下的 会话归属、邀请码记录等历史残留。请换一个用户名,或按上面的方法清空整张用户表后重新引导。
绑定的通行密钥设备丢了怎么办?
两个办法:① 若该账号还绑定了 TOTP,用「密码 + 动态码」登录后在【用户管理】里删除丢失的通行密钥;
② 请管理员在【用户管理】→ 对应用户行点「清除通行密钥」。管理员自己的通行密钥若全部丢失且未绑定 TOTP,
只能按上一条重置 admin 记录(这也是「两步验证开启时不允许删除最后一个通行密钥」的原因)。
通行密钥按钮不可用 / 提示改用 localhost?
浏览器不允许把 IP 地址当作通行密钥域。请用 http://localhost:3080 打开面板,或给面板配一个域名并走 HTTPS。
面板会直接给出应该使用的地址。
手机扫码绑定后,手机上也能直接登录面板吗?
可以。手机通行密钥是「可同步/混合」凭据:手机浏览器打开同一地址(必须同样是域名或 localhost)即可用
指纹/面容直接登录;无需电脑在场。
重启面板后需要重新登录吗? 不需要。未过期的会话会自动恢复;只有在从 0.5.1 之前的版本升级时,旧会话记录会失效一次。
普通用户看不到「模型」页,是故障吗? v0.7.0 起不是这样:每个用户在【设置】→【模型】里管理自己的模型与 API Key。 若你看到的是旧行为(提示"仅管理员可访问"),说明插件版本低于 0.7.0。
登录限流把所有人都算成同一个来源?
说明面板前面有反向代理。按上文开启 DSH_AUTH_TRUST_PROXY=1,限流将按真实客户端 IP 计数。
邀请码用完了 / 想关闭注册? 注册必须持有效邀请码,因此不生成邀请码即等于关闭注册;管理员可随时生成或撤销。
可以只要认证、不做数据隔离吗?
当前版本始终启用按用户隔离。需要自定义策略的部署可通过宿主插件调用 uiAuth 接口(见兼容性文档)。
源码为 TypeScript,构建产物一并提交(DSH 直接读取仓库,因此运行与安装不需要构建):
npm ci # 安装开发依赖
npm run typecheck # 宿主(tsconfig.json)与客户端(tsconfig.client.json)两套严格类型检查
npm run build # src/*.ts → lib/*.js,并构建客户端 bundle(内含契约自检)
npm test # 全量回归(构建 + 安全套件 + 策略/加解密 + 冒烟 + 登录页/端点)
npm run verify:clean # 校验 lib/ 与 src/ 一致(改源码后必须提交重新构建的产物)
常用的针对性检查:
| 命令 | 用途 |
|---|---|
npm run store:check | 商城契约门禁(依赖声明、权限信号、打包内容) |
npm run test:compat:0.2.0 | 真实 DSH 0.2.0 实例端到端(38 项) |
npm run test:compat:matrix | 多版本兼容矩阵(逐版本起一次性实例) |
npm run test:i18n:live | 语言切换端到端(en ↔ zh) |
npm run docs:matrix | 由安全套件的 JSON 结果重建安全报告 §2 |
模块职责与测试分层见 架构文档;开发环境、提交纪律与文案规范见贡献指南。 (README 不再重复模块表,避免两处漂移。)