Files
dsh 96b5fb6ed0 refactor: move logout/change-password UI from floating widget to settings
The injected floating buttons overlapped the mobile composer send button.
Replace the tapIndex-injected widget with a proper client plugin
(panel-auth-ui, ui/ package): registers an account & security section in
the panel settings via the settings.section slot, with change-password
form and logout action. Host plugin keeps only the /panel-auth/change-
password and /panel-auth/logout endpoints.

- ui/: ModuleLoader-format client bundle (dsh.client declaration,
  exports ./client and ./package.json), host no-op stub, bare-package
  symlink required in the profile node_modules (documented in README).
- Host: renderAuthWidget/injectWidget/tapIndex injection removed.
- Tests: widget tests removed; change-password flows unchanged.
2026-08-16 03:28:31 -04:00

159 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# panel-auth — DSH 面板访问密码插件
`dsh web` 面板加一层原生 HTTP 认证(自绘登录页 + Basic + 签名 Cookie),
挂载在 `/root/.dsh/profiles/web/cordis.patch.yml` 的用户补丁层,升级 DSH 不会丢。
## 认证逻辑
- 浏览器访问(`Accept: text/html`):匿名时返回**内置登录页**(无 JS、CSP 收紧),
表单 POST 到 `/panel-auth/login`,成功后签发 Cookie 并跳回原目标地址。
- API / 脚本(fetch、curl 等):保持经典 `401 + WWW-Authenticate: Basic` 契约,
仍可用 Basic 凭据直接调用,成功后同样签发 Cookie。
- WebSocket 升级:匿名一律 `401`,带 Cookie 放行(浏览器对 WS 不保证携带
Authorization 头,因此依赖 Cookie)。
- `/panel-auth/logout`:清除 Cookie 并回到登录页。
- Cookie 默认 30 天(`cookieTtlSeconds`),HttpOnly + SameSite=Lax。
- **Fail-open**`users` 为空或 `secret` 缺失/过短时不拦截任何请求,
配置写错不会把面板锁死。配置每次请求实时读取,改密码热生效。
## 登录日志(审计)
- 所有登录活动写入结构化 JSONL 文件,默认
`/root/.dsh/panel-auth-audit.jsonl`(可用 `auditLogPath` 配置修改)。
- 事件类型:
| event | 含义 |
| --- | --- |
| `login-ok` | 登录成功(含用户名、IP、User-Agent |
| `login-fail` | 登录失败(含用户名、IP、UA、`reason``bad-credentials` / `missing-fields` / `cross-origin` / `body-too-large` |
| `logout` | 主动登出 |
| `challenge` | 匿名浏览器导航被重定向到登录页 |
| `reject` | 匿名 API / WebSocket 请求被拒绝(含方法、路径) |
- 每条含 `ts`ISO 时间)、`ip`(优先 `X-Forwarded-For`,见下)、`ua`
- 文件超过 5MB 自动轮转为 `<path>.1`
- 查看示例:`tail -f /root/.dsh/panel-auth-audit.jsonl | jq .`
> IP 说明:面板经 Caddy 反代时,直连 socket 是回环地址;插件在检测到回环
> 来源时会取 `X-Forwarded-For` 的首个值作为真实 IP(Caddy 默认会带上该头)。
## 防爆破(默认开启)
- **按 IP 计数**:同一 IP 在 `failureWindowSeconds`(默认 300 秒)窗口内连续
`maxFailures`(默认 5)次密码错误后进入锁定期。
- **阶梯式锁定**:首次锁 `lockoutBaseSeconds`(默认 30 秒),再次触发翻倍,
上限 `lockoutMaxSeconds`(默认 3600 秒)。
- 锁定期内:登录页返回 `429 + Retry-After`"尝试次数过多"),API 返回
`429 {"error":"too many attempts"}`,且**不再执行 scrypt 校验**(不消耗 CPU)。
- **失败延迟**:每次密码错误额外等待 `failedLoginDelayMs`(默认 300ms),
拖慢单连接暴力尝试。
- **成功即清零**;锁定状态为进程内存态,面板重启后清空(重启面板需要
root,攻击者无法自行重置)。
- Basic 认证路径同样计入;IP 取自反代 `X-Forwarded-For` 的最后一跳
(Caddy 会覆写伪造值,见下)。
- 配置(`cordis.patch.yml``config` 中可调):
```yaml
bruteProtection: true # 关闭设为 false
maxFailures: 5
lockoutBaseSeconds: 30
lockoutMaxSeconds: 3600
failureWindowSeconds: 300
failedLoginDelayMs: 300
```
> 局限:分布式攻击(每尝试换一个 IP)不受单 IP 锁定约束;这由
> 300ms 失败延迟 + scrypt 慢哈希兜底。公网反代场景建议再配合 Caddy
> 层的 IP 白名单/云防火墙(如 Cloudflare)使用。
## 反向代理部署(重要)
DSH 上游将 `settings.*`、`credentials.*`、`agentPreset.*`、`host.pickDirectory`
等**特权 /api 方法锁定为仅回环(loopback)Host 可访问**(浏览器信任围栏的
设计:面板预期经 SSH 隧道访问)。用反向代理(如 Caddy)前置面板时,需把
Host 以回环形式转发给面板,否则这些方法返回 `403 forbidden`
```
dsh.example.com {
reverse_proxy 127.0.0.1:3080 {
header_up Host 127.0.0.1
header_up -Origin
}
}
```
- 为什么删 `Origin`:围栏同时校验 `Origin == Host`Host 呈回环时浏览器
携带的对外 Origin 必然不相等(全部 403)。删除后围栏改走
`Sec-Fetch-Site: cross-site` 检查(现代浏览器都发)拦截跨站,且所有请求
仍有 panel-auth 的密码/Cookie 层把关(跨站请求带不上 SameSite=Lax Cookie)。
- 认证不受影响:所有请求仍先过 panel-auth(密码/Cookie + 防爆破),
且 panel-auth 的 Cookie 是浏览器端存储,与 Host 头无关。
- panel-auth 的来源校验在 Host 为回环时自动跳过(代理场景);对外域名下的
真实跨站提交仍会被拒绝(`非法请求来源`)。
## 设置页「账号与安全」(登出 / 修改密码)
配套客户端插件 `panel-auth-ui`(本仓库 `ui/` 目录)在面板**设置**里注册
"账号与安全"区块,包含:
- **修改密码**:输入当前密码 + 新密码(≥8 位、不得与旧密码相同),
POST 到 `/panel-auth/change-password`
1. 校验会话(Cookie/Basic)与旧密码(错误计入防爆破);
2. 生成新 scrypt 哈希,**写回 `patchFilePath`**cordis.patch.yml,永续);
3. 通过 loader 热更新运行配置(`live-updated`,无需重启面板);
4. 审计记录 `password-change` 事件(含结果明细)。
- **退出登录**:跳转 `/panel-auth/logout`,清除 Cookie 并回到登录页。
- 改密不影响已登录会话(签名密钥不变,Cookie 继续有效)。
部署要求(客户端包按裸包名解析):
```bash
# 1. 把 ui/ 目录复制为 profile 内的独立包(host 存根 + client bundle
cp -r ui /root/.dsh/profiles/web/panel-auth-ui
# 2. 建立裸包名解析符号链接(profiles 级 node_modules 为 pnpm 布局)
ln -sfn ../web/panel-auth-ui /root/.dsh/profiles/node_modules/panel-auth-ui
# 3. cordis.patch.yml 增加行(dsh.client 由 package.json 声明驱动)
# - id: panel-auth-ui
# name: 'panel-auth-ui'
```
相关配置项:
```yaml
changePasswordPath: '/panel-auth/change-password'
patchFilePath: '/root/.dsh/profiles/web/cordis.patch.yml'
```
## 修改密码(命令行)
```bash
cd /root/.dsh/profiles/web/panel-auth
node hash.js admin 新密码 # 输出新 passwordHash
```
把输出的 `passwordHash` 替换进 `../cordis.patch.yml` 后(loader HMR 热应用;
如未生效则 `systemctl restart dsh-web`)。
## 新增用户
在 `cordis.patch.yml` 的 `users` 列表里再加一组 `username`/`passwordHash`。
## 更换签名密钥
```bash
node hash.js --secret # 生成新 secret
```
替换 `cordis.patch.yml` 的 `secret` 后重启。注意:更换密钥会使所有已签发
Cookie 立即失效,所有人需重新登录。
## 应急解锁(忘记密码时)
编辑 `/root/.dsh/profiles/web/cordis.patch.yml`
- 临时把 `users` 置为 `[]`(fail-open,面板恢复无密码状态),或
- 用上面的命令生成新哈希替换。
## 文件
- `index.js` — 插件本体(登录页、认证守卫、审计日志、webServer 包装)
- `crypto.js` — scrypt 哈希 / HMAC Cookie 签名(无依赖,纯 node:crypto
- `hash.js` — 生成密码哈希与随机密钥的 CLI
- `test.mjs` — 单元测试(`node test.mjs`,覆盖 15 组流程)