给Agent一个Docker但别交出宿主机:全自主渗透测试沙箱实操
最近这一年AI Agent 在安全圈子里算是彻底火起来了尤其是“让 Agent 自己跑渗透测试”这个方向。PentAGI 这类项目一出来很多人第一反应是兴奋以后是不是丢一个目标进去Agent 就能自动侦察、扫描、打点、提权、写报告了兴奋过后紧接着就是冒冷汗。渗透测试本质上是在模拟一个攻击者而 Agent 又是一个会自主决策的程序。你把它放在哪如果直接放在宿主机上它就等于握着你系统的钥匙如果你把它关进 Docker但配置不当它照样能顺着 Docker socket 摸到宿主机。所以“给 Agent 一个 Docker但别交出宿主机”这句话才是整套全自主渗透测试沙箱设计里最核心的原则。这篇文章不聊 PPT 架构只聊实操。我会从为什么需要沙箱、为什么选 Docker 而不是虚拟机、具体怎么配置到跑起来之后会遇到哪些坑逐个拆开讲。1. 先想清楚给 Agent 做沙箱防的到底是什么1.1 PentAGI 这类“全自主渗透测试”到底在做什么传统渗透测试是命令行工具的拼装nmap 扫端口gobuster 跑目录sqlmap 测注入拿到 shell 之后再上 metasploit 提权。这一整套流程最大的瓶颈不是工具而是“人”——需要人根据扫描结果判断下一步需要人在半夜爬起来处理报错需要人把零散的命令整理成报告。PentAGI 这类项目的思路是把“人”这个决策环节替换成大模型。Agent 拿到任务后会像一个渗透测试工程师一样工作先侦察目标资产发现开放的端口和服务比对已知漏洞库选择合适的利用方式尝试攻击失败就换策略成功就深入一步最终生成带证据链的报告。和脚本相比它的优势是能根据回显动态调整思路不是一条道走到黑。这意味着什么意味着你在实验室里跑的不再是一个“小程序”而是一个具备决策能力的自动化对手。它能做的事情多闯祸的可能性也大。比如扫描过程中误伤了同网段其他机器比如被扫描结果里的恶意文本诱导执行了危险命令比如对某个目标反复尝试利用导致服务被搞崩。这些事不一定要 Agent 有主观恶意才会发生能力边界不清、上下文被污染都可能触发。1.2 沙箱防的不是“Agent 叛变”而是“失误失控”很多人在设计沙箱时有个误区总想着“防 Agent 意识觉醒”。说实话以当前的模型水平没必要防这个也防不住。真正要防的是两类非常现实的风险。一类是 prompt injection。扫描结果、网页内容、服务 banner 都可能包含精心构造的文本Agent 在读取这些内容时可能被诱导去执行本不该执行的命令。这种攻击不需要多高的技术门槛一个恶意构造的 HTTP 响应头就能触发。另一类是自主执行过程中的策略漂移。Agent 在长时间运行后可能因为上下文过长、工具返回异常逐渐偏离最初的授权范围把扫面范围扩大甚至把宿主机自身当成了目标。所以整沙箱设计的底线思维应该是不管 Agent 是被误导了还是自己犯糊涂了它手里那把刀都不应该捅到我自己身上。宿主机上的文件、进程、网络、凭证、Docker 权限这些是绝对不能交出去的。明白这个目标之后后面所有的技术选型就都有了解题方向。2. 为什么是 Docker隔离边界的现实选择2.1 Docker 和虚拟机各自的边界在哪里先说句公道话Docker 容器的隔离强度确实不如虚拟机。容器和宿主机共享内核虽然用了 namespace 做隔离、cgroup 做限制但只要内核有漏洞理论上存在逃逸可能。所以如果你面对的是一个专业攻击者我不建议只靠 Docker 兜底。但 PentAGI 这种场景下的 Agent 不是专业攻击者它是一个可能犯错的自动化程序。用 Docker 做第一道隔离收益非常高启动快、销毁快。Agent 跑砸了一条docker rm -f就回到干净状态不用像虚拟机一样等半天快照回滚。可编排。用 Docker Compose 把整个实验环境固化到仓库里团队里的人拉下来就能复现。资源可控。cgroup 可以直接限制 CPU、内存、进程数、打开文件数Agent 再怎么失控也吃不完宿主机资源。镜像不可变。工具版本通过 Dockerfile 固定下来今天跑和三个月后跑环境一致。我自己在实验环境里跑这类 Agent 时会在 Docker 外面再加一层虚拟机。整个 Docker daemon 跑在一台专用的 VM 里这台 VM 才是我愿意暴露给 Agent 的“真实世界”。Docker 是第一道门虚拟机是第二道门这样即使容器逃逸发生攻击面也仅限于一台随时可以重置的 VM不会碰到我日常办公的宿主机。用坛子装酒、用柜子锁坛子道理是一样的。2.2 沙箱必须满足“可重建、可销毁、可复盘”给 Agent 做沙箱和给普通 Web 服务做容器有个很大的区别Agent 的运行过程本身具有高度不确定性和不可复现性。你今天让它跑一遍它可能走了一条完全不同的攻击路径。所以沙箱不能是一个“养着用”的长生命周期环境而应该是“用完即焚”的一次性环境。这就对沙箱提出了三个要求。可重建Dockerfile 和 Compose 文件必须能一键重新拉起整个环境不能依赖“某次手动 apt install”留下的现场。可销毁每次任务结束之后容器直接删掉所有中间状态交给日志系统去记录而不是留在容器里。可复盘Agent 每一步做了什么、调用了什么工具、输出了什么结果都要有完整的日志留存否则你根本没法复盘它为什么做出了某个危险动作。Docker 在这三点上的优势是天然的。镜像即环境定义容器即运行现场日志和卷是唯一的持久化出口。这也是我在标题里说“给 Agent 一个 Docker但别交出宿主机”的原因——Docker 既是授予 Agent 的活动空间也是我们在宿主和 Agent 之间画下的第一道红线。3. 动手搭一个 PentAGI 沙箱关键配置逐项拆解3.1 镜像基座工具集裁剪出“够用且可控”的环境沙箱镜像选什么常见的做法有两种。一种是直接用 Kali Linux 的官方 Docker 镜像里面预置了大量渗透测试工具省心但体积大攻击面也大很多工具 Agent 根本用不上。另一种是拿 Debian 或 Ubuntu 做底按需安装 Agent 实际会用到的工具镜像小依赖少出问题好排查。我建议偏向后者或者至少对 Kali 镜像做一次清理。以下是一个示例 Dockerfile只装了跑基础侦察扫描需要的工具FROM kalilinux/kali-rolling:latest ENV DEBIAN_FRONTENDnoninteractive RUN apt-get update \ apt-get install -y --no-install-recommends \ nmap \ sqlmap \ gobuster \ curl \ wget \ git \ python3-pip \ iputils-ping \ netcat-openbsd \ dnsutils \ jq \ --no-install-recommends \ rm -rf /var/lib/apt/lists/* RUN useradd -m -s /bin/bash agent \ mkdir -p /workspace/reports \ chown -R agent:agent /workspace USER agent ENTRYPOINT [/bin/bash]这里有几个细节要注意。第一指定--no-install-recommends避免装上一堆无关依赖。第二安装完立刻清理/var/lib/apt/lists/*把镜像体积压下来。第三创建一个普通用户agent容器入口不直接用 root后面配只读根文件系统时会少很多麻烦。第四预留/workspace/reports作为结果输出目录这个目录要挂载到宿主机权限在镜像构建时就定好。如果你跑的是 PentAGI 本身的 Python 服务还需要额外装一些 Python 依赖。我这里不展开具体的包名因为不同版本的 PentAGI 依赖差异很大。但有一个原则依赖装进镜像不要靠启动容器后再pip install。否则一重启就回到解放前。3.2 容器参数能力裁剪、只读根文件系统与强制安全配置镜像只是环境基础真正决定沙箱强度的是docker run或 Compose时的参数。下面是一份我在实验环境里实际使用的 Compose 配置每一行都有它的目的services: pentagi-agent: build: . container_name: pentagi-agent image: pentagi-sandbox:latest read_only: true tmpfs: - /tmp:size512m,mode1777 - /run:size64m,mode0755 cap_drop: - ALL cap_add: - NET_RAW security_opt: - no-new-privileges:true pids_limit: 256 mem_limit: 2g cpus: 2.0 ulimits: nofile: soft: 65536 hard: 65536 networks: pentest-net: ipv4_address: 172.28.0.100 volumes: - ./reports:/workspace/reports:rw logging: driver: json-file options: max-size: 50m max-file: 5 networks: pentest-net: driver: bridge ipam: config: - subnet: 172.28.0.0/24逐个解释关键参数。read_only: true把容器的根文件系统变成只读。设置之后Agent 无法修改容器里的任何已安装文件想往系统目录里写东西都不行。这能有效防止 Agent 被诱导后往系统里丢后门或者改配置。当然很多工具运行时需要在 /tmp 写临时文件所以用tmpfs给 /tmp 和 /run 分配了内存盘。注意 tmpfs 写进内存容器一停数据就没了这正好符合“一次性沙箱”的定位。cap_drop: ALL加上cap_add: NET_RAW是 Linux capabilities 裁剪的关键。默认情况下容器会继承宿主机赋予 root 的大量能力比如NET_ADMIN、SYS_ADMIN这些能力如果落到 Agent 手里它理论上可以搞网络配置、挂载文件系统非常危险。所以先把所有能力全部丢掉再只加回来跑渗透扫描必需的NET_RAWnmap 的 SYN 扫描、ping 需要原始套接字。如果 Agent 只做 HTTP 层测试连NET_RAW都可以不加。security_opt: no-new-privileges:true禁止容器内进程通过 setuid 等方式提升权限。这一条看着简单实际上能封掉一整类提权路径强烈建议开启。pids_limit、mem_limit、cpus、ulimits是资源上限。Agent 失控的常见表现是进程爆炸、内存吃满、fork 炸弹。PIDS limit 限制最大进程数即使 Agent 陷入死循环疯狂拉进程最多到 256 个就会报错不会拖垮宿主机。3.3 网络设计给 Agent 一条“看得见但出不去”的路网络配置是沙箱设计里最容易翻车的地方。很多教程为了方便直接用--network host让容器共享宿主机的网络命名空间。这样做等于把 Agent 直接暴露在宿主机所在的网络环境里它不仅能访问宿主机上的所有本地服务还能让宿主机的网卡接口全部对 Agent 可见。对一个渗透测试 Agent 来说这等于把“自己家”的大门钥匙交了出去。正确做法是使用自定义 bridge 网络上面 Compose 里的pentest-net就是干这个的。自定义 bridge 网络自带内建 DNS容器之间可以通过名称互相访问也方便我们给容器分配固定 IP。分配固定 IP 的意义在于你可以在宿主机上用 iptables 对 172.28.0.100 这个 IP 做精确的流量控制。网络策略的核心是回答两个问题Agent 需不需要访问外网Agent 的目标网络在哪里如果只是内网靶场实验我可以直接在宿主机上限制容器 egress 流量只允许访问靶场网段。命令大致是这样iptables -I FORWARD -s 172.28.0.100 -d 10.0.0.0/8 -j ACCEPT iptables -I FORWARD -s 172.28.0.100 -j DROP意思是只允许这个容器访问 10.0.0.0/8 的内网网段其他流量一律丢弃。如果 Agent 需要访问外网那就得在设计阶段想清楚它可能把扫描结果、读取到的主机信息传到外部这个行为到底能不能接受在真实渗透场景里这可能是合规要求但在实验室里我建议默认禁止出网需要时再逐个加白名单。还有一个小点容易被忽略容器里拿到目标网络之后宿主机自身往往也在这个网络里。Agent 可能因为误判把宿主机 IP 也列进扫描范围。我的做法是在 Agent 的配置文件里显式声明“禁止扫描宿主机及 Docker 网段”同时用 iptables 在宿主机层面做最后防线。技术上有时候防不住模型犯傻那就靠网络层强制兜底。3.4 结果与数据怎么安全地“带回来”Agent 在容器里生成的报告、扫描结果、漏洞利用的截图怎么安全地回传到宿主机很多同学第一反应是直接往宿主机的目录上挂 volume结果不仅挂载了目录还把整个宿主机的敏感目录也顺手挂进去了。我的方案是在镜像里预留一个专门的输出目录/workspace/reports挂载宿主机的./reports目录。关键是权限配置挂载的目录在宿主机上属于当前用户容器里如果用了 UID 不同的用户很容易出现写不进去的情况。最简单的做法是在 Dockerfile 里给agent用户指定一个和宿主机当前用户相同的 UID通过docker run --user或构建时指定。除了文件卷日志是另一个重要的回传通道。Docker 自带的 json-file 日志驱动会把容器里的 stdout 和 stderr 收集起来配合max-size和max-file做轮转可以避免日志无限增长。PentAGI 这类系统通常还会把每一条工具调用记录写到自定义日志里这些日志最好也输出到 stdout让 Docker 统一接管。这样宿主机上一条docker logs container就能看到 Agent 的所有动作。如果你要构建更正式一点的流水线可以在宿主机上跑一个 filebeat 或 vector把 Docker 日志和/workspace/reports里的文件统一采集到 Elasticsearch 或 Loki。这样 Agent 跑完之后报告、原始日志、流量包全部归档复盘时随时能查。3.5 Docker Socket 绝对不能直接交出去在所有沙箱配置里这是最不能妥协的一条。网上大量示例代码喜欢写-v /var/run/docker.sock:/var/run/docker.sock让容器里能直接调宿主机的 Docker API。对普通开发容器来说这已经是高风险操作对渗透测试 Agent 来说就是自杀式操作。为什么因为 Docker socket 本身就是一种 root 权限入口。拿到 Docker socket 的进程可以直接创建 privileged 容器可以挂载宿主机根目录到新容器里然后就能读写宿主机上的所有文件。换句话说你给了 Agent Docker socket就等于给了它宿主机 root。这完全违背了“不交出宿主机”的底线。如果 Agent 确实需要动态编排容器比如让某个工具在独立容器里跑正确做法是把编排能力收敛到一个独立的控制层。这个控制层跑在 Agent 容器之外的宿主机环境里对 Agent 只暴露一个窄接口比如一个 HTTP 服务只允许提交“启动一个指定镜像的容器”和“停止一个容器”的请求其他 Docker API 一概不开放。把所有危险操作包在业务语义里而不是直接把 Docker API 暴露给 Agent。你可能会说PentAGI 现在好像没有这种编排需求纯粹是自己跑工具就够了。那更好连这个口子都别开干脆不需要 Docker socket。4. 全自主不是无底线资源、时间与可观测性4.1 给 Agent 装上“监控”别等跑完了再后悔Agent 全自主跑起来之后最怕的不是它慢而是它偷偷做了危险操作你都不知道。所以要在一开始就把可观测性做足而不是等出事了再去补监控。我的习惯是三层日志并行。第一层是 Docker 日志收集容器的 stdout/stderr覆盖 Agent 调用工具时打印的所有回显。第二层是 PentAGI 自身的运行日志通常包括 LLM 的 prompt、模型回复、工具调用参数和结果摘要这一层能告诉你 Agent“为什么做这个决定”。第三层是网络流量日志在宿主机上对 Docker 网桥做流量镜像或抓包比如tcpdump -i br-xxxx -w /logs/pentagi-traffic.pcap这样即使 Agent 的日志被写坏或者被截断你手里还有一份原始流量包可以做证据链。三层日志配合才能做到“每一步都有迹可循”。4.2 时间盒与资源上限全自主不等于无限运行Agent 一旦全自主运行很可能进入一个“递归循环”——扫描不到结果就换参数重扫重扫不到就换工具换完工具又从头扫。如果不加限制它能跑一整天。所以在沙箱层面必须设置时间和资源的上限。资源上限前面已经说过了cgroup 做 CPU、内存、pids 限制。时间上限需要在编排层面做Docker 本身没有内置“运行超过 x 小时自动销毁”的机制但你可以写一个外部监控脚本定时检查容器启动时间超过阈值就执行停止和清理#!/bin/bash MAX3600 START$(docker inspect -f {{.State.StartedAt}} pentagi-agent) START_TS$(date -d $START %s) CURRENT_TS$(date %s) ELAPSED$((CURRENT_TS - START_TS)) if [ $ELAPSED -gt $MAX ]; then docker stop pentagi-agent docker rm pentagi-agent fi时间盒的好处不仅是防止资源耗尽还能倒逼 Agent 在有限时间内更聚焦地决策。真实渗透测试项目也是有时间窗口的这个限制本质上是在模拟真实约束。4.3 如果 Agent 真的“跑偏”了怎么止损止损方案的优先级从快到慢应该是停止容器、断开网络、销毁快照。docker stop已经把 Agent 的进程空间暂停了但如果它正在通过某个不在容器内的通道比如已经挂载到宿主机的卷做坏事就要立刻断开网络。在投入使用之前我建议你演练一遍止损过程。把这个沙箱投入真实任务之前先故意让 Agent 去执行一个越界操作看看它能不能被 Docker 层面的限制拦住日志能不能记录下来止损命令跑完需要几秒。不要等到真出事了才发现docker stop之后容器还卡在退出过程中。这里还有一个小技巧把整个 Docker daemon 跑在虚拟机里之后定时给虚拟机做快照。如果 Agent 逃逸了容器你还可以直接把整个 VM 恢复到几分钟前的状态。这个兜底虽然粗但非常可靠。5. 实操中的坑与排查记录5.1 容器里跑不了 nmap一堆工具“权限不足”很多第一跑沙箱的人都会遇到这个问题宿主机上 nmap 用得好好的放进容器里就报operation not permitted。原因很简单nmap 的 SYN 扫描和 host discover 需要创建原始套接字而容器默认没有NET_RAW这个 capability。解决办法就是我前面提到的在cap_add里加上NET_RAW。如果加了之后还是不行检查一下你的 Docker daemon 是否启用了 user namespace remapping某些配置会进一步屏蔽这些能力。另一种折中方案是让 Agent 优先使用 TCP connect 扫描nmap -sT这种扫描方式不需要原始套接字对权限要求低但痕迹也相对明显。我的建议是配置好NET_RAW然后用一个简单的 ping 命令先验证网络层再用nmap -sS验证扫描能力。基本排障路径跑通了再让 Agent 正式干活。5.2 Agent 访问目标网络总是“连不通”这个问题出现频率极高而且原因千奇百怪。最典型的是容器默认的 bridge 网络和宿主机 LAN 不互通容器里的进程能访问外网但访问不了宿主机局域网里的目标机器。解决办法是把容器网络模式切换到 macvlan或者直接把 Docker daemon 放在和靶机同网段的虚拟机里。还有一种常见原因是 DNS 解析异常。容器里的/etc/resolv.conf默认指向 Docker 内嵌 DNS如果目标机器只在内网做 host 解析Agent 很容易出现“域名解析失败”。这时候可以在镜像里写死/etc/hosts或者在docker run时用--dns指定内网 DNS 服务器。排查这类问题时要按顺序来先 ping IP再 ping 域名再用 curl 测试 HTTP 端口最后看 tcpdump 抓包结果。不要一上来就怪 Docker 网络配置很多时候是目标机器的防火墙或者 IDS 误拦了来自容器 IP 的流量。5.3 结果目录写不进去、报表文件权限一团乱这个坑几乎每个用 volume 挂载的人都会踩。容器里以 UID 1000 的用户运行宿主机上的挂载目录属于 UID 1001于是 Agent 写报表时直接 Permission denied。解决办法不复杂在 Dockerfile 里创建用户时指定 UID让容器内用户和宿主机用户保持一致。例如宿主机当前用户 UID 是 1001就改成useradd -m -u 1001 -s /bin/bash agent。如果实在改不了镜像可以在docker run时用--user参数覆盖 UID。接下来要注意的是不要把整个/workspace都挂载到宿主机只挂载/workspace/reports这一个子目录其他位置保持容器内部只读。这样即使 Agent 在容器里改了/workspace下的其他文件也不会影响宿主机。5.4 本地实验环境Docker Desktop / Windows的坑如果你只是在本地笔记本上做实验用 Docker Desktop 跑 PentAGI还有一些额外的坑。最典型的是 Docker Desktop 启动时报virtualization support not detected这通常是因为 Windows 的 Hyper-V 或 WSL2 虚拟化平台没有开启或者 BIOS 里的虚拟化扩展被禁用了。去系统设置里打开“虚拟机平台”再重启就能解决。另一个困扰是文件系统性能。Docker Desktop 在 macOS 和 Windows 上通过虚拟文件系统挂载宿主机目录性能比 Linux 原生环境慢不少跑大型扫描时 Agent 频繁读写报告目录会卡。如果 Agent 只是做轻量测试影响不大但跑大规模扫描还是建议直接用一台 Linux 机器或 Linux 虚拟机。5.5 排查速查表现象可能原因解决方式容器里无法 ping 通目标缺少 NET_RAW capabilitycap_add: NET_RAW容器里能 ping IP但解析不了域名Docker DNS 配置问题--dns 8.8.8.8或指定内网 DNS容器无法访问宿主机局域网bridge 网络隔离导致换 macvlan 或把 daemon 放到同网段 VM结果目录写不进去容器 UID 和宿主机 UID 不一致构建镜像时指定相同 UID容器 CPU 飙到 100% 卡死Agent 陷入循环设置--cpus和超时自毁脚本Docker Desktop 启动失败Windows 虚拟化未开启启用 Windows 虚拟机平台Agent 尝试修改系统目录根文件系统可写开启read_only: true和 tmpfs6. 把边界意识焊进 Agent 的工作流6.1 阶段式控制点全自主也要有“闸门”“全自主”是一个相对概念。真正的工程实践里我不会让 Agent 从侦察到利用全程无人值守。更稳妥的方式是分阶段授权侦察阶段完全自动利用阶段先进入“dry-run”就是把 Agent 准备执行的命令先打印出来经过人工确认之后再真正执行。PentAGI 这类系统在设计上本来就可以接入工具调用审批只不过很多人嫌麻烦关掉了。我的建议是首次跑新目标时务必打开审批等摸清了 Agent 的行为模式再逐步放开。这里不是不相信 Agent 的智能而是不相信未知网络环境的复杂性。就像新入职的安全工程师总要有个师傅盯几单再放手。6.2 用“策略文件”约束 Agent但别只靠提示词有些团队会在系统提示词里写一大堆“你不准做 xxx”然后指望模型100%遵守。这终究是概率事件不能作为安全边界。正确做法是提示词约束 代码层强制。在提示词层写清授权范围、禁止访问的网段、禁止删除目标上的数据、命令执行前要确认无害。在代码层用一个策略配置文件限制 Agent 能访问的网段和能调用的工具。我习惯用 YAML 写这个策略文件Agent 启动时读取工具调用接口会先校验这个策略再放行。比如scope: allowed_targets: - 10.10.0.0/24 forbidden_targets: - 127.0.0.1 - 172.28.0.0/24 max_retries: 3 forbidden_commands: - rm -rf / - shutdown - mkfs.*策略文件的价值在于即使提示词被注入或模型判断失误底层接口层依然会拒绝危险操作。安全不依赖模型的道德感这一点在 Agent 场景下尤其重要。6.3 后续还能怎么扩展这套沙箱方案跑顺之后可以继续往几个方向扩展。一个是把沙箱定义全部改成 GitOpsDockerfile、Compose 文件、策略文件都进仓库每次实验前从仓库拉取最新的环境定义避免“本地改了什么忘了记录”。另一个是把日志和报告接入自动化流水线任务结束后自动生成一份包含流量包、系统日志、Agent 决策链路的综合报告省去手动汇总的时间。如果实验规模变大还可以考虑给每个任务单独起一个 Compose 项目任务结束后整体销毁。这样不同的目标、不同的授权范围之间完全隔离Agent 的数据也不会串。我在实际跑 PentAGI 这类项目时最大的体会是别把 Agent 当成一个高级脚本要把它当成一个能力很强但经验不足的新人。给它工具之前先给它划好边界让它自由发挥之前想好它闯祸之后怎么收场。“给 Agent 一个 Docker但别交出宿主机”这句话看着像命令行技巧其实是一种安全工程的态度先想清楚失去控制的成本再决定赋予多少自由。