为你的AI代理构建安全沙箱
在本指南中,我将向你展示如何使用 Docker 和 Node.js 为本地开发构建一个沙盒命令运行器。它将为你的代理提供读/写工作空间和互联网访问——但只能访问你明确允许的特定域。
批准疲劳是安全性的敌人。学习为你的 AI 代理构建一个安全 Docker 沙箱,它消除了不断的权限提示。
如果你最近使用过任何代理编码工具,你知道套路。代理想要运行 npm install。你收到提示:允许命令? 然后它想要运行测试。允许命令? 然后它需要调试失败。允许命令?
到第十个提示时,你不再审核安全性。你只是在训练自己点击"是",好让你回到工作。这被称为批准疲劳,它是安全性的敌人。
解决方案不是让代理问更好听的问题。解决方案是将代理放在一个它不需要问的盒子里。
在本指南中,我将向你展示如何使用 Docker 和 Node.js 为本地开发构建一个沙盒命令运行器。它将为你的代理提供读/写工作空间和互联网访问——但只能访问你明确允许的特定域。
1、架构
我们不仅仅是在运行容器;我们在构建一个受控环境。我们的系统有两层防御:
- 文件系统监狱(Docker): 代理看到一个只读操作系统。它唯一可以写入的地方是我们挂载的特定项目目录。它无法触碰你的 SSH 密钥、其他文件夹中的
.env文件或你的系统配置。 - 网络门卫(Node.js 代理): 我们不仅仅给容器开放互联网。我们强制流量通过本地代理,该代理根据允许列表检查每个请求。

2、门卫(代理)
首先,我们需要一个代理。Docker 的原生网络控制是基于 IP 的,当你只想说"允许 registry.npmjs.org"时,这很烦人。
我们将构建一个最小化的 Node.js 代理来拦截流量。如果域被允许,它会通过管道传输数据。如果不允许,它会切断连接。
创建一个名为 proxy.js 的文件:
const http = require('http');
const net = require('net');
const url = require('url');
// 1. The Policy
const ALLOWED_DOMAINS = [
'registry.npmjs.org', // For installing packages
'github.com', // For cloning repos
'api.openai.com' // If the agent needs to talk to the LLM from inside
];
const PORT = 8888;
const server = http.createServer((req, res) => {
// Handle standard HTTP requests (less common now, but good to have)
const parsed = url.parse(req.url);
if (!isAllowed(parsed.hostname)) {
res.writeHead(403);
res.end('Blocked by Sandbox Proxy');
console.log(`[BLOCKED] HTTP request to ${parsed.hostname}`);
return;
}
// ... (HTTP forwarding logic would go here, omitting for brevity/HTTPS focus)
});
// 2. Handle HTTPS (The CONNECT method)
server.on('connect', (req, clientSocket, head) => {
const { port, hostname } = url.parse(`//${req.url}`, false, true);
if (!isAllowed(hostname)) {
console.log(`[BLOCKED] CONNECT request to ${hostname}`);
clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
clientSocket.end();
return;
}
console.log(`[ALLOWED] ${hostname}`);
// 3. The Pipe
const serverSocket = net.connect(port || 443, hostname, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
serverSocket.on('error', (err) => {
console.error(`[ERROR] Connection to ${hostname} failed:`, err.message);
clientSocket.end();
});
});
function isAllowed(hostname) {
// Simple substring match to allow subdomains (e.g., sub.github.com)
return ALLOWED_DOMAINS.some(d => hostname === d || hostname.endsWith(`.${d}`));
}
server.listen(PORT, () => {
console.log(`🛡️ Sandbox Proxy running on port ${PORT}`);
});
在一个终端中运行这个(node proxy.js)。这是你的防火墙。
3、监狱(容器)
现在我们需要环境。我们不仅仅是在运行 docker run ubuntu。我们在锁定它。
以下是我们想要的配置:
只读根: 代理无法修改操作系统文件。
- Tmpfs: 给它一个
/tmp中的草稿本,当容器死亡时消失。 - 用户映射: 以非根用户运行,所以创建的文件由你拥有,而不是
root。 - 网络配置: 注入指向我们主机机器的
HTTP_PROXY变量。
以下是概念性的 Docker 命令(我们将在第3部分将其包装在脚本中):
docker run \
--rm \
--user $(id -u):$(id -g) \
--read-only \
--tmpfs /tmp \
--network host \
-v $(pwd):/workspace \
-w /workspace \
-e HTTP_PROXY=http://localhost:8888 \
-e HTTPS_PROXY=http://localhost:8888 \
node:18-slim \
npm install
等等,为什么 --network host ? 对于 Linux 上的本地开发,这是让容器看到 localhost:8888(我们的代理)的最简单方法。在 macOS/Windows 上,你会使用 --network bridge 并将代理指向 host.docker.internal:8888。
4、运行器
让我们将它们组合成一个简单的运行器脚本。这是你的代理会调用的,而不是 exec()。
创建 sandbox-runner.js:
const { spawn } = require('child_process');
const os = require('os');
const commandToRun = process.argv[2] || 'echo "No command provided"';
const isMac = os.platform() === 'darwin';
// Configure network for Mac vs Linux
const networkFlag = isMac ? [] : ['--network', 'host'];
const proxyHost = isMac ? 'host.docker.internal' : 'localhost';
const proxyUrl = `http://${proxyHost}:8888`;
const dockerArgs = [
'run',
'--rm', // Cleanup after run
'--read-only', // Immutable OS
'--tmpfs', '/tmp', // Writable temp space
'--tmpfs', '/run', // Needed for some tools
'-v', `${process.cwd()}:/workspace`, // Mount current dir ONLY
'-w', '/workspace', // Set working dir
'-e', `HTTP_PROXY=${proxyUrl}`, // Force traffic to proxy
'-e', `HTTPS_PROXY=${proxyUrl}`,
'-e', 'NPM_CONFIG_REGISTRY=http://registry.npmjs.org/', // Force non-SSL for easier proxying, or allow HTTPS registry
...networkFlag,
'node:18-slim', // Image to use
'/bin/sh', '-c', commandToRun
];
console.log(`📦 Sandboxing: "${commandToRun}"`);
const child = spawn('docker', dockerArgs, { stdio: 'inherit' });
child.on('close', (code) => {
console.log(`\nEnd of sandbox session. Exit code: ${code}`);
});
5、让我们测试它
- 启动代理 在一个终端中:
node proxy.js
2. 尝试禁止请求(Google):
node sandbox-runner.js "curl -I https://google.com"
结果: 命令立即失败。代理记录:[BLOCKED] CONNECT request to google.com。
3. 尝试允许请求(GitHub):
node sandbox-runner.js "curl -I https://github.com"
结果: 成功。HTTP/1.1 200 Connection Established。
4. 尝试删除操作系统:
node sandbox-runner.js "rm -rf /bin"
结果: rm: cannot remove '/bin': Read-only file system。
6、为什么这很重要
我们从根本上改变了安全模型。
之前:
- 安全依赖于过程。(你阅读命令。)
- 失败模式:你累了,点击"是",代理将你的
.env文件上传到 pastebin。
之后:
- 安全依赖于架构。(网络拓扑。)
- 失败模式:代理尝试上传你的
.env文件,代理看到pastebin.com不在列表中,连接被丢弃。你甚至不需要在那里。
这允许你为沙箱给代理"自动批准"模式。你信任盒子,而不是机器人。
7、已知的限制
这种方法是健壮的,但它不是魔法。
- Docker Socket: 永远不要将
/var/run/docker.sock挂载到这个沙箱中。如果你这样做,代理可以生成一个新的特权容器并立即逃逸。 - 出口旁路: 我们依赖
HTTP_PROXY环境变量。一个真正恶意的二进制文件可以忽略这些变量并尝试直接连接到 IP。在企业环境中,你会在防火墙级别阻止非代理出口。对于本地开发,这阻止了意外外泄和 99% 的"供应链"攻击(通常使用标准 HTTP 库)。 - 性能: 为每个命令启动容器有开销。对于"与代码库聊天"循环,它可以忽略。对于紧密的"编辑-运行-测试"循环,你可能希望在后台保持容器运行。
8、结束语
代理正在变得足够强大以变得危险。我们不应该用权限提示来削弱它们;我们应该为它们构建操场,让它们快速运行并安全地破坏东西。
从这个 proxy.js 和 docker run 设置开始。这是 50 行代码,可能让你免于非常糟糕的一天。
原文连接:Build a Secure Sandbox for Your AI Agent
汇智网翻译整理,转载请标明出处