# 06 安全:隔离、沙箱、文件访问策略、网络与凭证 ## 1. 威胁模型 调用方(自己的上层平台)可信,但 **Agent 的行为不完全可信**:它会执行命令、读写文件、访问网络,而且可能被网页或文档中的提示注入误导。要防的是 Agent 越界。 | 风险 | 例子 | |---|---| | R1 读取敏感文件 | 读 `~/.ssh`、CLI 登录凭证、个人文档、其他会话的工作区 | | R2 越界写入/破坏 | 删改工作区以外的文件 | | R3 访问局域网 | 访问路由器、NAS、内网服务的管理接口 | | R4 数据外传 | 把资料内容发到外部网站 | | R5 资源耗尽 | 死循环脚本、大文件把机器拖垮 | ## 2. 分层防御 ``` L3 应用层 文件访问策略校验(canUseTool)· 审批 · WebFetch 私网拦截 L2 引擎沙箱 Claude Bash 沙箱 · Codex 权限配置(bubblewrap) L1 systemd ProtectHome=tmpfs / ProtectSystem=strict / 资源上限 (L0 专用用户:暂不启用,见 §5.3) ``` ## 3. 引擎自带沙箱具体限制了什么 ### 3.1 Claude Code(`@anthropic-ai/sandbox-runtime`,Linux 上用 bubblewrap + socat) | 方面 | 默认行为 | 配置 | |---|---|---| | **作用范围** | **只作用于 Bash 等 shell 命令及其子进程** | — | | 写 | 工作目录、临时目录、`additionalDirectories` | `sandbox.filesystem.allowWrite / denyWrite` | | 读 | **默认能读机器上大部分路径,包括 `~/.ssh`** | `sandbox.filesystem.denyRead / allowRead`(范围更窄的规则优先) | | 网络 | 没有直连出口,经本机代理按域名白名单放行(默认为空) | `sandbox.network.allowedDomains / deniedDomains` | | 逃逸口 | Claude 可以申请 `dangerouslyDisableSandbox` 在沙箱外重试 | `allowUnsandboxedCommands: false` | | 不可用时 | 默认静默降级为无沙箱运行 | `failIfUnavailable: true` | **不在沙箱内**:Read/Edit/Write/Grep/Glob/WebFetch 只受权限规则约束(`denyRead` 拦不住 Read 工具);MCP、hooks 以完整权限运行。 ### 3.2 Codex(bubblewrap;权限配置 permission profiles) - 作用于 Codex 拉起的**所有命令**(Codex 读文件也是通过命令完成的)。 - 新版的**权限配置**按路径设置 `read` / `write` / `deny`,范围更窄的路径优先;同一路径上 `deny` 优先于 `write`,`write` 优先于 `read`。可以从 `":root" = "deny"` 开始做"默认拒绝"。 - 内置路径别名:`:minimal`(运行时必需的系统路径)、`:workspace_roots`、`:tmpdir`。 - 网络默认关闭;开启后可以用代理加域名规则过滤。 - 注意:使用权限配置时不要再设置 `sandbox_mode`,两者混用时 Codex 会退回旧的沙箱设置。 ### 3.3 共同缺口 - Claude 的文件工具不在沙箱内 → 需要 L3 校验。 - 两者都依赖 bubblewrap 创建 user namespace → systemd **不能**开启 `RestrictNamespaces`。 ## 4. 文件访问策略(读权限可在 Agent 配置中设定) **可以设定**,而且两个引擎都有对应的原生机制。读写范围作为 Profile 的一部分,在后台编辑。 ### 4.1 策略模型 ```ts interface FileAccessPolicy { mode: 'restricted' | 'broad' // restricted:只能读列出的路径(推荐);broad:全盘可读,只排除 deny read: PathRule[] // 额外可读路径:资料库引用 或 绝对路径 write: PathRule[] // 额外可写路径(少用;工作区始终可写) deny: string[] // 额外禁止路径/通配,如 "**/*.env" } type PathRule = { knowledgeRootId: string } | { path: string } ``` 最终生效的权限由三层合并而成,上层不能被下层放宽: | 层 | 内容 | 谁来设定 | |---|---|---| | ① 全局强制禁止(不可覆盖) | `~/.claude/.credentials.json`、`~/.claude.json`、`~/.codex/auth.json`、`~/.ssh`、`~/.gnupg`、`/srv/agent-server/data`、**其他会话的工作区** | 后台"设置 → 安全",可追加,不能删除内置项 | | ② Profile 策略 | 上面的 `FileAccessPolicy` | 后台"Profiles" | | ③ 基线(始终存在) | 当前工作区读写;Python 运行环境、技能目录只读;`:minimal` 系统路径只读 | 内置 | ### 4.2 映射到引擎 | 合并后的策略 | Claude | Codex | |---|---|---| | 可读路径 | `additionalDirectories`(资料库)+ `sandbox.filesystem.allowRead` + `canUseTool` 对 Read/Grep/Glob 的路径白名单校验 | 权限配置:`":root"="deny"`(restricted 模式)、`":minimal"="read"`、各可读路径 `="read"` | | 可写路径 | 工作区 + `sandbox.filesystem.allowWrite` + `canUseTool` 对 Edit/Write 的校验 | `":workspace_roots"`: `"."="write"` + 额外可写路径 | | 禁止路径 | `sandbox.filesystem.denyRead` + `permissions.deny: Read(...)` + `canUseTool` | 各禁止路径 `="deny"` | | restricted 模式下的 Bash | `denyRead` 覆盖 home 和 `/srv/agent-server` 等大块目录,再用 `allowRead` 重新开放工作区和可读路径 | 由 `":root"="deny"` 天然覆盖 | `canUseTool` 中的路径校验是 Claude 侧的**权威判断**:路径先 `realpath` 规范化(防止 `..` 和软链接绕过),再与合并后的策略比对。命中 deny 时直接拒绝,不在策略范围内的路径按配置拒绝或转人工审批。 ### 4.3 后台体验 - Profile 编辑页有一个"文件访问"区块:选择模式,从资料库列表勾选可读目录,添加自定义路径和禁止规则。 - 右侧显示"生效预览":合并三层之后的最终可读、可写、禁止清单。 - 保存时校验:路径是否存在、服务进程能否看到(见 §5.2 中 ProtectHome 的影响)、是否与全局禁止项冲突。 ## 5. 进程隔离(systemd) ### 5.1 现阶段:以 `jerry-remote` 身份运行,用 systemd 加固 CLI 登录态保存在你的 home 目录中,所以服务需要访问 `~/.claude`、`~/.claude.json`、`~/.codex` 和 CLI 安装目录。做法是先用 `ProtectHome=tmpfs` 把整个 home 隐藏起来,再只把需要的路径绑定回来: ```ini [Service] User=jerry-remote Group=jerry-remote WorkingDirectory=/srv/agent-server ExecStart=/usr/bin/node /srv/agent-server/app/apps/server/dist/main.js Environment=HOME=/home/jerry-remote # 文件系统:整盘只读,home 不可见,只回绑必要路径 ProtectSystem=strict ProtectHome=tmpfs BindPaths=/home/jerry-remote/.claude /home/jerry-remote/.claude.json /home/jerry-remote/.codex BindReadOnlyPaths=/home/jerry-remote/.local/bin /home/jerry-remote/.local/share/claude ReadWritePaths=/srv/agent-server PrivateTmp=yes # 内核与权限 NoNewPrivileges=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes LockPersonality=yes # RestrictNamespaces 不能开:bubblewrap 需要 user/mount/net namespace # 资源上限 MemoryMax=8G CPUQuota=600% TasksMax=512 ``` 效果:Agent 看不到你的 `~/web`、`~/.ssh`、`~/Documents` 等目录。CLI 凭证目录虽然对进程可见,但由 §4.1 第①层的全局强制禁止在引擎沙箱和应用层两处拦截。 ### 5.2 对资料库路径的影响 在 home 目录下的资料(例如 `~/Documents/资料`)默认对服务**不可见**。有两种处理方式: - **推荐**:把资料放到 home 以外(如 `/srv/agent-server/knowledge`、`/data/...`)。 - 或者在 unit 中追加一行 `BindReadOnlyPaths=`,然后重启服务。后台在保存资料库时会检测路径是否可见,不可见时直接给出需要追加的配置行。 ### 5.3 专用用户(暂不启用) 同 UID 运行时仍存在的缺口:服务可以向你的其他进程发信号(systemd 249 没有 `PrivatePIDs`);home 以外属于你的文件按权限仍可访问;加固配置出错时没有兜底。以后需要时,迁移步骤是:创建 `agentsvc` 用户 → 用该用户重新登录 CLI → 修改 unit 的 User 和绑定路径。服务代码不受影响,因为所有路径都从配置中读取。 ## 6. 网络策略 | 通道 | 控制方式 | |---|---| | Claude Bash 命令 | 沙箱代理 + 域名白名单(后台可配,默认只放行 pip 源) | | Claude WebFetch | 在 `canUseTool` 中解析域名,拒绝私网、回环和链路本地地址 | | Claude WebSearch / Codex web_search | 由厂商服务端执行,不经过本机网络 | | Codex 命令 | 权限配置中 `network.enabled=false`;需要时走审批 | ## 7. 凭证与合规 - **使用方式**:你在终端中自己登录 `claude` 和 `codex`,服务直接复用这份登录态,供你自己的平台使用。按 Anthropic 条款的划分,这属于订阅所有者本人的使用;不涉及他人使用你的订阅。 - **两点约束**: 1. Pro/Max 的额度"假设为普通的个人使用"。服务化之后,常驻和多平台的调用量会和你在终端里的使用**共享同一份额度**。 2. 以后如果开放给其他人使用,需要改用 API Key(凭证类型已预留,见 ADR-0008)。 - **实现要求**: - 服务进程环境中**不设置** `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`,避免意外切换到 API 计费。 - **不复制**凭证文件。ChatGPT 的 refresh token 会轮换,复制一份会导致你终端里的登录失效。 - Codex 使用默认的 `~/.codex`,因此会加载你个人 `config.toml` 中的插件和 MCP(当前有 browser、computer-use、node_repl 等)。app-server 启动时**必须用 `-c` 覆盖并禁用这些项**,只启用服务需要的配置。实现时用 `--strict-config` 加启动自检来验证。 - Claude 侧通过 `settingSources: []` 不加载个人的 settings、插件和 hooks。 - 后台"引擎状态"页只显示登录状态、账户摘要和额度,不显示 token。 - 条款参考:[Claude Code Legal and compliance](https://code.claude.com/docs/en/legal-and-compliance),2026-10 核对。本文不是法律意见。 ## 8. M1 实测补充(2026-10-22) - **Bash 沙箱禁止创建 Unix socket**(seccomp:`socket(AF_UNIX) = EPERM`),LibreOffice 因此无法在 Bash 中运行。 - **不采用** `sandbox.network.allowAllUnixSockets: true`:运行账号在 `docker`、`lxd` 组中,一旦放开,Agent 就能连接 `/var/run/docker.sock`,等同获得宿主 root 权限。 - 改为提供受控工具 `convert_document`(`services/office.ts`):服务进程自行用 bubblewrap 启动 LibreOffice,`--unshare-all`(断网、独立 IPC/PID)、私有 `/tmp` 和 `/run`、隐藏 home 和服务数据目录、只挂载当前工作区;输入和输出路径都必须在工作区内。 - **restricted 模式下 Bash 的读取范围**:沙箱只能整块屏蔽目录(home、`/srv`、`/mnt`、`/media`、`/root`),`/tmp`、`/etc` 等系统路径对 Bash 仍然可读。文件工具(Read/Grep/Glob)由 hook 严格限制在白名单内。这是已知、可接受的缺口;systemd 部署时 `PrivateTmp=yes` 会让 `/tmp` 成为服务私有。 - Claude CLI 为每个工作目录分配的临时目录(`/tmp/claude-/<工作目录转义>`)已加入可写基线,否则模型会在写临时脚本时被拦截。 - 建议(未实施):把运行账号移出 `docker` / `lxd` 组,或者尽早迁移到专用用户(§5.3)。这两个组本身就是本机上权限最大的入口。