forked from dsh/panel-auth
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.
159 lines
7.2 KiB
Markdown
159 lines
7.2 KiB
Markdown
# 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 组流程)
|