Files
iAs/docs/SECURITY_BOUNDARIES.md
T
wunianxiao 142efb070f refactor(sandbox): 长期容器 + docker exec 替代一次性 docker run
容器策略从"每次 docker run 一次性容器"改为"长期容器 + docker exec":

- 统一产物目录 tools/bin/:build.sh 和 tool_manager 编译后复制产物到
  此目录,registry 解析路径优先用 tools/bin/<tool>。
- 长期容器 ias-sandbox:daemon 启动时 ensure(已运行则复用),
  tools/bin 只读挂载到 /tools,无网络/只读根/cap-drop ALL/资源限制。
- docker exec 调用:工具执行改为 docker exec -i ias-sandbox
  timeout -k 5 <secs> /tools/<name>,省去容器创建开销。
  timeout -k 5 确保超时 SIGTERM→SIGKILL,无孤儿进程。
- 新增 src/tools/sandbox.rs:ensure_container + build_exec_cmd。
- executor 删除 build_docker_cmd/sandbox_enabled,docker 分支改用
  sandbox::build_exec_cmd。
- env 按 spec 白名单 -e 注入(容器启动时仅有 PATH)。

验证:本地执行 hostname=debian,docker exec hostname=容器ID,
证明确实走长期容器 exec;25 测试通过。
2026-06-22 17:11:05 +08:00

187 lines
7.3 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.
# 两层安全边界设计
> 审批决定"能不能做"Docker 限制"做坏了能坏到哪里",审计回答"谁让系统新增或修改了什么能力"。
## 架构
```
LLM 调用工具
→ registry.dispatch(name, params, user_id, approved)
→ load spec + validate params
→ 【授权边界】if high_risk && !approved: 统一审批(不依赖消息类型)
→ 【隔离边界】按 sandbox.profile 选择执行方式:
- local : 本地 spawnenv_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 exec(隔离边界)
### 适用范围(按威胁模型精确化)
http/db 类工具的子进程是"瘦壳"——危险操作(联网、DB 读写)全在受信任的主进程里,
给它们套容器是纯开销,**没有隔离收益**。因此:
| 场景 | 是否容器隔离 | 原因 |
|------|------------|------|
| 开发者预写工具(weather/amap/... | ❌ 本地执行 | 子进程纯计算,危险操作在主进程 |
| tool_manager 产出的工具 | ✅ stdio 容器 | 含 LLM 提供的 rust_code,不受信任 |
| tool_manager 编译 | ✅ builder 容器 | cargo 执行任意 build script/proc macro |
### 统一产物目录
所有工具二进制统一收集到 `tools/bin/``build.sh` 编译后复制,`tool_manager` 编译后也复制)。
registry 解析路径时优先用 `tools/bin/<tool>`
### 长期容器模式
daemon 启动时创建常驻容器 `ias-sandbox``tools/bin/` 只读挂载到 `/tools`
```
docker run -d --rm --name ias-sandbox \
--network none --cap-drop ALL --security-opt no-new-privileges \
--read-only --memory 512m --cpus 1 --pids-limit 128 \
--mount type=bind,source=tools/bin,target=/tools,readonly \
--mount type=tmpfs,target=/tmp \
ias-stdio-runner sleep infinity
```
工具调用通过 `docker exec` 在容器内执行(省去每次 `docker run` 的创建开销):
```
docker exec -i -e KEY=VAL ias-sandbox timeout -k 5 <secs> /tools/<name> --command <cmd>
```
- 容器已运行则复用,daemon 重启不重建
- `timeout -k 5` 超时 SIGTERM、5s 后 SIGKILL,避免孤儿进程
- env 按 spec 白名单 `-e` 注入(容器启动时仅有 PATH
- 新工具由 tool_manager 编译后复制到 `tools/bin/`,bind mount 自动可见,无需重启容器
### 两类基础镜像
| 镜像 | 用途 | 网络 | 文件系统 |
|------|------|------|----------|
| `ias-stdio-runner` | tool_manager 产出的不受信任工具(长期容器) | none | 只读根 + tmpfs /tmp |
| `ias-tool-builder` | tool_manager 编译 Rust 工具(一次性容器) | none | 可写 /workbind 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 # 工具执行走长期容器 docker exec
export IAS_DOCKER_BUILDER=1 # tool_manager 编译走 builder 容器
```
未设置时退回本地执行(仍保留 env_clear + 白名单注入)。
daemon 启动时自动 ensure `ias-sandbox` 容器,失败则回退本地执行。
### 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` 据此判断是否 reloadtool_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` + 产物复制到 `tools/bin/` |
| datetime 本地执行 | +08:00(宿主时区,开发者工具默认不套容器) |
| tool_manager 产出工具 docker exec | 容器内 hostname=容器ID,证明确实走长期容器 exec |
| 容器常驻 | docker ps 可见 ias-sandboxexec 完无孤儿进程 |
| 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 | ✅ |