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_OVERRIDE、SETUID 都是提权路径。
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 |
只保留绑定低端口能力 | 去掉 SETUID、DAC_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 返回值应被采集和告警 |
几个容易踩的坑:
- strace 抓取不完整——不同输入路径会触发不同 syscall。建议用多组测试数据跑三轮,合并结果后再生成 profile。
- 网络白名单太宽——
api.example.com解析到多个 IP,DNS 变更后白名单失效。建议用 IP 段而不是域名做 iptables 规则,或者配合 DNS proxy 控制。 - 只读根文件系统与包管理冲突——如果 Agent 需要动态安装依赖,改用
pip install --target /tmp/deps并在启动脚本中设置PYTHONPATH=/tmp/deps。
沙箱安全不是一次配置就能完工的事。每次 Agent 能力扩展、每次依赖更新,都要重新跑 strace、重新审视白名单、重新验证边界。但正是这套持续校验的流程,才让"隔离"从纸面承诺变成生产环境里扛得住压力的实际安全。