stevenwilliamson/sandboxed-copilot
GitHub: stevenwilliamson/sandboxed-copilot
一个通过沙箱化代理隔离 Copilot 流量的安全方案,防范提示注入与数据外泄。
Stars: 1 | Forks: 0
# sandboxed-copilot
[](LICENSE)
主要目标是防御**间接提示注入**和**异常 AI 行为**:如果 Copilot 被操纵试图窃取数据或访问意外目的地,网络防火墙会阻止它。
无需针对每个项目进行设置 — 只需 `cd` 到任意目录并运行 `sandboxed-copilot`。

## 工作原理
两个容器通过 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, 代理, 代码辅助, 只读挂载, 域名白名单, 威胁情报, 容器化, 应用安全, 开发者工具, 提示注入防御, 数据防泄露, 无文件攻击, 日志审计, 沙箱, 流量控制, 源代码安全, 网络安全, 网络安全, 自动化安装, 请求拦截, 隐私保护, 隐私保护