Files
panel-auth/README.md
T
dsh 8823049b66 feat: brute-force protection with per-IP escalating lockout
- Failures counted per client IP (X-Forwarded-For last hop behind the
  proxy); after maxFailures within the window the IP is locked out,
  doubling per repeat up to lockoutMaxSeconds.
- Locked IPs get 429 + Retry-After (login page / JSON for API), and the
  scrypt verification is skipped entirely while locked (no CPU burn).
- Fixed failedLoginDelayMs delay on every bad credential attempt.
- Basic-auth path counts and clears identically; success resets the IP.
- All thresholds configurable; in-memory state only.
- Tests: lockout, expiry restore, basic-path counting, XFF last-hop key.
2026-08-16 01:48:36 -04:00

101 lines
4.6 KiB
Markdown
Raw 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)使用。
## 修改密码
```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 组流程)