092f3b78f2
http/db 类工具的子进程是瘦壳(纯计算),危险操作全在受信任主进程, 套容器是纯开销无隔离收益。调整为: - 开发者预写工具:移除 spec 的 sandbox: profile: stdio,回默认 local 本地执行(仍保留 env_clear + 白名单注入) - tool_manager 产出的工具:spec_yaml 自动写 sandbox: profile: stdio, 不受信任代码(LLM 提供 rust_code)默认进 stdio 容器 - tool_manager 编译:仍走 builder 容器 - 移除 ias-network-runner Dockerfile(当前无工具直接联网) - executor docker 分支仅保留 stdio/builder,删 network 分支 验证:datetime 本地执行 +08:00;tool_manager 产出工具 spec 含 sandbox: profile: stdio 且容器内执行成功;25 测试通过。
156 lines
5.9 KiB
Markdown
156 lines
5.9 KiB
Markdown
# 两层安全边界设计
|
||
|
||
> 审批决定"能不能做",Docker 限制"做坏了能坏到哪里",审计回答"谁让系统新增或修改了什么能力"。
|
||
|
||
## 架构
|
||
|
||
```
|
||
LLM 调用工具
|
||
→ registry.dispatch(name, params, user_id, approved)
|
||
→ load spec + validate params
|
||
→ 【授权边界】if high_risk && !approved: 统一审批(不依赖消息类型)
|
||
→ 【隔离边界】按 sandbox.profile 选择执行方式:
|
||
- local : 本地 spawn(env_clear + 白名单注入)
|
||
- stdio : docker run ias-stdio-runner (无网络、只读根)
|
||
- network : docker run ias-network-runner(联网、资源限制)
|
||
- builder : docker run ias-tool-builder (离线编译,仅 tool_manager)
|
||
→ stdin 传 envelope → stdout 读 JSON → 超时 kill → 容器销毁
|
||
```
|
||
|
||
## 第一层:统一工具级审批(授权边界)
|
||
|
||
**位置**:`src/tools/executor.rs` `execute_loop()` 入口
|
||
|
||
高风险工具在 **spawn 之前** 必须获得用户确认,与工具后续输出的消息类型(`result`/`http`/`db`)无关。
|
||
|
||
```rust
|
||
if spec.is_high_risk() && !approved {
|
||
// 发送确认码 → 等待用户回复 → Approved/Rejected/Expired
|
||
}
|
||
```
|
||
|
||
**修复的漏洞**:原实现审批只在 `http`/`db` 消息分支触发,`stdio → result` 可绕过。`tool_manager`(high risk)直接输出 result 即可零确认执行。
|
||
|
||
## 第二层:一次性 Docker 容器(隔离边界)
|
||
|
||
### 适用范围(按威胁模型精确化)
|
||
|
||
http/db 类工具的子进程是"瘦壳"——危险操作(联网、DB 读写)全在受信任的主进程里,
|
||
给它们套容器是纯开销,**没有隔离收益**。因此:
|
||
|
||
| 场景 | 是否容器隔离 | 原因 |
|
||
|------|------------|------|
|
||
| 开发者预写工具(weather/amap/...) | ❌ 本地执行 | 子进程纯计算,危险操作在主进程 |
|
||
| tool_manager 产出的工具 | ✅ stdio 容器 | 含 LLM 提供的 rust_code,不受信任 |
|
||
| tool_manager 编译 | ✅ builder 容器 | cargo 执行任意 build script/proc macro |
|
||
|
||
### 两类基础镜像
|
||
|
||
| 镜像 | 用途 | 网络 | 文件系统 |
|
||
|------|------|------|----------|
|
||
| `ias-stdio-runner` | tool_manager 产出的不受信任工具 | none | 只读根 + tmpfs /tmp |
|
||
| `ias-tool-builder` | tool_manager 编译 Rust 工具 | none | 可写 /work(bind mount) |
|
||
|
||
构建:`docker/build-images.sh`
|
||
|
||
### 容器安全基线
|
||
|
||
所有容器统一:
|
||
- `--rm` 一次性,用完销毁
|
||
- `--cap-drop ALL` 丢弃所有 Linux capability
|
||
- `--security-opt no-new-privileges` 禁止提权
|
||
- `--memory` / `--cpus` / `--pids-limit` 资源限制
|
||
- 只挂载工具二进制(readonly)+ tmpfs /tmp
|
||
- 不挂载项目根目录、不挂载 docker.sock
|
||
- env 按白名单注入(`env_clear` + spec.env)
|
||
|
||
### 沙箱配置(spec)
|
||
|
||
```yaml
|
||
sandbox:
|
||
profile: stdio # local | stdio | builder
|
||
network: none # none | default
|
||
memory: 256m
|
||
cpus: "0.5"
|
||
pids_limit: 64
|
||
read_only: true
|
||
```
|
||
|
||
默认 `profile: local`(不启用容器)。开发者预写工具保持默认。
|
||
**tool_manager 产出的工具自动写入 `sandbox: profile: stdio`**(见 tool_manager `spec_yaml`),
|
||
不受信任代码默认进容器。
|
||
|
||
### 启用沙箱
|
||
|
||
```bash
|
||
export IAS_DOCKER_SANDBOX=1 # 工具执行走一次性容器
|
||
export IAS_DOCKER_BUILDER=1 # tool_manager 编译走 builder 容器
|
||
```
|
||
|
||
未设置时退回本地执行(仍保留 env_clear + 白名单注入)。
|
||
|
||
### builder 容器特殊处理
|
||
|
||
`--cap-drop ALL` 丢掉 `CAP_DAC_OVERRIDE`,root 无法写 bind mount 的非 root 目录。
|
||
解法:builder 容器以宿主 `uid:gid` 运行(`--user`),自然能写自己的目录。
|
||
镜像内 `CARGO_HOME=/tmp/cargo` 预缓存 serde_json 并 `chmod 777`,任意 uid 可用。
|
||
编译加 `--offline`,确保 `--network none` 下不尝试联网。
|
||
|
||
## tool_manager 加固
|
||
|
||
| 措施 | 实现 |
|
||
|------|------|
|
||
| 禁止自改 | `RESERVED_NAMES` 包含 `tool_manager`,禁止 create/update/build |
|
||
| 强制 high risk | `spec_yaml` 忽略调用方 risk_level,一律写 `high` |
|
||
| 审计日志 | 每次调用追加 `tools/.tool_audit.log`(ts/uid/action/tool/result) |
|
||
| 构建超时 | `timeout 180s` 包裹 cargo build |
|
||
| 失败清理 | 构建失败删除残留旧二进制,避免 reload 后调用旧版本"假成功" |
|
||
| 离线编译 | `IAS_DOCKER_BUILDER=1` 时在 builder 容器内 `--offline` 编译 |
|
||
|
||
## 结构化结果
|
||
|
||
`executor` 返回 `ToolRunOutcome` 枚举替代裸字符串:
|
||
|
||
```rust
|
||
enum ToolRunOutcome {
|
||
Ok(String),
|
||
Err { kind: ToolErrorKind, message: String },
|
||
}
|
||
```
|
||
|
||
`daemon` 据此判断是否 reload(tool_manager 成功后重载注册表),
|
||
替代原 `is_tool_error()` 的中文前缀字符串匹配。
|
||
|
||
## 审批并发修复
|
||
|
||
`ApprovalManager::clean_expired()` 两阶段加锁:
|
||
收集过期 `code_hash` → 更新 DB → 重新加锁按 `code_hash` 定位删除。
|
||
不依赖快照 index,避免并发回复导致 index 错位误删未过期审批。
|
||
|
||
## 验证
|
||
|
||
| 场景 | 结果 |
|
||
|------|------|
|
||
| tool_manager update 自身 | ❌ 拒绝(保留名) |
|
||
| create_tool risk_level=low | spec 强制 high |
|
||
| tool_manager 产出工具 spec | 自动含 `sandbox: profile: stdio` |
|
||
| datetime 本地执行 | +08:00(宿主时区,默认不套容器) |
|
||
| tool_manager 产出工具 Docker 执行 | 容器内运行成功 |
|
||
| builder 容器编译 serde_json | ✅ 离线成功 |
|
||
| cargo test | 25 passed |
|
||
|
||
## 实施状态
|
||
|
||
| # | 步骤 | 状态 |
|
||
|---|------|------|
|
||
| 1 | 统一工具级审批 | ✅ |
|
||
| 2 | 禁止 tool_manager 自改 | ✅ |
|
||
| 3 | 强制生成工具 high risk | ✅ |
|
||
| 4 | env_clear 环境白名单 | ✅ |
|
||
| 5 | 修 clean_expired 并发 | ✅ |
|
||
| 6 | 三类 Docker 镜像 + sandbox spec | ✅ |
|
||
| 7 | stdio 工具迁移到一次性容器 | ✅ |
|
||
| 8 | network 工具迁移 | ➖ 不需要:工具走宿主代理联网,子进程纯计算,套容器无隔离收益 |
|
||
| 9 | tool_manager build 迁移到 builder 容器 | ✅ |
|
||
| 10 | 结构化结果替换 is_tool_error | ✅ |
|