239 lines
13 KiB
Markdown
239 lines
13 KiB
Markdown
# 14 · 实例会话敏感信息暴露面审计与加固
|
||||
|
|
|
|||
|
|
- 日期:2026-09-10
|
|||
|
|
- 触发:一份"dsh 实例会话读取是否会涉及服务关键信息泄密"的安全审计报告
|
|||
|
|
- 结论一句话:**跨租户/提权不成立(门户资产全部拒绝访问),真正的缺口是"实例出网完全不受限"+"云元数据端点可达"+"会话无保留期";而 `.credentials.yaml` 的暴露无法靠 dsh 机制消除,只能降值与观测。**
|
|||
|
|
- 状态:审计已完成 → **加固项待用户确认后执行**(见 §五)
|
|||
|
|
|
|||
|
|
> **TL;DR**|**结论**:实例会话敏感信息暴露面审计:**跨租户 / 提权不成立**(门户资产全部拒绝访问)。
|
|||
|
|
> **关键**:真缺口 = 实例**出网完全不受限** + 云元数据端点可达 + **会话无保留期**;`.credentials.yaml` 只能降值与观测。
|
|||
|
|
> **状态**:🔄 审计完成;加固项后续由档案 39 / 28 等落地
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、原审计报告要点(待核实)
|
|||
|
|
|
|||
|
|
报告给出了 dsh 实例内 agent 会话的可读清单、被拒清单,以及"读取 → 落盘 → 外传"三段风险链,并给出 5 条建议:
|
|||
|
|
|
|||
|
|
| # | 建议 | 本文判定 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 1 | 对话中不放真实密钥 | ✅ 成立(本次审计全程未输出任何密钥正文) |
|
|||
|
|
| 2 | 收紧沙箱出网,加白名单 | ⚠️ 方向成立,但**硬白名单会直接打断业务**,需分层 |
|
|||
|
|
| 3 | 会话记录保留期与清理 | ✅ 成立,当前**无保留期**,应立即做 |
|
|||
|
|
| 4 | `.credentials.yaml` 当密钥文件管理 | ✅ 成立,但**无法靠 dsh 机制隔离**(见 §四) |
|
|||
|
|
| 5 | 保持 `approval: ask` | ✅ 成立,且应**显式固定**,不依赖内置默认 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、实测核验(四轮只读审计,2026-09-10)
|
|||
|
|
|
|||
|
|
审计脚本:`scripts/audit-secret-exposure.sh` / `-2.sh` / `-3.sh`(工作区根目录)。
|
|||
|
|
**红线:脚本只取 stat 元数据与键名,严禁输出任何密钥正文。**
|
|||
|
|
|
|||
|
|
### 2.1 实例身份与自家目录可见性
|
|||
|
|
|
|||
|
|
| 项 | 实测结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| 实例进程 | `node /usr/local/bin/dsh --profile web --host 127.0.0.1 --port 41427`,运行 uid = `dsh-cce6d1cdb376430480f0` |
|
|||
|
|
| 家目录 | `drwx------` 属主即实例 uid(100002 / 114801) |
|
|||
|
|
| `.credentials.yaml` | **161 B,mode 600,属主 = 实例运行 uid** → agent 会话可读 |
|
|||
|
|
| 该文件结构 | `records["client-connection/browser-session"].payload.secret` = 43 字符 base64url |
|
|||
|
|
| **其性质** | **不是 LLM provider API Key**,是**浏览器 Web 客户端连接实例的会话密钥** |
|
|||
|
|
| `sessions/` | mode 700,2 个会话,488 K |
|
|||
|
|
| `storages/session_projcache/sessions/*.json` | mode 600,3 个,共约 14 K |
|
|||
|
|
| `settings.yaml` | mode 600,33 B,内容仅 `agent-presets: default: cordis` |
|
|||
|
|
|
|||
|
|
### 2.2 门户资产(越权/跨租户是否成立)
|
|||
|
|
|
|||
|
|
以实例 uid 实测 `test -r`:
|
|||
|
|
|
|||
|
|
| 路径 | 结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| `/opt/dshs` | ✅ 拒绝 |
|
|||
|
|
| `/opt/dsh` | ✅ 拒绝 |
|
|||
|
|
| `/var/lib/dshs` | ✅ 拒绝 |
|
|||
|
|
| `/var/lib/dshs/dshs.db`(mode 600 root) | ✅ 拒绝 |
|
|||
|
|
| `/etc/shadow` | ✅ 拒绝 |
|
|||
|
|
| `/root` | ✅ 拒绝 |
|
|||
|
|
| `/home/admin`、`/home/www` | ✅ 拒绝 |
|
|||
|
|
| `/home`(仅列目录名) | ⚠️ 可列(泄漏两个账号名,信息量极小,可接受) |
|
|||
|
|
|
|||
|
|
> **账户隔离(ISOLATION=account + setpriv)有效,跨租户与提权路径均不成立。**
|
|||
|
|
|
|||
|
|
### 2.3 出网能力(关键缺口)
|
|||
|
|
|
|||
|
|
以实例 uid(`setpriv --reuid`)实测:
|
|||
|
|
|
|||
|
|
| 目标 | 结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| `https://www.baidu.com` | **200** |
|
|||
|
|
| `https://httpbin.org/get` | **200** |
|
|||
|
|
| `https://api.deepseek.com` | 401(可达) |
|
|||
|
|
| DNS 解析 | ✅ 可用 |
|
|||
|
|
| `iptables -P OUTPUT` | **ACCEPT,无任何针对 dsh uid 的限制** |
|
|||
|
|
|
|||
|
|
### 2.4 云元数据端点(本报告最高危项,实测降级)
|
|||
|
|
|
|||
|
|
| 端点 | 结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| `http://169.254.169.254/...` | 不可达 |
|
|||
|
|
| `http://metadata.google.internal/...` | 不可达 |
|
|||
|
|
| **`http://100.100.100.200/latest/meta-data/`** | **200 可达** |
|
|||
|
|
|
|||
|
|
进一步只取**目录列表**(不取任何凭据正文):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
disks/ dns-conf/ eipv4 hibernation/ hostname image-id image/ instance-id
|
|||
|
|
instance/ mac network-type network/ ntp-conf/ owner-account-id private-ipv4
|
|||
|
|
region-id serial-number source-address sub-private-ipv4-list vpc-cidr-block
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **无 `ram/` 目录 → 该 ECS 未挂载 RAM 角色 → 无 STS 临时凭据可窃取。**
|
|||
|
|
> 因此从"云凭据失窃(P0)"降级为"**云上资产画像 + 内网拓扑泄露(P2)**":
|
|||
|
|
> 可读取 `instance-id` / `owner-account-id` / `vpc-cidr-block` / `private-ipv4` / `region-id` / `serial-number`。
|
|||
|
|
|
|||
|
|
### 2.5 dsh 沙箱能力边界(决定哪些加固可行)
|
|||
|
|
|
|||
|
|
只读查阅官方包 `@deepseek-ai/[email protected]`:
|
|||
|
|
|
|||
|
|
| 能力 | 实测 |
|
|||
|
|
|---|---|
|
|||
|
|
| `dsh-sandbox-policy` 模式 | 仅 `read-only` / `workspace-write` / `danger-full-access` 三档 |
|
|||
|
|
| **约束对象** | **只约束"文件修改",不约束读取**(README 原文:`cannot modify files` / `may modify files under ... workspaceRoot`) |
|
|||
|
|
| **网络/出网控制** | **完全没有**(包内无 `network` / `egress` 相关实现) |
|
|||
|
|
| `dsh-user-approval` | 仅 `ask` / `never` 两档 |
|
|||
|
|
| `dsh-permission-presets` | 把 sandbox + approval 捆绑为命名预设,默认提供 `workspace-write`(sandbox=workspace-write, approval=ask) 与 `danger-full-access`(…, never) |
|
|||
|
|
| 当前实例配置 | `profiles/web/cordis.yml` 与 `cordis.patch.yml` **均为 `[]`**,未配置 permission-presets → 走内置默认(schema `.default("read-only")`) |
|
|||
|
|
|
|||
|
|
> **结论:dsh 沙箱解决不了"读",也解决不了"外传"。这两段只能在 OS 层(门户/防火墙)处理。**
|
|||
|
|
|
|||
|
|
### 2.6 uid 段(防火墙规则用)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
hashUid(userId, baseUid) = baseUid + (hash % 100000) // 存量用户
|
|||
|
|
新用户 uid = baseUid + row_id // DB 自增,无冲突
|
|||
|
|
观测值:100002、114801
|
|||
|
|
→ 统一落在 [baseUid, baseUid + 99999]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.7 规模
|
|||
|
|
|
|||
|
|
- 用户数:2
|
|||
|
|
- `/var/lib/dshs` 占用:17 M
|
|||
|
|
- 磁盘:40 G 用 8.5 G(23%)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、风险定性(对照"读 → 落盘 → 外传"三段链)
|
|||
|
|
|
|||
|
|
| 段 | 现状 | 可控性 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **读** | agent 以自身 uid 读自家 `.credentials.yaml` / 会话记录,属"自己读自己",非越权 | ❌ dsh 沙箱不支持读限制;OS 层也无法区分"dsh 主程序读"与"agent 工具读"(同 uid) |
|
|||
|
|
| **落盘** | 默认 sandbox mode = read-only,写入已被限制;但未显式固定,存在内置默认漂移风险 | ✅ 可显式固定 preset |
|
|||
|
|
| **外传** | **完全开放**,且元数据端点可达 | ✅ **唯一真正有效的控制点** |
|
|||
|
|
|
|||
|
|
**因此加固重心应放在"外传"段,其次"降值"(让读到的东西不值钱),"读"段只能接受。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、`.credentials.yaml` 为什么不能简单隔离
|
|||
|
|
|
|||
|
|
1. 该密钥是**浏览器 Web 客户端连接实例的会话密钥**——实例进程要**写**,浏览器要**读**。
|
|||
|
|
2. 实例进程与 agent 工具调用**同 uid**,OS 层无法区分。
|
|||
|
|
3. dsh 沙箱不提供"读路径 deny"。
|
|||
|
|
4. 真正隔离需要改 dsh 主程序(**红线 R2 禁止**)或把 credentials 搬到 root-only 路径 + dsh 侧适配(同样需改官方行为)。
|
|||
|
|
|
|||
|
|
→ **降级处理**:承认其可被会话读取,改为**降值 + 观测**(短生命周期 / 可轮换 / 读行为留痕),并在文档中标注"任何时刻不要把该密钥或等价物贴进对话"。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、加固方案对比(待确认)
|
|||
|
|
|
|||
|
|
| # | 措施 | 收益 | 代价 / 风险 | 可逆性 | 建议 |
|
|||
|
|
|---|---|---|---|---|---|
|
|||
|
|
| **H1** | **封禁元数据端点**:nft `meta skuid <段>` + `ip daddr 100.100.100.200` → reject | 消除云上资产画像与内网拓扑泄露 | 极低(仅 dsh uid 段,root 与云监控不受影响) | 一条命令删除规则 | ✅ **立即执行** |
|
|||
|
|
| **H2** | **显式固定 permission preset**(`workspace-write` + `approval: ask`),走官方 cordis patch 机制 | 消除"内置默认漂移";不碰红线 R2 | 低(官方机制,需重启实例生效) | 改回 patch 即可 | ✅ **立即执行** |
|
|||
|
|
| **H3** | **会话保留期 GC**:`sessions/` 与 `session_projcache/` 超 N 天清理(默认 30 天,首跑 dry-run) | 收敛"落盘"段的存量 | 用户会丢失 N 天前的聊天记录 | 脚本可停;删除不可恢复 | ✅ 执行(**需你确认保留天数**) |
|
|||
|
|
| **H4** | **出网观测**:记录实例外联目标域名(先记录不拦截) | 为后续白名单提供事实依据 | 低 | 停服务即可 | ✅ **立即执行** |
|
|||
|
|
| **H5** | **出网严格白名单**:仅放行 LLM API、npm/pip 等 | 真正切断外传 | **高**:用户自有服务、各类 API 会被打断,需长期维护白名单 | 可回滚 | ❌ **暂缓**,等 H4 观测数据出来再评估 |
|
|||
|
|
| **H6** | `.credentials.yaml` 隔离 | — | **技术上不可行**(§四) | — | ⬇️ 降级为"降值 + 文档标注" |
|
|||
|
|
|
|||
|
|
### H1 实施要点(两个必踩坑)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 坑 1:阿里云 DNS 是 100.100.2.136 / 100.100.2.138,绝不能把 100.100.0.0/16 整段封掉
|
|||
|
|
# —— 只封元数据端点的 /32:100.100.100.200
|
|||
|
|
# 坑 2:Docker 用 iptables-nft,别往它的链里塞;单独建 nft table 最干净
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
建议落地形式:`/etc/nftables-dsh-egress.nft` + systemd oneshot 服务(持久化,重启不丢)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、执行记录(2026-09-10 12:03)
|
|||
|
|
|
|||
|
|
已执行 **H1 + H4**(用户确认范围);H2 经核实转为待确认;H3 用户未选、暂缓。
|
|||
|
|
|
|||
|
|
### H1 + H4 · 出网护栏(已完成并验证)
|
|||
|
|
|
|||
|
|
落地物:
|
|||
|
|
|
|||
|
|
| 文件 | 作用 |
|
|||
|
|
|---|---|
|
|||
|
|
| `/etc/nftables-dsh-egress.nft` | 规则源文件(独立 `table ip dsh_egress`,不碰 Docker 链) |
|
|||
|
|
| `/etc/systemd/system/dsh-egress.service` | oneshot 单元,开机自动加载 |
|
|||
|
|
| `scripts/harden-h1-h4-egress.sh`(本机) | 一键复现脚本(幂等) |
|
|||
|
|
|
|||
|
|
规则(只作用于 uid 100000–199999):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# H1 · 阻断云元数据端点(先记录再拒绝)
|
|||
|
|
meta skuid 100000-199999 ip daddr 100.100.100.200 log prefix "dsh-egress-BLOCK " level warn
|
|||
|
|
meta skuid 100000-199999 ip daddr 100.100.100.200 counter reject
|
|||
|
|
|
|||
|
|
# H4 · 观测实例新建外联(先记录不拦截;排除 loopback 与 DNS 降噪)
|
|||
|
|
meta skuid 100000-199999 ip daddr != 127.0.0.0/8 tcp dport != 53 ct state new counter log prefix "dsh-egress " level info
|
|||
|
|
meta skuid 100000-199999 ip daddr != 127.0.0.0/8 udp dport != 53 ct state new counter log prefix "dsh-egress " level info
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
验证结果:
|
|||
|
|
|
|||
|
|
| 检查 | 结果 |
|
|||
|
|
|---|---|
|
|||
|
|
| 实例 uid → `100.100.100.200` | **HTTP 000(阻断)**;reject 计数器命中 2 次,BLOCK 日志 2 条 |
|
|||
|
|
| 实例 uid → `https://www.baidu.com` | HTTP 200(业务不受影响) |
|
|||
|
|
| 实例 uid DNS 解析 | ✅ 正常(`100.100.2.136` 未被误封) |
|
|||
|
|
| root → `100.100.100.200` | HTTP 200(云监控/运维不受影响) |
|
|||
|
|
| 门户 `127.0.0.1:3080` | HTTP 200 |
|
|||
|
|
| 降噪效果 | DNS 日志行 0 条(原占绝大多数) |
|
|||
|
|
| systemd | `enabled` + `active`;stop→start 幂等,表可撤可恢复 |
|
|||
|
|
|
|||
|
|
**踩坑记录(已固化进脚本注释)**:
|
|||
|
|
|
|||
|
|
1. 阿里云 DNS 是 `100.100.2.136` / `100.100.2.138`,**绝不能封 `100.100.0.0/16` 整段**——只精确封元数据端点 `/32`。
|
|||
|
|
2. Docker 用 iptables-nft,**不要往它的链里塞规则**,独立建 nft 表最干净。
|
|||
|
|
3. `nft -f` 遇已存在的表会报 `File exists` → 单元必须加 `ExecStartPre=-nft delete table ip dsh_egress`(`-` 忽略失败)才幂等。
|
|||
|
|
4. 观测要排除 53 端口:nft 日志**不记录查询域名**,DNS 行是纯噪音且量最大。
|
|||
|
|
|
|||
|
|
### H2 · 固定 permission preset(改为待确认)
|
|||
|
|
|
|||
|
|
核实结果:
|
|||
|
|
|
|||
|
|
- `@deepseek-ai/dsh-permission-presets` **已在 `@deepseek-ai/dsh-base` 的默认 bundle 清单中**(host 侧),客户端侧 `@deepseek-ai/dsh-client-ui-permission-presets` 在 `dsh-web-app` 中 → **插件已随默认 bundle 加载**。
|
|||
|
|
- 当前 `profiles/web/cordis.patch.yml` 内容为默认空(`[]`),未覆盖;官方包逻辑为 `defaultPreset = config.defaultPreset ?? inferredDefault`。
|
|||
|
|
- 即:**当前生效值走官方推断默认,从服务端无法直接读出**(客户端 Web UI 有 Permissions 选择器可直接查看)。
|
|||
|
|
|
|||
|
|
未执行的理由:注入 `cordis.patch.yml` 需**重启用户实例**才生效,会打断当前会话;且可能与 `ensure-role-profile-patch.cjs`(按角色隐藏「模型」分区)的管理标记冲突。
|
|||
|
|
|
|||
|
|
→ 处理方式:先由用户在 dsh Web UI 的 Permissions 选择器确认当前预设;若不是 `workspace-write + ask`,再备份 `cordis.patch.yml` → 注入 → 重启实例 → 验证。
|
|||
|
|
|
|||
|
|
### H3 · 会话保留期(用户未选,暂缓)
|
|||
|
|
|
|||
|
|
触发条件:用户数 ≥ 5 或 `sessions/` 总量 > 1 GB 时再启用。
|
|||
|
|
|
|||
|
|
## 七、回滚
|
|||
|
|
|
|||
|
|
| 项 | 回滚方式 |
|
|||
|
|
|---|---|
|
|||
|
|
| H1 + H4 | `systemctl disable --now dsh-egress`,或 `nft delete table ip dsh_egress` |
|
|||
|
|
| H2 | 备份还原 `profiles/web/cordis.patch.yml` 并重启实例 |
|
|||
|
|
| H3 | 停用 timer;**已删除数据不可恢复**,故首跑必须 dry-run |
|