为你的AI代理构建安全沙箱

批准疲劳是安全性的敌人。学习为你的 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、让我们测试它

  1. 启动代理 在一个终端中:
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.jsdocker run 设置开始。这是 50 行代码,可能让你免于非常糟糕的一天。


原文连接:Build a Secure Sandbox for Your AI Agent

汇智网翻译整理,转载请标明出处