stevenwilliamson/sandboxed-copilot

GitHub: stevenwilliamson/sandboxed-copilot

一个通过沙箱化代理隔离 Copilot 流量的安全方案,防范提示注入与数据外泄。

Stars: 1 | Forks: 0

# sandboxed-copilot [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) 主要目标是防御**间接提示注入**和**异常 AI 行为**:如果 Copilot 被操纵试图窃取数据或访问意外目的地,网络防火墙会阻止它。 无需针对每个项目进行设置 — 只需 `cd` 到任意目录并运行 `sandboxed-copilot`。 ![proxy monitor showing allowed traffic in green, denied traffic in red, and an exfiltration attempt flagged as EXFIL](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/7c6636bed9004517.png) ## 工作原理 两个容器通过 Docker Compose 一起运行: ``` Internet ←→ [ external network ] ←→ proxy (Squid) ←→ [ internal network ] ←→ copilot ``` | 容器 | 角色 | |-----------|------| | **copilot** | Ubuntu 24.04,运行 `gh` CLI + Copilot 扩展 + `mise` + Ruby + Python + Node.js。无直接互联网路由 — 所有流量均通过代理。 | | **proxy** | Squid 正向代理。读取 `allowlist.txt`(从主机以只读方式挂载)并拒绝所有不在列表中的域名。变更后 5 秒内自动重载。 | 代理无法修改允许列表 — 它以只读方式挂载到代理容器,且 copilot 容器没有访问代理配置文件的路径。 ## 要求 - [Docker Desktop](https://www.docker.com/products/docker-desktop/)(或 Docker Engine + Compose 插件) - 拥有 Copilot 访问权限的 GitHub 账户 - 主机上已通过 `gh auth login` 认证的 `gh` CLI,**或** 设置 `GITHUB_TOKEN` 环境变量 ## 安装 ``` git clone https://github.com/stevenwilliamson/copilot-sandbox.git cd copilot-sandbox bash install.sh ``` 安装程序会: 1. 将 Docker 资源复制到 `~/.sandboxed-copilot/` 2. 写入默认的 `allowlist.txt`(保留任何现有自定义项 — 同时会写入 `.new` 文件以便与上游变更进行 diff 和合并) 3. 将 `sandboxed-copilot` 启动器安装到 `~/.local/bin/` 4. 构建两个 Docker 镜像(首次运行约需 5 分钟;后续运行很快) 5. 可选地为 bash、zsh 或 fish 设置 shell 补全 如果 `~/.local/bin` 不在 `PATH` 中,安装程序会明确告知需要添加的内容。 ### 保持更新 ``` sandboxed-copilot update ``` 从源目录重建两个镜像,拉取最新的基础镜像以获取最新的 Ubuntu 安全补丁,并自动重新安装启动器二进制文件。 ## 使用 ``` cd /your/project sandboxed-copilot # open an interactive shell sandboxed-copilot gh copilot suggest … # run a Copilot command directly sandboxed-copilot -- bash -c "npm test" # run any command in the sandbox ``` 当前目录会被挂载到容器内的 `/workspace`。 认证信息会自动从主机上的 `gh auth login` 获取 — 无需手动导出令牌。如果希望在 CI 中显式传递(例如): ``` GITHUB_TOKEN=ghp_... sandboxed-copilot ``` ### 欢迎横幅 每次交互式会话都会显示一个横幅,展示工作区、工具版本、认证状态和代理状态: ``` --- sandboxed-copilot -------------------------------------------------- Workspace /workspace Tools ruby 3.2.x python 3.13.1 node 22.14.0 mise Auth ✓ Authenticated as @yourusername Proxy active (from host: sandboxed-copilot proxy status) ------------------------------------------------------------------------ Type 'exit' or Ctrl-D to return to your host shell. ``` ### Shell 历史记录 Bash 历史记录会在容器重启后通过 Docker 卷(`shell-history`)持久化,并在每次命令执行后写入,因此即使崩溃或执行 `docker kill` 也不会丢失。 ## Shell 补全 为所有子命令启用 Tab 补全: ``` # bash source <(sandboxed-copilot completion bash) # zsh sandboxed-copilot completion zsh > "${fpath[1]}/_sandboxed-copilot" # fish sandboxed-copilot completion fish > ~/.config/fish/completions/sandboxed-copilot.fish ``` `install.sh` 会自动为当前 Shell 配置补全。 ## 每个项目的配置 在项目根目录创建 `.sandboxed-copilot` 文件并提交,以便整个团队受益: ``` # .sandboxed-copilot [allowlist] # 仅为此项目允许的额外域名。 # 会话期间与全局允许列表合并。 .npmjs.com api.my-internal-service.example [env] # 要转发到容器的主机环境变量。 NPM_TOKEN # forwards the current value of NPM_TOKEN from your shell DATABASE_URL=sqlite # sets a literal value ``` - `[allowlist]` — 仅在会话期间有效;退出时会被清除,因此不会泄漏到其他项目。 - `[env]` — 作为 `-e NAME=VALUE` 转发给 `docker compose run`。 找到配置文件时,启动器会打印 `loaded project config (.sandboxed-copilot)`。 ## 管理允许列表 全局允许列表位于 `~/.sandboxed-copilot/config/allowlist.txt`。代理在变更后 **5 秒**内自动重载 — 无需重启。 ### 查看被阻止的内容 ``` # 显示当前会话中阻止的域名 sandboxed-copilot proxy denied # 所有会话的完整历史记录 sandboxed-copilot proxy denied --all ``` ### 交互式添加域名 ``` # 添加单个域名(提示用户或项目范围) sandboxed-copilot proxy allowlist api.example.com # 将阻止的域名直接通过允许列表向导传递 sandboxed-copilot proxy denied | sandboxed-copilot proxy allowlist ``` 向导会询问每个域名应添加到 **用户允许列表**(`~/.sandboxed-copilot/config/allowlist.txt`)还是 **项目允许列表**(当前目录下的 `.sandboxed-copilot`)。 ### 默认允许列表 以下域名默认启用: | 用途 | 域名 | |---------|---------| | GitHub + Copilot | `.github.com`, `.githubusercontent.com`, `.githubcopilot.com`, `default.exp-tas.com` | | mise | `mise.jdx.dev`, `mise.run` | | Node.js(预安装) | `nodejs.org`, `.npmjs.com`, `.npmjs.org` | | Ruby(系统;通过 `gem install` 或可选的 `mise use ruby@`) | `cache.ruby-lang.org`, `.rubygems.org` | | Python(预安装) | `.pypi.org`, `files.pythonhosted.org` | ### 常用补充 | 用途 | 要添加的域名 | |---------|---------------| | Yarn | `.yarnpkg.com` | | Go 模块 | `proxy.golang.org`, `sum.golang.org` | | Docker Hub | `.docker.com`, `.docker.io` | | GitHub Packages | `.pkg.github.com` | ## 代理模式 可以从主机临时打开或锁定代理 — 变更在 5 秒内生效: | 命令 | 效果 | |---------|--------| | `sandboxed-copilot proxy status` | 显示当前模式和剩余时间 | | `sandboxed-copilot proxy allow-all [mins]` | 开放所有出站流量 *mins* 分钟(默认 30),然后自动恢复 | | `sandboxed-copilot proxy lock` | 限制为仅 Copilot CLI 所需的最小域名 | | `sandboxed-copilot proxy reset` | 恢复正常的用户允许列表 | **示例 — 安装新 npm 包,然后重新锁定:** ``` sandboxed-copilot proxy allow-all 10 # open for 10 minutes sandboxed-copilot # enter the container npm install express # install freely exit sandboxed-copilot proxy reset # restore allowlist immediately ``` `allow-all` 计时器在代理容器内运行,即使关闭终端也会自动到期。 ## 代理监控 实时监控所有活跃沙箱会话中的代理流量: ``` sandboxed-copilot proxy monitor ``` 输出使用颜色编码 — 绿色表示允许的连接,红色表示被拒绝的连接: ``` TIME SESSION METHOD DOMAIN STATUS ──────────────────────────────────────────────────────────────────────────── 14:03:21 12345678 CONNECT api.github.com ✓ allowed 14:03:22 12345678 CONNECT registry.npmjs.com ✗ DENIED 14:03:24 87654321 CONNECT copilot-proxy.githubusercontent.com ✓ allowed ``` 按 Ctrl-C 停止。适用于多个并发的沙箱会话。 ## 预安装的运行时 [mise](https://mise.jdx.dev) 在系统范围内可用。以下运行时已预安装并可直接使用,无需额外设置: | 运行时 | 命令 | |---------|---------| | **Ruby**(Ubuntu 24.04 系统包) | `ruby`, `gem`, `bundle`, `irb` | | **Python**(最新版,通过 mise) | `python`, `pip`, `python3` | | **Node.js**(LTS,通过 mise) | `node`, `npm`, `npx` | ``` gem install bundler # works out of the box pip install requests # works out of the box npm install # works out of the box mise use ruby@latest # upgrade Ruby to a newer version (compiles from source) mise use go@latest # install additional runtimes on demand mise install # read from .mise.toml / .tool-versions in /workspace ``` ## Git 身份 主机上的 `~/.gitconfig`(以及存在的 `~/.config/git/config`)会以只读方式挂载到容器,因此 Copilot 提交的每一次提交都会使用你的姓名和邮箱。`safe.directory=/workspace` 会自动设置以避免所有权不匹配错误。 ## 安全模型 ### 保护范围 | 控制 | 保护 | |---------|-----------| | Squid 允许列表(默认拒绝全部) | 出站 HTTP/HTTPS 仅限明确列出的域名 | | SSL 剥离(TLS 检测) | 代理终止并检查所有 HTTPS 流量,使用每安装生成的 CA 证书 — 日志中可见完整 URL 和请求详情,而不仅是 CONNECT 主机名 | | **GitHub 令牌外泄检测** | 代理在所有出站请求中扫描 GitHub 令牌模式(`ghp_`、`gho_`、`ghs_`、`ghu_`、`github_pat_`),并阻止任何向非 GitHub 目的地的令牌携带请求。覆盖授权头(Squid ACL)、URL(Squid ACL)和请求体(Go ICAP 扫描器)。在所有代理模式下均处于激活状态。请注意,这无法防御刻意的令牌,但能检测并阻止大多数意外泄露。 | | **GitHub API 端点阻止** | ICAP 扫描器始终阻止 `POST /user/repos` 和 `POST /orgs/*/repos` 到 `api.github.com`,以及默认情况下阻止 `POST /repos/*/releases`。`uploads.github.com` 在普通和锁定模式下被拒绝。这可防止 Shai-Hulud 类的供应链外泄攻击,利用 GitHub 自身基础设施作为外泄通道。 | | CONNECT 限制为 443 | 防止通过允许的域名进行 SSH 或其他非 HTTPS 隧道 | | `Safe_ports` ACL 在 Squid 中 | 在所有代理模式下阻止对非常规端口的纯 HTTP 请求 | | `forwarded_for off` + `via off` 在 Squid 中 | 剥离会泄露容器内部 IP 和 Squid 版本的标头 | | `internal: true` Docker 网络 | 无 IPv4 路由到互联网 — 流量必须经过代理 | | 代理容器中禁用 IPv6 | 防止通过 IPv6 绕过 `HTTP_PROXY` 拦截 | | 配置目录在代理容器中挂载为 `:ro` | 代理无法修改其允许列表或代理模式 | | 未挂载 Docker 套接字 | 代理无法逃逸到主机 Docker 守护进程 | | `cap_drop: ALL` | 丢弃容器中所有的 Linux 能力。即使以 root 运行,也无法挂载文件系统、加载内核模块、创建设备节点或操作命名空间 — 这些是已知容器逃逸技术所需的操作。 | | 自定义 seccomp 配置文件 | 在 Docker 默认配置基础上额外阻止 `ptrace`/`process_vm_*`(进程注入)和 `io_uring_*`(历史 CVE;并非任何工作负载所需)。 | | `pids_limit: 512` | 防止 fork 炸弹和失控进程创建 | | `mem_limit: 4g` | 限制内存使用,防止代理耗尽主机内存 | | `/tmp` 作为 `tmpfs` 挂载,选项为 `noexec,nosuid,nodev` | 防止将二进制文件写入 `/tmp` 并执行 — 经典本地攻击 staging 技术 | | **包依赖冷却期** | npm/pnpm、uv、Yarn v4、Bun 和 Deno 已配置为拒绝发布在过去 N 天内(默认 7 天)的包。这提高了利用新发布的恶意包进行供应链攻击的成本。 | ### 为何使用 root 并搭配 `cap_drop: ALL`? 容器以 root 身份运行,但丢弃所有 Linux 能力。在单用户容器中,单独的 Unix 用户对隔离并无实际意义 — 代理可以写入和执行任意软件,无论 UID 为何。`cap_drop: ALL` 是真正的隔离层:即使以 root 运行,也无法挂载文件系统、加载内核模块、创建设备节点或执行启用已知容器逃逸的操作。 此设计也意味着 `apt-get install` 可开箱即用,无需额外配置。 ### GitHub 令牌外泄检测 容器内的 `GITHUB_TOKEN` 环境变量可用于经过身份验证的 Copilot 和 `gh` CLI 调用。如果被注入的恶意提示试图外泄此令牌,代理将在三个层面进行拦截: | 检测层 | 向量 | 机制 | |----------------|--------|-----------| | Squid ACL | `Authorization` 头 | `req_header` 正则表达式 — 在任何 ICAP 往返之前即触发 | | Squid ACL | 请求 URL / 查询字符串 | `url_regex` — 快速路径匹配 | | ICAP 扫描器(Go) | POST / PUT / PATCH **请求体** | 静态编译的 Go 二进制程序,位于代理容器中,监听 `127.0.0.1:1344`;最多扫描 1 MB 请求体 | 发送至 `.github.com`、`.githubusercontent.com` 或 `.githubcopilot.com` 且携带 GitHub 格式令牌的请求**不会被阻止** — 这些是合法的认证 API 调用。 检测事件会记录到代理容器内的 `/var/log/squid/exfil.log`(仅记录目标主机,不记录完整 URL 或令牌值),并在 `sandboxed-copilot proxy monitor` 中以黄色 `⚠ EXFIL` 标签显示。ICAP 服务使用 `bypass=off`:如果扫描器进程崩溃,对非 GitHub 目的地的 POST/PUT/PATCH 请求会明显失败,而不是静默绕过检测。 ### GitHub API 端点阻止(Shai-Hulud 类防护) 令牌外泄检测仅覆盖对*非 GitHub* 目的地的请求。更微妙的攻击 — “Shai-Hulud” 类 — 利用 GitHub 自身基础设施(已被允许列表)作为外泄通道: 1. 调用 `POST /user/repos` 或 `POST /orgs/{org}/repos` 创建新仓库 2. 将窃取后的代码或密钥 `git push` 到该仓库(正常推送到 `github.com` — 无法检测,除非阻止所有 git) 3. 或创建发布并上传资源到 `uploads.github.com` 沙箱从两个层面强化防护: **ICAP 端点阻止** — ICAP 扫描器还会检查对 `api.github.com` 的 POST/PUT/PATCH 请求,并阻止: | 端点 | 是否阻止 | |----------|---------| | `POST /user/repos` | 始终阻止 — 仓库创建是外泄必需 | | `POST /orgs/{org}/repos` | 始终阻止 — 组织仓库创建 | | `POST /repos/{owner}/{repo}/releases` | 默认阻止;可通过 `sandboxed-copilot proxy releases enable` 解锁 | `git push/pull` 使用 GitHub Smart HTTP(`/user/repo.git/...`)而非 `api.github.com`,因此正常 Git 操作完全不受影响。被阻止的尝试会记录到 `exfil.log`,前缀为 `GITHUB-API-BLOCK`。 **默认拒绝 `uploads.github.com`** — Squid 显式拒绝 `uploads.github.com`,该域仅用于发布资源上传,与常规 Copilot 或 Git 工作流无关。执行 `proxy releases enable` 后会自动解除。 如需允许合法的 `gh release` 工作流: ``` sandboxed-copilot proxy releases enable # unlock uploads.github.com + POST /repos/*/releases sandboxed-copilot proxy releases disable # re-block (default) sandboxed-copilot proxy releases status # check current state ``` 即使启用了发布,仓库创建端点仍保持阻止状态。 ### 包依赖冷却期 为缓解利用新发布包进行的供应链攻击(类型欺骗、依赖混淆或被劫持的废弃包),沙箱为支持的工具配置了最小发布年龄: | 包管理器 | 最小版本 | 机制 | |----------------|----------------|-----------| | npm | v11.10.0+ | `NPM_CONFIG_MIN_RELEASE_AGE` 环境变量 | | pnpm | v10.16+ | 读取 `NPM_CONFIG_MIN_RELEASE_AGE` | | uv | v0.9.17+ | `UV_EXCLUDE_NEWER` 环境变量 | | Yarn v4 | v4.10.0+ | 在启动时写入 `~/.yarnrc.yml` | | Bun | v1.3+ | 在启动时写入 `~/.config/bun/bunfig.toml` | | Deno | v2.6+ | 在启动时写入 `~/.config/deno/deno.json` | 冷却期配置在 `~/.sandboxed-copilot/config/package-cooldown`(单个整数 — 天数)。默认值为 `7`。可通过以下方式管理: ``` sandboxed-copilot cooldown status # show current setting sandboxed-copilot cooldown 14 # change to 14 days sandboxed-copilot cooldown disable # disable (allow all package ages) ``` 更改在容器下次启动时生效。如需安装近期发布的包: ``` sandboxed-copilot cooldown disable # on the host sandboxed-copilot # start a new session npm install some-new-package # ... 然后在完成后重新启用 sandboxed-copilot cooldown 7 ``` **已知限制**:pip v26+ 仅支持绝对时间戳(不支持相对持续时间),因此未进行配置。gem/Bundler 无原生冷却支持。被冷却期内发布的锁定依赖仍会被阻止 — 如需临时安装,请使用 `cooldown disable`。 ### TLS 检测(ssl_bump) 代理使用 Squid 的 `ssl_bump` 功能终止并检查所有 HTTPS 流量。当代理连接到 `https://api.github.com` 时: 1. 窥探 TLS ClientHello 以读取 SNI 主机名 2. 检查主机名是否在允许列表中(若被阻止则在此处拒绝连接) 3. 为 `api.github.com` 签发动态证书,由安装特定的 CA 签名 4. 向 Copilot 容器呈现此证书(容器信任该 CA) 5. 与真实的 `api.github.com` 建立新的 TLS 连接 这使得完整的 URL、HTTP 方法和响应码在代理日志中 — 而不只是普通正向代理所见的 opaque `CONNECT api.github.com:443` 隧道。 **CA 证书生命周期:** | 文件 | 位置 | 可访问范围 | |------|----------|----------------| | `ca.key` — 私钥(权限 600) | `~/.sandboxed-copilot/config/ca.key`(仅主机) | 代理容器(通过 `:ro` 挂载配置) | | `ca.crt` — 公钥证书 | `~/.sandboxed-copilot/config/ca.crt` | 代理容器(`:ro` 挂载)+ Copilot 容器(`:ro` 单文件挂载,启动时安装到信任库) | - 首次通过 `install.sh` 使用 `openssl req` 生成 — 每台机器唯一 - 在重新安装时保留(与 `allowlist.txt` 相同);删除并重新运行 `install.sh` 可轮换 - **不会添加到主机系统信任库** — 仅 Copilot 容器信任 ### 间接提示注入的能力与限制 若仓库中的恶意文件(或代理获取到的网页)包含注入指令: | | 能力 | |-|-----------| | ✅ 无法 | 将文件外泄到任意服务器(代理阻止) | | ✅ 无法 | 通过头部、URL 或 POST 体外泄 `GITHUB_TOKEN` 到非 GitHub 服务器(令牌外泄检测) | | ✅ 无法 | 向工具供应商发送遥测 — 镜像中已设置 `GITHUB_NO_TELEMETRY=1` 和 `DO_NOT_TRACK=1` | | ✅ 无法 | 通过 REST API 创建 GitHub 仓库(`POST /user/repos`、`POST /orgs/*/repos`) | | ✅ 无法 | 上传发布资源到 `uploads.github.com`(默认阻止;可通过 `proxy releases enable` 解锁) | | ✅ 无法 | 安装新发布的恶意包(7 天冷却期对 npm、uv、Yarn v4、Bun、Deno 生效) | | ✅ 无法 | 通过网络安装持久化恶意软件(代理阻止) | | ✅ 无法 | 修改允许列表以授予自身新网络访问权限(只读挂载) | | ⚠ 可以 | 修改 `/workspace` 内的文件 — 这是有意为之;Copilot 需要写入代码 | | ⚠ 可以 | 访问允许列表中的任何域名(GitHub、npm、PyPI 等) | ### 已知限制 - **DNS 查询** — Docker 内置 DNS 解析器(127.0.0.11)是回环地址,会绕过网络路由。攻击者可通过 DNS 子域名查询编码少量数据(每次约 50 字节);阻止它会破坏容器名称解析。 - **`allow-all` 模式是全局的** — `proxy allow-all` 会打开所有运行中的沙箱会话,而不仅是当前会话。 ## 项目结构 ``` . ├── Dockerfile # Copilot container (Ubuntu 24.04) ├── entrypoint.sh # Shows startup banner ├── docker-compose.yml # Orchestrates copilot + proxy ├── sandboxed-copilot # Launcher script (installed to ~/.local/bin/) ├── install.sh # Installation script ├── uninstall.sh # Uninstallation script ├── VERSION # Current version number ├── AGENTS.md # Instructions for the Copilot agent running inside the container ├── config/ │ ├── allowlist.txt # Default outbound allowlist │ ├── ca.crt # Per-install CA certificate (generated by install.sh) │ ├── ca.key # CA private key (generated by install.sh, chmod 600) │ └── project-allowlist.txt # Per-project domains (written by launcher) ├── proxy/ │ ├── Dockerfile # Squid proxy container │ ├── squid.conf # Squid config — deny all, include access_rules.conf │ └── entrypoint.sh # Starts Squid + allowlist watcher + mode handler └── test/ └── smoke.sh # Smoke test suite ``` ## 运行测试 ``` bash test/smoke.sh ``` 该测试套件会: 1. 从源码构建两个镜像 2. 验证 `gh` CLI 和 `mise` 已安装并可执行 3. 验证容器以 root 身份运行且具备 `cap_drop: ALL`(零 Linux 能力) 4. 验证可通过代理访问允许列表域名(`github.com`) 5. 验证非允许列表域名(`example.com`)被阻止 6. 验证无法绕过代理直接访问互联网 7. 验证新增允许列表条目在 10 秒内生效(实时重载测试) 8. 验证 `copilot-cli` 二进制文件已预安装在镜像中(构建时固化) 9. 验证 `/home/copilot` 下所有文件归 root 所有 10. 验证 HTTPS 流量由 ssl_bump 拦截(TLS 检测已激活) 测试完成后会自动清理。 ## 卸载 ``` ~/.sandboxed-copilot/uninstall.sh ``` 停止容器、移除 Docker 镜像与数据卷、删除 `~/.sandboxed-copilot/`,并移除 `sandboxed-copilot` 启动器脚本。安装脚本会复制到安装目录,因此即使仓库被删除也能正常工作。
标签:AI安全, Chat Copilot, Docker Compose, GitHub Copilot, MacOS取证, NIDS, Squid, 代理, 代码辅助, 只读挂载, 域名白名单, 威胁情报, 容器化, 应用安全, 开发者工具, 提示注入防御, 数据防泄露, 无文件攻击, 日志审计, 沙箱, 流量控制, 源代码安全, 网络安全, 网络安全, 自动化安装, 请求拦截, 隐私保护, 隐私保护