沙箱安全:隔离边界之外的策略与 enforcement

2026-06-01 28 预计阅读时间:1 分钟
来源:docker.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

40% 的受访企业把安全列为规模化 Agentic AI 的头号难题——这不是因为大家不懂"隔离",而是因为光有隔离远远不够。沙箱把进程关起来,沙箱安全则要确保这扇门在真实压力下不会被撞开、被绕过、被悄悄撬开。两者之间的差距,正是生产环境事故的常见起点。

沙箱 ≠ 沙箱安全

传统沙箱解决的是"空间隔离":进程 A 看不到进程 B 的文件,容器 C 不能访问宿主机网络。实现方式包括 namespace、cgroup、seccomp、chroot,以及更重的虚拟化层。

沙箱安全要回答的是一组更硬的问题:

  • 谁能在隔离区内做什么? 不是"能不能跑",而是"跑了之后能不能写文件、开端口、调内核模块"。
  • 边界被突破时有没有告警和阻断? 单纯隔离不会告诉你有人正在尝试 mount 宿主机文件系统。
  • 策略是否可审计、可回滚? 一条 seccomp 规则改错了,能不能快速还原,而不是重新打包镜像。

换句话说,沙箱是墙,沙箱安全是墙 + 门禁 + 监控 + 应急预案。

四个必须覆盖的策略维度

1. 系统调用过滤(seccomp)

seccomp 是 Linux 内核提供的 syscall 过滤机制。默认 Docker profile 只允许约 300 条 syscall,但很多部署直接用 --security-opt seccomp=unconfined 把它关掉——等于墙建了又拆了门。

正确的做法:基于实际运行需求定制白名单,而不是一刀切放开。

2. 能力裁剪(Linux Capabilities)

容器默认拿到约 14 条 Linux capabilities,而大多数应用只需要 NET_BIND_SERVICE(绑定低端口)和 CHOWN。多出来的 DAC_OVERRIDESETUID 都是提权路径。

3. 资源边界(cgroup + ulimit)

隔离了 CPU 和内存还不够。需要明确限制: - PID 数量(防止 fork bomb) - 文件描述符上限 - 磁盘 IOPS / 写入量

4. 网络与存储策略

  • 网络出站白名单:只允许访问指定 API endpoint,而不是整个互联网。
  • 存储卷挂载:只挂只读卷,或者用 tmpfs 限制写入大小。

实战:为 Agentic AI 代码执行构建沙箱安全策略

下面是一个可以直接改造的 Docker + seccomp 组合方案,用于隔离 AI Agent 生成的代码执行环境。

第一步:生成定制 seccomp profile

strace 抓取目标程序实际使用的 syscall,再转为白名单:

# 抓取 Python 脚本运行时的 syscall
strace -c -f python3 /app/agent_task.py 2>&1 | tail -n +3 | head -20

# 输出大致如下:
# syscall            calls    errors
# write              142      0
# read               89       0
# openat             34       0
# close              56       0
# fstat              22       0
# mmap               18       0
# mprotect           12       0
# ...

基于抓取结果,编写 seccomp JSON profile(这里给出一个精简版,只允许常见 Python 运行所需 syscall):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "read", "write", "openat", "close", "fstat",
        "mmap", "mprotect", "munmap", "brk",
        "rt_sigaction", "rt_sigprocmask",
        "ioctl", "access", "pipe", "select",
        "poll", "readlink", "mremap",
        "clone", "exit_group", "wait4",
        "fcntl", "dup", "dup2",
        "getpid", "getuid", "getgid",
        "lseek", "pread64", "pwrite64",
        "newfstatat", "set_tid_address",
        "set_robust_list", "futex",
        "clock_gettime", "clock_nanosleep",
        "madvise", "getrandom",
        "lstat", "stat", "statfs",
        "arch_prctl", "sysinfo",
        "writev", "prlimit64",
        "getdents64", "uname"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

保存为 seccomp-agent.json

第二步:用 Docker 启动带完整安全策略的沙箱

docker run -d \
  --name agent-sandbox \
  --security-opt seccomp=seccomp-agent.json \
  --security-opt no-new-privileges=true \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --pids-limit 64 \
  --memory 512m \
  --memory-swap 512m \
  --ulimit nproc=512:512 \
  --ulimit nofile=1024:1024 \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --read-only \
  --network sandbox-net \
  python:3.11-slim \
  python3 /app/agent_task.py

逐行解释关键参数:

参数 作用 为什么需要
seccomp=seccomp-agent.json syscall 白名单 未经抓取的 syscall 全部返回 EPERM,堵住提权路径
no-new-privileges=true 禁止 setuid 提权 即使镜像内有 setuid 二进制,也无法升权
--cap-drop ALL --cap-add NET_BIND_SERVICE 只保留绑定低端口能力 去掉 SETUIDDAC_OVERRIDE 等危险 capability
--pids-limit 64 限制进程数 防止 fork bomb 耗尽宿主机 PID
--memory 512m --memory-swap 512m 内存硬限 OOM 时直接杀容器,不会拖垮宿主机
--tmpfs /tmp:rw,noexec,nosuid,size=64m 受控临时目录 可写但不可执行,堵住"写脚本再执行"的攻击链
--read-only 根文件系统只读 只能写 /tmp,其他路径全部不可修改
--network sandbox-net 独立网络 需要配合下面的网络策略

第三步:创建受限 Docker 网络

# 创建只允许访问指定 API 的内部网络
docker network create \
  --driver bridge \
  --subnet 172.28.0.0/16 \
  --opt com.docker.network.bridge.enable_icc=false \
  --opt com.docker.network.bridge.enable_ip_masquerade=false \
  sandbox-net

enable_icc=false 禁止容器间通信,enable_ip_masquerade=false 禁止 NAT 出站。如果 Agent 需要访问外部 API,在宿主机用 iptables 做白名单:

# 只允许沙箱网段访问 api.example.com 的 443 端口
iptables -A FORWARD -s 172.28.0.0/16 -d api.example.com -p tcp --dport 443 -j ACCEPT
iptables -A FORWARD -s 172.28.0.0/16 -j DROP

第四步:验证策略生效

# 在沙箱内尝试危险操作,应该全部失败
docker exec agent-sandbox sh -c \
  'mount /dev/sda1 /mnt; chmod 777 /etc; curl https://evil.com; python3 -c "import os; os.fork()*1000"'

# 预期输出:
# mount: permission denied
# chmod: changing permissions of '/etc': Read-only file system
# curl: (6) Could not resolve host
# Resource temporarily unavailable  (fork 被 pids-limit 拦截)

每一条都被不同策略层拦截——这就是"墙 + 门禁 + 监控"叠加的效果。

从沙箱到沙箱安全:落地清单

把隔离变成可信赖的安全边界,需要逐项确认:

检查项 状态 备注
seccomp profile 是否基于实际 syscall 抓取定制? 默认 profile 或 unconfined 都不够
Linux capabilities 是否裁剪到最小集? --cap-drop ALL 再逐条加回
no-new-privileges 是否启用? 一行参数堵住整条 setuid 提权路径
PID / 内存 / FD 等资源是否设了硬限? 软限在攻击时毫无意义
文件系统是否只读 + tmpfs noexec? 防止"写入再执行"攻击链
网络出站是否白名单? 默认 bridge 网络等于全开放
策略变更是否有审计日志? seccomp 和 capability 变更必须可追溯
突围尝试是否有告警? seccomp ERRNO 返回值应被采集和告警

几个容易踩的坑:

  1. strace 抓取不完整——不同输入路径会触发不同 syscall。建议用多组测试数据跑三轮,合并结果后再生成 profile。
  2. 网络白名单太宽——api.example.com 解析到多个 IP,DNS 变更后白名单失效。建议用 IP 段而不是域名做 iptables 规则,或者配合 DNS proxy 控制。
  3. 只读根文件系统与包管理冲突——如果 Agent 需要动态安装依赖,改用 pip install --target /tmp/deps 并在启动脚本中设置 PYTHONPATH=/tmp/deps

沙箱安全不是一次配置就能完工的事。每次 Agent 能力扩展、每次依赖更新,都要重新跑 strace、重新审视白名单、重新验证边界。但正是这套持续校验的流程,才让"隔离"从纸面承诺变成生产环境里扛得住压力的实际安全。


相关推荐