dsh凭证注入与认证机制:密码、Key、Nginx 三层防护
昨天修 dsh 图片输入报错的时候,顺带翻了一遍认证相关的代码。发现 dsh 的凭证管理其实有三层——浏览器 Basic Auth、进程环境变量、本地配置文件,每层各司其职。搞清楚了之后,整个 dsh 的安全边界就清晰了。
一、第一层:Nginx Basic Auth(外网访问网关)
dsh web 默认只监听 127.0.0.1:3080,不暴露到公网。公网访问必须经过 Nginx 反代,而 Nginx 配了 Basic Auth:
1 | server { |
关键点:
- 密码文件:
/www/server/panel/vhost/nginx/.htpasswd_dsh,用户名固定是dsh,密码是 BT Panel 生成的 apr1 hash - 忘记密码怎么办:重新生成 htpasswd 文件即可
1
2
3HASH=$(openssl passwd -apr1 "你的新密码")
echo "dsh:${HASH}" > /www/server/panel/vhost/nginx/.htpasswd_dsh
chmod 644 /www/server/panel/vhost/nginx/.htpasswd_dsh - 目录权限陷阱:
/www/server/panel/vhost/nginx/默认是 700,nginx worker(www用户)无法读取。需要:1
chmod 711 /www/server/panel/vhost /www/server/panel/vhost/nginx
- crypto.randomUUID polyfill:dsh 的前端用到了
crypto.randomUUID(),老版本浏览器不支持。Nginx 里配了一段 JS 注入:然后用1
2
3
4
5location = /dsh-polyfill.js {
default_type application/javascript;
charset utf-8;
return 200 'if(typeof crypto!==\"undefined\"&&!crypto.randomUUID){...}';
}sub_filter注入到所有 HTML 的</head>前面。
二、第二层:密码文件(dsh web 登录凭证)
Nginx 那层只是外网访问的门卫。dsh web 自己也有登录机制——存在 /root/dsh-web-password.txt 里:
1 | yoy-3081!dsh |
这个文件是什么?dsh 前端用这个用户名(dsh)+ 密码来构建 Basic Auth header,发给后端 API。我们的调试代码里就是这样用的:
1 | password = 'yoy-3081!dsh' |
所以实际上有两个密码:
- Nginx 的
.htpasswd_dsh:拦的是 HTTP 层,没通过直接 401 /root/dsh-web-password.txt:是 dsh 应用层认的,通过 Nginx 后还要对这个
两层密码可以不同,也可以相同。我的环境里恰好是一样的。
三、第三层:API Key 的环境变量注入
这是最关键的一层——dsh 怎么拿到 Sensenova / HCNSEC 的 API Key?
答案:进程环境变量,不是直接从配置文件读取。
配置文件里只写变量名
/root/.dsh/settings.yaml 里写的是:
1 | llm-pi-ai: |
注意是 apiKeyEnv,不是 apiKey。告诉 pi-ai:”去环境变量里找这个 key”。
实际的 Key 存在哪里?
两个地方:
1. 凭证文件(供 dsh 启动时注入)
/root/.dsh/.credentials.yaml:
1 | version: 1 |
2. 进程环境(实际运行时生效)
1 | # 查看 dsh 进程的完整环境变量 |
输出:
1 | SENSENOVA_API_KEY=sk-hhh...7yYs |
注意:SENSENOVA_API_KEY(没有下划线)和 settings.yaml 里写的 SENSE_NOVA_API_KEY(有下划线)不完全一致。这说明 dsh 在启动时对环境变量名做了规范化处理——可能是去掉了下划线,或者两边都支持。
为什么不用明文写在 settings.yaml 里?
安全考量:
- settings.yaml 可能进了 git(虽然 .gitignore 通常排除,但有人忘加)
- 环境变量不在文件系统上,进程退出后就不存在
- 可以用容器、systemd EnvironmentFile、或者启动脚本控制注入时机
四、Hermes Agent 和 dsh 的 Key 是两套
这里容易混淆:
| Hermes Agent | dsh | |
|---|---|---|
| 配置文件 | /root/.hermes/config.yaml |
/root/.dsh/settings.yaml |
| API Key 位置 | 直接写在 config 里 | 环境变量,从 .credentials.yaml 注入 |
| sensenova key | sk-HcU...8aG5 (hcnsec provider) |
sk-hhh...7yYs (sense-nova provider) |
两套 Key 不同,说明用了不同的账号或者不同的接入点。Hermes Agent 走的是 hcnsec 聚合代理,dsh 直接走 sensenova 官方接口。
五、启动参数里的 –trusted-host
1 | nohup dsh web --no-open --host 127.0.0.1 --port 3080 \ |
--trusted-host 的作用:告诉 dsh 哪些外部地址是可信的。这影响 WebSocket 连接和跨域策略。
如果不加这个参数,从公网 IP 访问时,前端会判定非回环地址,部分功能(比如 Settings 页面)会被禁用——这就是 skill 里提到的 “加载提供方目录失败” 问题。
六、完整的认证流程
用户从浏览器访问 http://43.153.148.187:3081 到发出第一个 API 请求,经历了这几步:
1 | 1. 浏览器输入 URL |
每一层都在验证身份,但目的不同:
- Nginx Basic Auth:防止未授权访问(运维安全)
- dsh 应用层 Auth:标识当前用户(功能隔离)
- 环境变量里的 API Key:标识对上游模型的访问权(计费/配额)
七、常见问题排查
问题1:忘记 dsh web 密码
1 | # 查看当前密码文件 |
问题2:Nginx 401 怎么重置
1 | # 生成新密码(会提示输入两次) |
问题3:API Key 失效了
1 | # 1. 更新凭证文件 |
写在最后
dsh 的安全设计是典型的”纵深防御”:Nginx 在外、应用 Auth 在中、API Key 在内。每一层解决不同维度的问题,组合起来既安全又灵活。
理解了这个分层,以后排查”认证相关”的问题就有方向了——先问自己:是哪个层面的认证出了问题?