
把智能体脚本丢在宿主机上裸跑这事我干过后来被坑过一次就老实了。当时我本地跑一个叫 OpenClaw 的自动化脚本API 密钥就写在项目根目录的.env文件里进程一崩日志随手贴给朋友排查贴完才反应过来——日志里居然把密钥完整打出来了。更别提 Python 依赖动不动几十个包装一个新库就把旧环境搞坏最后只能重装系统。后来我把 OpenClaw 整个关进 Docker 沙盒镜像只读、权限收紧、密钥通过外部环境变量注入跑了一个多月再没出过岔子。这篇文章就把完整的做法拆开讲清楚适合正在用 OpenClaw 或者类似智能体脚本、又不想让密钥整天裸奔的开发者参考。1. 为什么要给 OpenClaw 上 Docker 沙盒先拆解“裸奔”到底裸在哪1.1 智能体本地运行的三宗罪密钥、环境与权限先说密钥问题。绝大多数本地脚本项目密钥的存放方式都长一个样项目根目录放一个.env文件或者直接在config.py里写死一行API_KEY sk-...。这种做法的危险点不在于你本机安全性有多差而在于它把密钥变成了一个“到处可复制”的东西。脚本异常时打印配置信息会带上它错误上报工具会把环境变量打包发到远程甚至有些人习惯把整个项目目录压缩后发到群里求助压缩包里就躺着那个明文密钥文件。OpenClaw 这类智能体项目尤其容易踩这个坑因为它本身就要对接外部 API运行时间长、日志量大出问题需要调试的概率比普通脚本高得多。第二个问题是环境依赖。OpenClaw 的运行依赖通常很复杂底层要装编译工具、不同版本的运行时、一堆 Python 包。直接跑在宿主机上全局环境会被一点点污染掉今天为 A 项目装了一个包明天 B 项目就 import 失败系统升级把 Python 版本一换之前所有虚拟环境全部失效。更隐蔽的是某些依赖包会有安装后脚本在系统里留下定时任务或者开机自启服务你根本不知道。隔离这个诉求用venv能解决一部分但解决不了系统级依赖和权限扩散。第三个问题最容易被忽略权限过大。宿主机上跑的脚本默认以你的用户身份运行拥有读你 SSH 私钥、写你 shell 配置、访问你桌面文件的权限。如果 OpenClaw 被远程投递的恶意数据打穿攻击者拿到的是你整个用户目录的读取权你存在~/.ssh/下的私钥、浏览器里保存的会话文件全都见了光。把这些合并成一件事来看本质就是你让一个不可完全信任的程序住进了你家主卧还把保险柜钥匙放它枕头下面了。1.2 为什么偏偏选 Docker而不是 venv、systemd 或虚拟机有人会问我用venv建个虚拟环境再用systemd服务跑起来是不是也能解决能解决一部分但离“沙盒”还很远。venv只隔离 Python 包不隔离文件系统权限脚本照样能读任意路径systemd能限制内存和 CPU但限制不了文件访问范围也没有“只读根文件系统”这种概念。虚拟机倒是隔离彻底可代价太大启动要几十秒镜像几个 GB 起步迁移备份麻烦就为跑一个脚本实在不划算。Docker 用的是内核级隔离加上镜像分层和不可变文件系统正好卡在“够用”和“轻量”之间。具体来说它有四个地方是其他方案给不了的隔离维度venvsystemd 服务虚拟机Docker 沙盒Python 包隔离有无有有文件系统只读无无可配原生支持权限能力裁剪无有限可配原生支持密钥不落镜像无无可配原生支持启动时间秒级秒级分钟级秒级Docker 的镜像构建机制还有一个关键优势密钥可以在运行时才注入根本不进入镜像文件。之后不管镜像怎么分发、怎么复制里面都不会残留密钥信息这一点对“防裸奔”来说是决定性的。2. 核心设计把密钥“关起来”而不是“藏起来”2.1 镜像选型的思路小、瘦、净先说明一点网上很多 Dockerfile 喜欢用最新的python:latest或者 unbuntu 全量镜像图省事但那对沙盒来说不是好事。镜像越大攻击面越大镜像包含的调试工具越多被攻破后攻击者能利用的工具也越多。我的原则是只用滑块软件不带任何额外的调试包运行用户用非 root装依赖时用--no-cache-dir把缓存清掉。基础镜像方面优先考虑python:3.11-slim这类瘦身版。不要一开始就上 alpine虽然它更小但某些依赖包需要现场编译容易栽跟头尤其是 OpenClaw 这类依赖复杂的项目需要先确认每个依赖在 alpine 的 musl 环境下都能正常安装。实测下来slim 镜像体积比完整版小一大半编译问题少安全维护也跟得上够用了。镜像里只装运行时必要的东西。比如 OpenClaw 如果只是跑定时任务调 API那 git 都不一定要装。表面上省几个包是小事实际上是在缩小“万一被打穿之后的可用工具集”。我通常还会在构建阶段用多个RUN分步安装最后用rm -rf /var/lib/apt/lists/*清掉包管理器的索引缓存进一步瘦身。2.2 密钥注入的三种方式与取舍如果密钥是在构建阶段写进镜像的——比如 Dockerfile 里ENV API_KEYsk-xxx或者COPY .env .——那基本等于白做沙盒。因为镜像层是历史记录密钥会永远留在层文件里任何一个能拿到镜像的人都能通过docker history或者解包镜像层把它翻出来。正确做法是把密钥注入挪到容器启动阶段。有三条路可选环境变量注入用docker compose的env_file或environment字段在创建容器时传入。这种方式最直接秘密不会进镜像只需要保证宿主机上的 .env 文件权限收紧。密钥文件挂载把密钥以文件形式挂载进容器路径例如/run/secrets/api_key。好处是容器内看起来有文件但实际是宿主机内存映射过去的不落盘坏处是单机用 Docker 走这套要额外维护一套绑定流程。外部密钥管理服务启动前先从加密存储拉取密钥再注入。安全性最高但复杂度也最高适合多人团队维护的场景单机自用有点重。方式安全性配置复杂度单机自用推荐度环境变量 env_file中高低强烈推荐密钥文件挂载高中推荐外部密钥管理很高高看需求我自己目前用第一条路宿主机上放一个secrets/openclaw.env文件权限设为 600compose 里用env_file引入。注意这个文件绝不能提交进 git要在.gitignore和.dockerignore里都排掉。实际效果比明文写在配置里强太多了容器内能看到环境变量但整个镜像里搜不到任何密钥痕迹。2.3 资源限制与最小权限原则沙盒的意义不仅在于“密钥不落盘”更在于“就算被打穿了也翻不出水花”。这里需要叠加几道配置。第一道是只读根文件系统read_only: true。容器内的根文件系统挂载为只读进程想往/usr或者/etc里写东西直接失败。但 OpenClaw 运行中肯定要写临时文件、缓存、数据所以要把一些路径单独声明为可写日志挂到./logs数据挂到./data临时目录用tmpfs映射到内存。内存型临时目录即使被写入容器一停就清空不会污染宿主机。第二道是删除内核能力cap_drop: ALL之后只加最小必要的能力。容器默认继承的内核权限比你想的多得多全部删掉可以挡住一大堆提权路径。再加上security_opt: no-new-privileges:true彻底禁止子进程获得更高权限。对 OpenClaw 这种跑在用户态的应用来说完全够用。第三道是资源配额mem_limit、cpus、pids_limit都设好。OpenClaw 里面跑 Agent 流程偶尔会 fork 出很多子进程或者吃掉大量内存不限的话可能把宿主机拖到卡死。用pids_limit限制进程总数防止 fork 炸弹式行为用mem_limit设置上限超过就触发 OOM容器被杀总比宿主机被拖死强。3. 实操全流程把 OpenClaw 装进 Docker 沙盒3.1 先规划目录哪些进镜像哪些不进镜像第一步不是写 Dockerfile而是先把目录结构理清楚。一个干净的 OpenClaw 部署目录应该长这样~/openclaw/ ├── app/ │ ├── main.py │ ├── agent.py │ └── requirements.txt ├── secrets/ │ └── openclaw.env ├── data/ ├── logs/ ├── .dockerignore └── docker-compose.yml关键点在于secrets/目录绝对不能进入 Docker 构建上下文。.dockerignore文件里写secrets/ .env **/__pycache__/ *.log .git/如果你的app/目录里本来就有.env构建时会全部打进镜像这是很多人踩过的坑。还有.git/目录也要排掉不仅仅是为了减小上下文体积还因为镜像里不应该出现源码历史记录泄露面越小越好。data/和logs/目录用挂载卷持久化代码更新时可以随时重建容器数据保留在宿主机上不丢失。3.2 Dockerfile 编写要点接着写一份可以直接抄的 Dockerfile。前提是你的 OpenClaw 是一个 Python 应用入口文件是main.pyFROM python:3.11-slim # 设置工作目录 WORKDIR /app # 创建非 root 用户注意版本差异 RUN groupadd --system openclaw useradd --system --gid openclaw --home-dir /app openclaw # 先复制依赖清单利用 Docker 缓存层 COPY app/requirements.txt . # 安装依赖并清理缓存 RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 复制源码并设置属主 COPY --chownopenclaw:openclaw app/ . # 切换为非 root 用户 USER openclaw # 声明默认命令 ENTRYPOINT [python, main.py]有几个细节要说清楚。第一COPY requirements.txt必须在COPY app/之前单独执行这样改了源码但依赖没变时docker 可以直接复用依赖层缓存构建速度能快非常多。第二非 root 用户一定在USER openclaw之前创建否则后续指令全以 root 身份执行镜像炸了权限配置也白搭。第三如果 OpenClaw 的入口文件依赖某些动态库slim 基础镜像缺什么就在构建阶段用apt-get补什么装完立刻清缓存。3.3 compose 编排与启动Dockerfile 只是把镜像搭好了真正的沙盒约束都写在docker-compose.yml里services: openclaw: build: . image: openclaw-sandbox:local container_name: openclaw-sandbox restart: unless-stopped env_file: - ./secrets/openclaw.env read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true mem_limit: 1g pids_limit: 256 cpus: 0.5 volumes: - ./data:/app/data - ./logs:/app/logs logging: driver: json-file options: max-size: 10m max-file: 3一行一行过一下。env_file用来注入密钥路径指向 secrets 目录宿主机上那个文件权限设置成600。read_only加tmpfs的组合解决只读文件系统写缓存的问题注意如果 OpenClaw 依赖~/.cache目录就得额外加一条tmpfs: /home/openclaw/.cache别问我怎么知道的问就是踩过。cap_drop: ALL是删权限的核心mem_limit和pids_limit按你的实际需求调整如果日志显示 OOM 就把内存上限提高一点。volumes只挂两个数据目录容器里面看到的是受限的定点可写区域。logging限制日志大小防止日志无限增长把磁盘占满。启动命令很简单cd ~/openclaw docker compose up -d --build docker compose logs -f--build会在启动前重新构建镜像确保代码是最新的。日志里如果能正常看到启动信息没有被权限错误或者密钥缺失报错就说明容器基本活着了。3.4 如何验证密钥没有“裸奔”部署完还要做一轮自查这一步很多人会跳过但它恰恰是避免出丑的关键。我会按顺序做四个检查。第一个检查确认镜像里没有密钥。用docker history openclaw-sandbox:local查看所有镜像层历史重点看有没有ENV带出敏感值再用docker save openclaw-sandbox | tar -tf -浏览镜像文件列表确认没有.env文件混在镜像层里。第二个检查确认运行中容器的密钥来源。docker inspect openclaw-sandbox能看到环境变量以明文形式出现在 Config.Env 里这是正常的因为它是注入的不是镜像自带的。第三个检查验证进程视角。进容器看一眼进程环境变量docker exec openclaw-sandbox cat /proc/1/environ | tr \0 \n这里能看到密钥确实存在说明注入生效。再把根文件系统权限过一遍docker exec openclaw-sandbox touch /etc/test会报错说明只读生效。第四个检查从外部宿主机角度确认密钥没有隐藏在日志或者镜像层的不可见角落。比如直接把挂载的 logs 目录grep -r sk-一遍确认没有把实际密钥写进运行日志。前面提到日志会打印密钥的问题就需要在源码里对日志输出做一层脱敏或者用过滤规则把敏感字段替换掉。4. 常见问题排查与避坑实录4.1 三个高频故障内存爆炸、权限拒绝、密钥失效第一类是内存持续增长然后容器被杀。OpenClaw 长时间跑 Agent 循环时如果没设置上限容器内存能涨到几个 G最后触发宿主机 OOM killer。排查命令是docker stats看 MEM USAGE 是不是持续走高解决方向是调高mem_limit同时在 OpenClaw 代码里加上内存缓存清理别让历史对话记录无限堆积。第二类是权限拒绝。典型报错是PermissionError: [Errno 13] Permission denied发生在 OpenClaw 试图往某个路径写文件时。原因基本都是只读文件系统不允许根目录写入而我没有把该路径加入挂载卷或者 tmpfs。解决思路很朴素确认它要写哪些目录把它们映射出去。如果只是缓存就加tmpfs配置如果是业务数据就挂载卷。第三类是密钥失效。容器正常启动但一调外部 API 就报 401 或者认证失败。我遇到过一次是宿主机上改了.env文件内容重新docker compose up -d却不重建容器导致旧的环境变量还留着。一定要用docker compose up -d --force-recreate强制重建容器或者先docker compose down再up否则你以为换了密钥实际容器里跑的仍然是旧的。4.2 怀疑密钥已经泄露四步急救万一哪天你发现自己把贴了密钥的日志发到公开渠道了别慌按这个顺序处理。第一步立刻撤销泄露的密钥。去对应的 API 管理后台把它禁用不管是不是真的泄露宁可错杀不可放过。密钥这东西跟内裤一样一旦被人看见过就该换掉不能再用了。第二步隔离和取证。把可能的泄露源单独拿出来检查日志文件、环境变量、镜像层用前文说的四个检查方法过一遍。确认泄露范围到底是只有日志还是镜像也有问题。第三步轮换新密钥。改掉宿主机上secrets/openclaw.env的值用docker compose up -d --force-recreate重建容器让新密钥生效。第四步补漏洞。如果日志会打印配置改为脱敏输出如果有.env被打包进镜像去改.dockerignore如果是因为权限太大导致被读走文件回头检查 cap_drop 和挂载卷配置。每次泄露都是系统完善的机会别只换个密钥就完事。4.3 日常维护升级、备份、轮换沙盒不是建完就万事大吉日常得管起来。升级 OpenClaw 依赖时改掉requirements.txt后重新docker compose up -d --build镜像会重建但挂载卷的数据不会丢这是容器方案的好处。备份也简单把data/目录打包一下就行整个容器随时可以用镜像重建。密钥轮换建议设一个周期。我是每两个月手动换一次方式很直接后台生成新密钥更新 env 文件重建容器跑一遍连通性检查。过程不到五分钟但习惯一旦养成风险就降下来一大截。如果你嫌手动麻烦可以写个 cron 脚本定时执行但要注意脚本本身不能把密钥明文打到日志里不然刚轮换完又裸奔一遍那就闹笑话了。5. 再进一步沙盒之外的硬核加固5.1 网络与进程隔离把攻击面收到最小容器网络默认走 bridge但这不意味着万事大吉。可以让 OpenClaw 容器只连接一个自定义内部网络同时不映射任何宿主机端口因为这类智能体通常只需要出站访问 API完全不需要对外提供入站服务。在 compose 里加一段networks: openclaw_net: internal: trueinternal: true的网络连出站访问都会被限制如果你的 OpenClaw 确实需要访问外网可以去掉 internal改用 iptables 控制出站规则只放行它需要用到的域名和端口。严格一点连 DNS 解析都可以走白名单。这个属于进阶玩法普通单机用户不必一上来就搞知道有这个方向就行。更需要警惕的是 docker.sock 问题。任何情况下都不要把/var/run/docker.sock挂载进容器即使你没这么挂也要在文档里写明这个风险。挂载了 docker.sock 等于把整个 Docker 引擎的控制权交给了容器攻击者拿到这个接口可以直接创建特权容器反手就把宿主机提权了。OpenClaw 这类运行不可信代码的容器尤其不能碰这个。5.2 日志脱敏与镜像扫描的习惯运行日志里出现密钥多数情况不是框架问题而是代码里打印配置对象太顺手了。我的习惯是写一个脱敏函数把所有键值对序列化前过一遍匹配到key、token、secret、password之类的字段名直接替换成***。另外日志滚动配置也要开别让日志文件无限增长否则密钥藏在几百 MB 的日志里想清理都无从下手。镜像扫描建议纳入常规流程。可以把 docker 镜像导出后丢给开源的镜像安全扫描工具扫一遍关键漏洞也可以只关心有没有敏感信息。有个更土但有效的办法把镜像导出成 tar 包后解包用grep -r搜常见密钥前缀比如sk-、AKIA这类特征串能看到就说明有泄漏。方法不高级但很管用。我发现还有个容易忽略的点容器内如果装了 git使用git clone拉取私有仓库时私钥或者 token 可能会被写进容器层的~/.gitconfig或者临时目录。一旦提交镜像又嵌进去了。所以尽量不在容器内做源码拉取代码应该由宿主机负责构建阶段处理容器只跑最终产物。5.3 自动化运维时最容易忽略的坑如果你用 cron 定时重启容器或者定时拉取更新有几个坑很实际。第一重建容器时不要用docker compose restart它只是停掉再启动同一个容器不重新读取 env_file 的变更要换配置必须docker compose up -d --force-recreate。第二容器内进程的时区默认是 UTC如果你的任务计划是针对本地时间的日志里能看到明明到点却没执行你需要在 compose 环境变量里挂TZAsia/Shanghai。第三容器重建后挂载卷里的文件属主可能变成 root而容器内运行用户是非 root目录权限不对会导致写不了文件解决方式是挂载时在宿主机先设置好属主和权限或者 compose 启动后手动chown一次。这些细节单独看不值一提但组合在一起就是真实运维中“莫名其妙出问题”的根源。沙盒方案的安全收益不是靠某个单独配置达成的而是靠这些边界条件一个不落地收口。我个人在实际操作中的体会是给 OpenClaw 上 Docker 沙盒最有价值的不是那一堆安全参数而是它逼着你把“密钥从哪里来、数据往哪里存、进程能访问什么”这三件事彻底想清楚了。以前我的项目结构是一团乱麻配置、数据、日志全堆在一起现在哪怕去掉沙盒光是这套分层的目录和权限习惯就足够让代码干净一个量级。如果你想做建议也别一次把所有加固全上先跑通基础容器确认业务正常再逐步加只读、加资源限制、加密钥注入。一次改一项出了问题你能准确知道是哪一项引起的比你一口气全配完然后对着报错猜半天要好处理得多。等这些都跑稳了再回头看那些裸奔在宿主机上的老脚本你会感谢自己当初下了这个决心。