ByteNoteByteNote
Docker 沙箱纵深防御,一张清单
字

字节笔记本

2026年10月24日 · 约 11 分钟读完

Docker 沙箱纵深防御,一张清单

API中转
¥120

沙箱:限制文件与网络访问

本文是《从 0 写 Agent》系列第 33 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Docker 沙箱:纵深防御清单」。

上一章讲了 Sandbox 抽象。这一章深入最安全的后端,Docker。

agents-go 的 Docker 后端是容器安全的教科书级实现,几乎每一行 HostConfig 都有注释说明"为什么这么做"。无网络、只读根文件系统、丢弃所有 capabilities、nobody 用户、PID 限制、可选 gVisor……这是"纵深防御"的完整清单。


引子:容器逃逸的现实

Docker 默认配置不够安全。默认容器:

  • 有网络(能外连)
  • root 文件系统可写(能改容器内文件)
  • 以 root 运行(容器内 root)
  • 保留 capabilities(CAP_NET_RAW 能抓包,CAP_SYS_PTRACE 能调试)

这些默认值对跑 Agent 生成的不可信代码来说太危险。 模型生成的代码可能有恶意网络请求、可能利用容器内 root 提权。必须收紧。

agents-go 的 Docker 后端把所有默认都改成了"最严格",这就是纵深防御。


安全实践

纵深防御清单

agents-go 的 Docker HostConfig(sandbox/docker/docker.go:188-211),每个设置都"收紧":

1. NetworkMode: none,无网络

go
HostConfig: container.HostConfig{
    NetworkMode: "none",  // 完全无网络
}

默认无网络。 代码不能外连,不能发恶意请求、不能下载恶意软件、不能数据外泄。

需要网络的场景(比如代码要调 API),可以配置放开,但默认禁。

2. ReadonlyRootfs: true,只读根文件系统

go
ReadonlyRootfs: true,  // 根文件系统只读

容器内的 / 是只读的。 代码不能修改系统文件、不能植入后门、不能改配置。

但代码要写文件怎么办?用匿名 volume(下面讲)。

3. CapDrop: ALL,丢弃所有 capabilities

go
CapDrop: []string{"ALL"},  // 丢弃所有 Linux capabilities

Linux capabilities 是 root 权限的细分(CAP_NET_RAW、CAP_SYS_ADMIN 等)。默认容器保留一些,可能被利用。全丢弃,最小权限。

4. no-new-privileges,禁止提权

go
SecurityOpt: []string{"no-new-privileges"},  // 禁止获取新权限

即使代码里有 setuid 二进制,也不能提权。 防"通过 setuid 程序获取 root"的攻击。

5. User: nobody,非 root 运行

go
User: "65534:65534",  // nobody:nobody

以 nobody 用户(UID 65534)运行,不是 root。 即使容器内有提权漏洞,nobody 权限有限,破坏力小。

6. PidsLimit,进程数限制

go
PidsLimit: ptr(int64(128)),  // 最多 128 个进程/线程

防止 fork bomb(while True: os.fork())。没有限制,fork bomb 能耗尽宿主进程表。

7. Memory/NanoCPUs,资源限制

go
Memory:    limits.MemoryBytes,  // 如 256MB
NanoCPUs:  int64(limits.CPUs * 1e9),  // 如 0.5 核

内存和 CPU 限制。 防止代码吃光宿主资源(a = [0] * 999999999)。

8. 可选 Runtime: runsc(gVisor)

go
Runtime: "runsc",  // 如果配了 gVisor

gVisor 是 Google 的用户态内核,它拦截容器的系统调用,在用户态模拟,提供比默认 Linux 内核更强的隔离。即使容器内有内核漏洞利用,gVisor 挡住。可选,需要宿主预装 gVisor。


匿名 Volume + tar 注入:为什么

根文件系统只读,但代码要写文件(比如生成图表)。agents-go 的解法(docker.go:198-205),匿名 volume 挂载到工作目录:

go
// docker.go:198(概念)
// 根 fs 只读,但 Docker 不允许往只读 fs 写文件
// 所以用一个匿名 volume 挂到工作目录
Mounts: []container.Mount{{
    Type:   "volume",
    Target: "/work",  // 工作目录
}},
WorkingDir: "/work",

代码在 /work 目录跑,这个目录是可写的 volume,但根 fs 还是只读。 代码能写自己的输出文件,但不能改系统文件。

tar 注入文件

代码文件怎么传进容器?不能直接 cp(根 fs 只读)。agents-go 用 tar 流注入(docker.go:305 的 buildTar):

go
// 创建一个 tar 归档,包含所有要写入的文件
tarStream := buildTar(req.Files)
// 通过 Docker API 的 CopyToContainer 注入到 volume
client.CopyToContainer(ctx, containerID, "/work", tarStream, ...)

tar 流直接注入到匿名 volume,绕过只读 fs 限制。 这是个巧妙的工程方案。


日志处理的细节

Docker 的日志是 stdout/stderr 复用的流(multiplexed stream)。agents-go 的 demuxLogs(docker.go:238)和 stopWhenFull(docker.go:252)处理这个:

go
// 概念示意
// Docker 日志:[type=stdout][data...][type=stderr][data...]...
// demuxLogs 分离成两个独立的 stdout/stderr
// 每个流独立封顶 MaxOutputBytes
// stopWhenFull:只有两个流都满了才停止读取(防一个流淹没另一个)

关键设计,"两流都满才停读"(docker.go:236 注释)。如果 stdout 先满了就停读,stderr 可能丢失。等两个都满才停,保证两个流的信息都捕获到。


超时后的日志读取

超时杀进程后,要读最后的日志(看为什么超时)。但此时请求的 context 已经耗尽:

go
// docker.go:154(概念)
// 超时后 ctx 已 cancel,不能再用它读日志
// 用 context.WithoutCancel(ctx) 创建新 context + logReadTimeout
logCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
client.ContainerLogs(logCtx, containerID, ...)

context.WithoutCancel(Go 1.21+)创建一个"不继承取消"的新 context。这让超时后还能用新的超时读日志,不被原 context 的取消影响。

这是个微妙的细节,超时不是"立即放弃",而是"停止执行但允许短暂读日志"。


纵深防御的意义

为什么这么多层防御?因为单层防御可能被突破。

  • 无网络 + 但有 root → root 可能通过内核漏洞逃逸
  • 有 root + 但无 capability → 没 capability 的 root 权限有限
  • 无 capability + 但能提权 → no-new-privileges 防
  • ……每一层都假设上一层可能被突破

纵深防御(defense in depth)的核心,不依赖任何单层,多层叠加,攻击者要突破所有层才能造成伤害。

agents-go 的 Docker 配置就是完整的纵深防御清单,网络、fs、capability、提权、用户、资源、可选 gVisor。这是跑不可信代码的安全基线。


Part V 回顾

第 28-33 章,让 Agent 连接外部世界:

章节能力
28-29 MCP连接 MCP 工具生态
30 Skills渐进式披露的技能
31 os.Root安全的文件读取
32 Sandbox 抽象隔离执行代码
33 Docker 沙箱纵深防御

Agent 现在能,用外部工具(MCP)、加载技能(Skills)、安全读文件(os.Root)、隔离跑代码(Sandbox)。


下一篇

第 34 章,进入 Part VI,完整应用。把前面所有组件(Agent 内核、Session、HITL、Tracing、MCP、Sandbox)组装成一个真实的 Agent 平台,agents-server。单二进制、内嵌前端、SQLite、开箱即用。


本文是《从 0 写 Agent》系列第 33 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: