PentAGI安全实践:用Docker沙箱锁死AI Agent的破坏半径 最近做实网攻防和 AI 安全的朋友大概率都刷到过 PentAGI 这个名字。简单说这是一个丢给它一个目标范围就能自主完成踩点、扫描、漏洞分析、尝试利用、最终出报告的全流程 AI 渗透测试 Agent。圈内对它的评价两极分化有人说这是渗透测试的未来有人说它顶多是个高级玩具。但无论站哪边有一个话题避不开这种“会自己动手”的 Agent一旦方向判断错误、命令写错、利用链失控破坏半径怎么控制PentAGI 给出的解法很直接——给 Agent 一个 Docker但别交出宿主机。把整个工作环境锁进沙箱里让它在里面随便折腾炸了也就是容器重启的事宿主机毫发无伤。这个思路说起来简单真正落地时牵扯到 capabilities 裁剪、seccomp 配置、网络隔离、资源限制、用户命名空间等一系列问题。这篇文章就聊聊 PentAGI 沙箱设计背后的考量以及我实际搭建这套环境时用的配置和踩过的坑。1. 为什么 PentAGI 这类自主 Agent 离不开 Docker 沙箱1.1 自主 Agent 的翻车代价有多大先说结论不是 PentAGI 本身有多危险而是所有“自主执行”的 Agent 都天然伴随不可控性。你以为它是个经验丰富的高级工程师实际上它更像一个充满热情但偶尔会犯迷糊的实习生——大部分时候靠谱但犯错的时候你根本拦不住。我实际跑过几轮自主渗透测试任务典型的翻车场景就有这么几类第一个是目标范围混淆。Agent 拿到一个网段之后会自己去做资产测绘。本来应该只扫目标企业的公网 IP但有几次它把宿主机所在的网关网段也纳入了扫描范围直接去扫宿主机网段里的 22、3306、6379 端口。如果这时候宿主机是办公环境里的机器Agent 就会把内网资产当成测试目标来打。这不是幻觉而是 LLM 在上下文里对“目标范围”的边界理解不够严格导致的。第二个是命令破坏性。让 Agent 去下载并使用某个公开漏洞利用脚本它从 GitHub 拉了一个项目下来里面自带了一个清理临时文件的逻辑执行完利用步骤之后顺手把/tmp下所有文件删了等于把前面装好的工具链清掉了一半整个任务直接中断。更极端的情况是如果 Agent 判断“当前环境需要修复权限问题”它可能直接执行chmod -R 777 /之类的命令在无沙箱环境下这基本等于自毁。第三个是横向影响。渗透测试工具里大量使用爆破、高频请求、踩点扫描如果 Agent 以宿主机 IP 为源地址发起这些流量目标的安全设备一旦做反向追踪暴露的就是你的真实出口地址。也就是说Agent 犯的错最后背锅的是宿主机。所以结论很清楚这类 Agent 的能力边界不能靠“提示词约束”必须靠硬隔离。Docker 沙箱的价值就在于把 Agent 所有的进程、文件、网络行为全部限制在一个可控的容器里随便它折腾宿主机不担风险。1.2 Docker 沙箱到底兜住了什么Docker 沙箱不是万能的但它确实能在几个关键维度上把风险兜住进程隔离容器跑在独立的 PID namespace 里Agent 在容器内部只能看到自己的进程看不到宿主机上的其他进程也就没法直接攻击宿主机上运行的服务。文件系统隔离容器使用独立的 rootfs加上只读挂载和 tmpfs 之后Agent 就算把整个容器文件系统写烂宿主机文件系统也不会受影响。网络隔离通过自定义 bridge 网络Agent 容器只能访问同一个 Docker 网络里的目标容器访问不了宿主机网络上的其他设备。资源隔离cgroups 限制 CPU、内存、进程数、磁盘 IOAgent 哪怕跑出一个死循环或者 fork 炸弹也只会把自己所在的容器搞挂不会拖垮宿主机。可丢弃性这是 Docker 相比物理机最大的优势。一个任务跑完直接docker rm -f把容器销毁换一个新容器重新开始。Agent 在容器里积累的所有“脏状态”一并清空不留残余。有人可能会问为什么不用虚拟机虚拟机当然隔离得更彻底但问题在于启动速度、资源占用和编排复杂度。渗透测试 Agent 的场景是频繁起停往往一个目标就要起一整套环境用 KVM 虚拟机动辄几十秒到几分钟的启动时间加上每台 VM 几个 GB 的内存开销根本不划算。Docker 容器秒级启动、单实例内存可以控制在几个 GB 以内更适合这种高频率、短周期的任务模式。1.3 “别交出宿主机”的三个红线“给 Agent 一个 Docker但别交出宿主机”这句话落到工程实现上是三条不能碰的红线第一绝对不能挂载 docker.sock。/var/run/docker.sock是 Docker 守护进程的 Unix Socket挂载给容器等于把 Docker 的控制权交给了容器内进程。拿到 docker.sock 之后容器里的 Agent 可以创建特权容器、挂载宿主机根目录直接提权到宿主机 root——这就是经典的 Docker 逃逸路径。PentAGI 的沙箱环境里任何情况下都不会把 docker.sock 挂给 Agent这是第一优先级。第二绝对不能给--privileged特权模式。privileged 模式下容器几乎拥有宿主机内核的所有权限可以访问/dev下的所有设备、加载内核模块、直接操作宿主机硬件。这跟“交出宿主机”没有任何区别。设计上只用最小化的 capabilities而不是粗暴地给一个特权容器。第三绝对不能使用 host 网络模式。host 模式下容器共享宿主机的网络栈容器里的 Agent 能看到宿主机上所有网卡和端口源 IP 也直接变成宿主机 IP。这样一来网络维度上的隔离就完全失效了。后面会详细说网络怎么设计。2. 沙箱设计的六个关键收敛面2.1 Capabilities 权限收敛去掉一切用不到的超级权限很多人用 Docker 跑工具时会忽略 Linux capabilities 这个概念。简单说Linux 内核把 root 权限细化成了几十个小权限项每个 capability 控制一类特权操作。Docker 容器启动时默认会赋予容器内 root 用户一大批 capabilities虽然比宿主机 root 少但里面仍然有不少是渗透测试 Agent 用不到的。我的做法是在docker run里先--cap-dropALL把全部 capabilities 丢掉然后再按需添加。PentAGI 的 Agent 实际需要的能力很少主要是这几项NET_RAW允许创建 raw socketnmap 的 SYN 扫描、ping 探测都需要它。NET_BIND_SERVICE允许绑定 1024 以下端口某些工具会有绑定低端口的需求。CHOWN、DAC_OVERRIDE有时候工具需要修改文件属主或读写权限这两个是常见的文件操作类能力。绝对不能加的是这几项SYS_ADMIN、SYS_PTRACE、SYS_MODULE、NET_ADMIN。SYS_ADMIN是逃逸重灾区mount、namespace 操作全都依赖它给了它等于给了一条完整的容器逃逸路径。SYS_PTRACE允许对进程做调试可以注入进程同样危险。NET_ADMIN允许修改网络配置会破坏网络隔离边界。实践配置大概长这样docker run -d \ --name pentagi-agent \ --network pentagi_lab \ --cap-dropALL \ --cap-addNET_RAW \ --cap-addNET_BIND_SERVICE \ --cap-addCHOWN \ --cap-addDAC_OVERRIDE \ --security-opt no-new-privileges \ --security-opt seccomp./pentagi-seccomp.json \ --read-only \ --tmpfs /tmp:rw,size512m \ --cpus 2 \ --memory 2g \ --pids-limit 512 \ pentagi-agent:latest重点是--cap-dropALL加按需放行的组合不要图省事直接--privileged。我见过不少人为了跑 nmap 方便直接在 Compose 里写privileged: true这基本就是裸奔。2.2 seccomp 与 AppArmor系统调用层再上一道锁capabilities 控制的是“能不能做某类特权操作”而 seccomp 是在系统调用层面做更细粒度的过滤。一个容器即使没有SYS_ADMIN如果 seccomp 没有拦截mount系统调用某些已知的逃逸漏洞依然可能被利用。所以 seccomp 是防逃逸的第二道防线也是我认为 PentAGI 沙箱里最值得花时间配置的一块。Docker 内置了一个默认 seccomp profile已经禁用了部分危险系统调用像mount、ptrace、reboot、kexec_load这些默认就在黑名单里。但默认 profile 面向的是通用场景对于跑渗透测试工具的 Agent还可以进一步收紧。我实际的建议是自定义一个 profile把 Agent 根本用不到的系统调用全部挡掉。比如渗透测试工具核心会用到的是网络相关socket、connect、sendto、recvfrom、文件相关openat、read、write、进程相关clone、execve、exit这几类其他像mount、umount2、ptrace、init_module、delete_module、setns、unshare、keyctl这些高危调用直接返回错误。简化版 profile 类似这样{ defaultAction: SCMP_ACT_ERRNO, architectures: [SCMP_ARCH_X86_64], syscalls: [ { names: [ accept, bind, connect, getsockname, getpeername, getsockopt, setsockopt, listen, recvfrom, sendto, socket, socketpair, shutdown, openat, read, write, close, lseek, mmap, mprotect, munmap, brk, ioctl, fstat, lstat, stat, getdents64, fcntl, access, uname, clone, execve, exit, exit_group, wait4, getpid, gettid, getuid, getgid, geteuid, getegid, futex, nanosleep, clock_gettime, getrandom, sigaction, rt_sigaction, prctl, arch_prctl ], action: SCMP_ACT_ALLOW } ] }这个 profile 的defaultAction是SCMP_ACT_ERRNO意思是除了白名单里的系统调用其他的一律返回错误。跑渗透测试工具时如果遇到某个工具需要额外系统调用看报错再一层层往白名单里加原则是“默认拒绝按需放行”。同时建议打开 AppArmor至少用 Docker 自带的docker-defaultprofile。在 Ubuntu 这类默认启用 AppArmor 的系统上给容器加--security-opt apparmordocker-default就能启用。AppArmor 和 seccomp 是互补的前者管文件路径、网络访问等宏观策略后者管系统调用级的行为两道一起用。Debian 系列如果默认没装 AppArmor需要先apt install apparmor apparmor-utils再启用。2.3 网络隔离让 Agent 只看得到该看的目标网络这块是 PentAGI 沙箱设计的核心之一。方式不是把 Agent 的网络完全断掉而是给它一张“定向”的网卡让它只能看到目标环境看不到其他任何东西。我的设计方案是创建一个独立的 Docker bridge 网络把 Agent 容器和靶标容器放在同一个网络里形成一个专属的“实验内网”。Agent 在这个网络里可以自由扫描、发起流量但出了这个网络就什么都访问不了。核心是不能使用--network host这一点再怎么强调都不过分。具体的 Compose 网络设计后面会给出示例。这里先说明几个需要注意的细节一是自定义 bridge 网络默认会启用 Docker 内置 DNS容器内的/etc/resolv.conf会指向127.0.0.11。大部分情况下没问题但有些渗透工具会自己处理 DNS偶尔出现解析不了的怪问题这时候要留意是不是 Docker DNS 转发的问题。二是如果想要限制 Agent 访问外网只允许它访问内网目标可以在宿主机上用 iptables 加规则或者在 Docker 网络层面做限制。PentAGI 这类工具很多时候需要从外网拉取 exploit payload所以完全断外网不现实我的做法是允许 agent 容器对外访问但只开放 80/443然后用透明代理做审计。三是源 IP 的问题。agent 容器通过自定义 bridge 网络访问外部时默认会做 NAT源 IP 是宿主机的 IP。如果不想让目标环境看到真实宿主机 IP可以在外层再套一层网络出口或者用专门的出口节点转发流量。这一点取决于你的测试场景要求。2.4 资源限制防死循环更防 fork 炸弹自主 Agent 在执行任务时很难保证每一步都符合预期LLM 生成的指令死循环、脚本递归调用、工具并发失控都是常见问题所以资源限制一定要做不能偷懒。我通常在 PentAGI 沙箱容器上设置四类限制内存--memory 2g限制容器最大可用内存。不够就加大但必须显式设置防止 Agent 把宿主机内存吃光。加了--memory之后建议同时设置--memory-swap保持一致避免 swap 被滥用。CPU--cpus 2限制最多使用 2 个逻辑核心。对于扫描类任务CPU 占用率其实不高真正吃 CPU 的是那些大量并发 socket 的压测工具限制 CPU 可以避免影响到宿主机上其他服务。进程数--pids-limit 512这个非常关键。Agent 执行漏洞利用脚本时如果出现 fork 炸弹或者某个工具异常地不断创建子进程pids-limit 会在进程数达到上限时直接拒绝创建新进程避免拖垮整个宿主机。默认情况下 pids-limit 是 0表示不限制等于裸奔。磁盘--storage-opt size10g限制容器可写的磁盘空间。扫描结果、报告、下载的 payload 都可能积攒大量数据不限制的话容器磁盘可以无限增长占满宿主机的磁盘分区。设置完资源限制之后建议在容器里实际测一遍。跑一个大工具观察内存、CPU、进程数是否都被撑住。我踩过一次坑--memory 1g对于正常扫描够用但一旦 Agent 同时启动多个工具整个容器直接被 OOM Kill任务无故中断。后面把内存加到 2g同时调整了 Agent 的工具并发数才稳定下来。2.5 只读根文件系统与临时目录分离沙箱的另一个细节是文件系统设计。我的原则是Agent 所在的容器根文件系统必须是只读的它所有需要写文件的地方都放到 tmpfs 或者一块独立的临时卷里。为什么会这么设计因为 Agent 在容器里执行漏洞利用、下载 payload、运行脚本可能无意中覆盖掉系统文件、写坏配置。如果把 rootfs 设成只读Agent 想改系统文件也改不动就算把/usr/bin下所有文件删了也只是删在一个只读层上重启容器立刻恢复原样。实现上很简单docker run加一个--read-only然后给/tmp挂一个 tmpfs--read-only \ --tmpfs /tmp:rw,size512m大部分命令行工具、渗透测试框架都会把临时文件写到/tmp或者用户目录。放到 tmpfs 的好处很明显数据写在内存里不会落盘容器销毁即彻底消失不会留下敏感痕迹。像 Metasploit、nmap 这类工具临时文件需求不大512m 基本够了。如果 Agent 有大体积 payload 要下载可以单独给一个工作目录挂一块匿名卷方便在不同容器间传递结果。另外Agent 产生的报告和测试结果需要一个持久化通道。我的做法是在 Agent 容器里挂一个只读方向的卷宿主机往容器里喂目标信息Agent 把结果写到宿主机可读的目录但 Agent 自身不能卸载或修改挂载点本身。用 named volume 比 bind mount 更好管理权限。2.6 用户命名空间与降权容器内 root 不等于宿主机 root最后一条是容器内运行的用户身份。默认情况下容器里以 root 运行这个 root 在宿主机上映射的也是 root。虽然容器有 namespace 隔离但一旦出现内核漏洞导致逃逸容器内的 root 就直接变成了宿主机的 root这是最坏的情况。缓解方案有两个层面。第一个是在容器内用非 root 用户运行 Agent 进程。在 Dockerfile 里创建普通用户然后USER pentagi让 Agent 主进程以普通用户身份跑。大多数渗透测试工具不需要 root需要 root 的场景比如绑定低端口可以用NET_BIND_SERVICEcapability 解决不需要真的用 root 用户。第二个是 Docker 的用户命名空间重映射。在/etc/docker/daemon.json里配置{ userns-remap: default }配置之后Docker 会把容器内的 root 用户映射到宿主机上一个普通用户通常是dockremap这样即使容器逃逸得到的权限也只是一个普通用户的权限而不是 root。这个选项在安全要求高的生产环境强烈建议开启。需要注意的是开启 userns-remap 之后旧的容器和镜像需要重新构建映射关系数据卷的权限也需要重新调整所以最好在一开始就规划好。这两个方案可以叠加使用userns-remap 提供一层宿主机级别的降权容器内再用非 root 用户运行双保险。3. 一套可直接复制的 PentAGI 沙箱编排方案3.1 架构设计控制平面、执行平面、靶场平面分层前面讲了这么多设计原则落地时需要一个清晰的架构。我把 PentAGI 沙箱分成三个平面第一个是控制平面。这部分跑在宿主机上负责 Agent 的决策大脑——也就是 LLM 推理。LLM 的 API 调用放宿主机上执行避免在容器里保存 API Key。Agent 产生的操作指令由控制平面下发到执行平面。第二个是执行平面。这就是沙箱容器本身PentAGI Agent 的所有工具都在这里运行。容器通过网络、capabilities、seccomp 隔离只能向特定目标发起请求。第三个是靶场平面。这是一个或多个故意配置为脆弱的容器比如运行 DVWA、Metasploitable、Juice Shop 这类靶场镜像或者你模拟的目标应用。靶场容器与执行平面容器在同一个 Docker bridge 网络里Agent 在这个网络内可以任意测试但网络之外的世界完全不可达。为什么要分三个平面因为 Agent 的决策链路和执行链路安全需求不同。控制平面保存着最敏感的凭证信息如果让 Agent 在执行环境里直接保管密钥一旦沙箱被攻破密钥就泄露了。分离之后就算执行平面的容器被完全打穿攻击者也拿不到 LLM API Key也控制不了 Agent 的决策逻辑。3.2 docker-compose.yml 完整示例基于上面的架构我贴一套实际在用的 Compose 配置services: pentagi-agent: image: pentagi-agent:latest container_name: pentagi-agent networks: - pentagi_lab cap_drop: - ALL cap_add: - NET_RAW - NET_BIND_SERVICE - CHOWN - DAC_OVERRIDE security_opt: - no-new-privileges:true - seccomp:./seccomp/pentagi-profile.json - apparmor:docker-default read_only: true tmpfs: - /tmp:rw,size512m cpus: 2 mem_limit: 2g memswap_limit: 2g pids_limit: 512 volumes: - pentagi-work:/work environment: - TARGET_RANGE172.28.0.0/24 logging: driver: json-file options: max-size: 10m max-file: 3 restart: no pentagi-target: image: vulnerables/web-dvwa:latest container_name: pentagi-target networks: - pentagi_lab cap_drop: - ALL cap_add: - NET_BIND_SERVICE security_opt: - no-new-privileges:true pids_limit: 256 restart: no networks: pentagi_lab: driver: bridge ipam: config: - subnet: 172.28.0.0/24这里有几个细节值得说明pentagi_lab网络手动指定了子网172.28.0.0/24这样 Agent 可以通过环境变量TARGET_RANGE明确知道目标网段范围避免它把扫描对象扩散到其他网络。Agent 的日志用 json-file driver 并限制日志文件数量和大小防止 Agent 疯狂输出把宿主机磁盘写满。靶场容器同样做了降权处理而不是默认直接跑。靶场本身是故意有漏洞的但它的权限依然要收窄避免靶场被攻破之后反过来影响 Docker 守护进程。没有挂载 docker.sock没有 privileged没有 host 网络。这三条红线在这个 Compose 里直接被硬编码挡住了。3.3 结果回收与任务级隔离渗透测试任务结束之后Agent 会产生扫描报告、漏洞利用记录、凭据信息等文件。回收这些结果时要注意两点一是结果的可信度二是 Agent 是否在容器里留下了不该留的东西。我的做法是每个任务启动一个全新的 Agent 容器任务结束后销毁容器但结果输出目录通过 named volume 保留下来docker run --rm -v pentagi-work:/work -v pentagi-reports:/reports dockerdummy-copy:latest 2/dev/null docker cp pentagi-agent:/reports ./reports-$(date %Y%m%d)实践中更简单的方案是Agent 任务结束后在宿主机上直接docker cp容器里的结果目录。由于容器是我们启动的结果目录是可控的可以确保拿到的报告没有经过 Agent 的“二次修改”——毕竟 Agent 自己写的报告可能包含幻觉内容需要审计之后再采用。任务隔离还有一个好处不同目标之间不会串数据。上一个任务里 Agent 积累的临时文件、缓存、错误配置不会影响到下一个任务。Agent 每次都是从干净的镜像启动环境一致性有保障。4. 实战中的典型问题与排障记录4.1 容器频繁被 OOM Killed这个问题我遇到的最多。现象是 Agent 跑着跑着突然中断docker ps -a看到容器状态是 Exiteddocker inspect里的OOMKilled字段是true。排查思路很简单先看dmesg | tail -n 50如果宿主机内核日志里出现Out of memory: Killed process确认是 OOM。看docker stats观察容器实际内存占用曲线判断是设置值太小还是 Agent 自身存在内存泄漏。如果是工具并发导致峰值超限调整 Agent 的并发参数如果是工具本身的问题把内存限制调大或者给 Agent 加一个--memory-swap的缓冲。经验值普通扫描任务 2g 内存够用但如果你同时让 Agent 跑 sqlmap、nuclei、nmap 多个工具内存轻松冲破 2g。我的建议是一开始就给 Agent 设置明确的“单工具并发上限”而不是让多个工具同时全速跑否则不仅内存吃紧目标方安全设备也很容易触发告警。4.2 nmap 报 Operation not permitted容器里跑nmap -sS时报operation not permitted大概率是没给NET_RAWcapability。SYN 扫描需要创建 raw socket而 raw socket 需要NET_RAW权限。还有种情况是会 ping 不通目标但端口扫描正常。这是因为 ICMP 报文发送同样需要 raw socket如果cap-add里少了NET_RAWAgent 的存活探测阶段就会误判“目标主机下线”整个扫描直接就停了。检查能力可以用capsh --print或者直接在容器里跑grep CapEff /proc/self/status用capsh --decode$(cat /proc/self/status | grep CapEff | awk {print $2})看实际启用了哪些 capabilities。如果cap_dropALL配错了permission denied 就会成为高频报错。4.3 目标网络不可达与 DNS 解析异常Agent 扫不到靶场容器排查方向一般有两个。一个是网络不在同一个 bridge 里。如果 Agent 容器和靶场容器不在同一个 Docker 网络它们之间默认不通。检查docker network inspect pentagi_lab里是否两个容器都在线不在就用docker network connect pentagi_lab container连进去。另一个是自定义 bridge 网络的 DNS 问题。Agent 容器里/etc/resolv.conf指向127.0.0.11这是 Docker 内置 DNS。如果 Agent 要通过域名访问外部目标需要确认 Docker DNS 能正常转发。遇到解析超时可以先docker exec进容器里手动nslookup测试区分是 Docker DNS 的问题还是容器外 DNS 的问题。4.4 逃逸风险自查清单沙箱配置完之后强烈建议做一轮逃逸风险自查。以下每一项都是严重风险点任何一条命中都说明沙箱不达标检查项危险原因正确配置挂载了/var/run/docker.sock可以直接控制 Docker 守护进程提权到宿主机 root绝不挂载privileged: true容器拥有宿主机全部设备与内核权限关闭用最小 capabilities 替代network_mode: host共享宿主机网络栈网络隔离失效使用自定义 bridge 网络授予SYS_ADMINcapability可执行 mount、namespace 操作经典逃逸路径从cap_dropALL开始按需添加未设置no-new-privileges允许容器内进程执行 setuid 提权一律开启容器内 root 无用户命名空间映射内核漏洞逃逸后直接获得宿主机 root开启userns-remap未配置 seccomp/AppArmor高危系统调用未被抑制自定义 seccomp profile 默认 AppArmor未限制 pids 与内存fork 炸弹或内存耗尽可能拖垮宿主机设置pids_limit与mem_limit这个清单我每次搭新环境都会拿出来过一遍。不要相信“默认配置应该安全”这种错觉——Docker 默认的隔离是够用的但对于跑自主渗透测试 Agent 这种高危场景必须按最小权限原则逐项收敛。最后再分享一个我在 PentAGI 沙箱实践里最深的感受给 Agent 一个 Docker本质上是一种“预先认输”的策略——你不是相信它永远不出错而是默认它一定会犯错然后把错误半径提前锁死。这种思路在 AI Agent 越来越自主的今天适用范围远超渗透测试本身。任何让 Agent 直接操作外部资源的场景数据库变更、自动化运维、处理敏感数据都可以用同样的一套沙箱哲学来约束。先把容器拆明白Agent 怎么折腾都没事这个底是值得花时间打的。