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中可调):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:- 校验会话(Cookie/Basic)与旧密码(错误计入防爆破);
- 生成新 scrypt 哈希,写回
patchFilePath(cordis.patch.yml,永续); - 通过 loader 热更新运行配置(
live-updated,无需重启面板); - 审计记录
password-change事件(含结果明细)。
- 退出登录:跳转
/panel-auth/logout,清除 Cookie 并回到登录页。 - 改密不影响已登录会话(签名密钥不变,Cookie 继续有效)。
部署要求(客户端包按裸包名解析):
# 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'
相关配置项:
changePasswordPath: '/panel-auth/change-password'
patchFilePath: '/root/.dsh/profiles/web/cordis.patch.yml'
修改密码(命令行)
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。
更换签名密钥
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— 生成密码哈希与随机密钥的 CLItest.mjs— 单元测试(node test.mjs,覆盖 15 组流程)
No comments yet. Be the first to write one.