用 Docker给OpenClaw套上铠甲:容器沙箱隔离实战

5 阅读8分钟

ArmorClaw(www.armorclaw.cn) 客户端如何通过 Docker 容器化运行 openclaw,为OpenClaw提供生产级的安全隔离。本文将逐行拆解 ArmorClaw 的容器安全配置,展示如何构建一个「外柔内刚」的沙箱环境。

1. 为什么需要容器沙箱?

openclaw 的能力与风险

openclaw 是一个功能强大的 AI Agent 框架,它可以:

  • 执行任意 Shell 命令
  • 读写文件系统
  • 安装 npm/pip/go 工具
  • 访问网络资源
  • 启动子进程

这些能力让 AI Agent 非常强大,但也带来了巨大的安全风险。如果 openclaw 直接运行在宿主机上:

风险类型场景后果
恶意代码执行AI 被诱导执行 rm -rf ~/Documents重要文件丢失
资源耗尽AI 启动大量子进程或无限循环系统卡死
权限提升恶意 Skill 尝试获取 root 权限完全控制宿主机
数据泄露AI 读取 SSH 密钥、API Key敏感信息外泄

ArmorClaw 的解决方案:容器化

ArmorClaw 客户端通过 Docker 容器运行 openclaw,提供了多层隔离保护。以下是核心架构:

┌─────────────────────────────────────────┐
│         ArmorClaw 客户端            │
│  (Electron 应用,运行在宿主机)       │
└────────────┬────────────────────────┘
             │
             │ Docker API
             ▼
┌─────────────────────────────────────────┐
│         Docker 守护进程              │
└────────────┬────────────────────────┘
             │
             │ 容器隔离
             ▼
┌─────────────────────────────────────────┐
│      openclaw 容器 (沙箱)         │
│  - 文件系统隔离                    │
│  - 网络隔离                       │
│  - 资源限制                       │
│  - 权限最小化                     │
└─────────────────────────────────────────┘

2. 最小权限原则:逐行拆解 ArmorClaw 的 Docker 安全配置

ArmorClaw 在 docker-manager.ts 中实现了完整的容器安全配置。让我们逐行拆解。

--cap-drop ALL:移除所有 Linux capabilities

Linux capabilities 将 root 权限拆分成多个独立权限单元。默认情况下,容器拥有很多不必要的权限。ArmorClaw 使用 --cap-drop ALL 移除所有 capabilities,确保容器只能做「份内之事」。

// docker-manager.ts: startContainerFromImage()
const cmd = `${docker} run -d \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  ...`

为什么只添加 NET_BIND_SERVICE?

openclaw 需要绑定端口(默认 18789)提供 HTTP 服务,这是唯一需要的特权。其他所有权限都被移除。

演示:有无 cap-drop 的区别

# 没有 cap-drop 的容器
docker run --rm -it debian:bookworm-slim bash
# 容器内可以执行
capsh --print  # 查看当前 capabilities,有很多
mount -t tmpfs none /mnt  # 可以挂载文件系统

# 带有 --cap-drop ALL 的容器
docker run --rm --cap-drop ALL -it debian:bookworm-slim bash
# 尝试挂载文件系统
mount -t tmpfs none /mnt
# mount: /mnt: permission denied

--security-opt no-new-privileges:防止 setuid 提权

这个选项防止容器内的进程通过 setuid 二进制文件获取额外权限。即使容器内运行的是 root 用户,也无法获得额外的权限。

// docker-manager.ts
const cmd = `${docker} run -d \
  --security-opt no-new-privileges \
  ...`

演示:尝试通过 setuid 提权

# 在没有 no-new-privileges 的容器中
# 攻击者可以上传带有 setuid 位的二进制文件
# 利用内核漏洞进行提权

# 有了 no-new-privileges
# 即使容器内存在可利用的 setuid 二进制
# 内核也会阻止提权尝试

非特权用户运行

ArmorClaw 容器内的 openclaw 以非 root 用户运行,大幅降低容器逃逸的风险。

// 容器镜像中创建非 root 用户
// Dockerfile
RUN useradd -m -s /bin/bash node
USER node

对比:root vs 普通用户在容器逃逸中的风险差异

用户容器逃逸影响宿主机权限
root完全控制相当于宿主机 root
node有限破坏仍需绕过用户权限

3. 资源限制:防止”合法”的 DoS 攻击

即使 AI 是「善意」执行代码,一个无限循环或内存泄漏就足以让系统崩溃。ArmorClaw 通过资源限制提供最后一道防线。

CPU 限制:--cpus

// docker-manager.ts
const resources = getContainerResourceConfig()
const cmd = `${docker} run -d \
  --cpus=${resources.cpus} \
  ...`

// 默认配置
export const DEFAULT_CONTAINER_RESOURCES: ContainerResourceConfig = {
  cpus: 0.5,        // 0.5 个 CPU 核心
  memoryMB: 2048,    // 2GB 内存
  pidsLimit: 50,      // 最多 50 个进程
  nofileLimit: 256,    // 最多 256 个文件描述符
  diskLimitMB: 5120,   // 5GB 磁盘限制
}

用户可以在客户端 UI 中动态调整资源限制:

// docker-manager.ts: updateContainerResources()
async updateContainerResources(resources: ContainerResourceConfig): Promise<{...}> {
  await execAsync(
    `${docker} update --cpus=${resources.cpus} --memory=${resources.memoryMB}m --pids-limit=${resources.pidsLimit} ${CONTAINER_NAME}`,
    this.execOptions
  )
}

内存限制:--memory + --memory-swap

const cmd = `${docker} run -d \
  --memory=${resources.memoryMB}m \
  --memory-swap=${resources.memoryMB * 2}m \
  ...`

内存限制防止 AI 的无限循环或内存泄漏导致系统 OOM(Out of Memory)。swap 限制设置为内存的 2 倍,确保容器不会过度使用 swap。

进程数限制:--pids-limit(防 fork 炸弹

Fork 炸弹是最经典的 DoS 攻击方式,一个简单的递归调用就能耗尽所有进程资源。

const cmd = `${docker} run -d \
  --pids-limit=${resources.pidsLimit} \
  ...`

演示:fork 炸弹

# 没有 pids-limit
:(){ :|:& };:  # fork 炸弹
# 系统瞬间崩溃,所有进程耗尽

# 有 pids-limit
:(){ :|:& };:
# 进程数达到上限后,新进程被拒绝
# docker: Error response from daemon: OCI runtime create failed: ...

ArmorClaw 默认限制进程数为 50,足以运行 openclaw 和常用工具,但足以防止 fork 炸弹。

文件描述符限制

const cmd = `${docker} run -d \
  --ulimit nofile=${resources.nofileLimit}:${resources.nofileLimit} \
  ...`

文件描述符限制防止容器打开过多文件,耗尽系统资源。

4. 网络隔离:bridge 模式的正确姿势

为什么不用 host 模式

--network host 会让容器共享宿主机的网络命名空间,这意味着:

  • 容器内的服务直接暴露在宿主机端口
  • 容器可以访问宿主机所有网络服务
  • 容器内的进程可能绑定到宿主机已有端口

ArmorClaw 明确不使用 host 模式:

// config-manager.ts
export function shouldUseHostNetwork(): boolean {
  return false  // 统一使用 bridge 模式
}

如何只暴露必要端口

// docker-manager.ts
const networkFlag = useHostNetwork ? '--network host' : `-p ${CONTAINER_PORT}:${CONTAINER_PORT}`
const cmd = `${docker} run -d \
  ${networkFlag} \
  ...`

ArmorClaw 只映射容器需要的端口(18789),其他所有端口都无法从外部访问。

防止容器扫描宿主机服务

ArmorClaw 通过 DNS 配置限制容器的网络访问:

// docker-manager.ts: getHostDnsFlags()
private async getHostDnsFlags(): Promise<string> {
  const publicDns = ['223.5.5.5', '119.29.29.29']
  // ... 检测宿主机 DNS,过滤私网地址 ...
  const merged = [...new Set([...publicDns, ...hostPublic])].slice(0, 3)
  return merged.map(s => `--dns ${s}`).join(' ')
}

const cmd = `${docker} run -d \
  ${dnsFlags} \
  ...`

容器只能访问配置的 DNS 服务器,无法扫描宿主机的内网服务。

5. 文件系统隔离:volume 挂载的最小化

只挂载数据目录,不挂载整个 home

ArmorClaw 只挂载 openclaw 需要的数据目录,不挂载整个用户目录:

// docker-manager.ts
const dataDir = getOpenClawDataDir()
const cmd = `${docker} run -d \
  -v "${dataDir}:/home/node/.openclaw" \
  -v "${dataDir}/container-logs:/tmp/openclaw" \
  -v armorclaw-npm-cache:/home/node/.npm \
  -v armorclaw-go-cache:/home/node/go/pkg/mod/cache \
  -v armorclaw-uv-cache:/home/node/.cache/uv \
  ...`

好处

  • 容器内无法访问宿主机的其他目录
  • 即使 AI 执行 rm -rf /,也只会删除容器内的文件
  • 敏感文件(SSH 密钥、浏览器数据)完全隔离

工具安装隔离

openclaw 支持通过 Skill 插件安装各种工具(npm、pip、go install 等)。ArmorClaw 将这些工具安装到容器内的用户目录,不影响宿主机:

// docker-manager.ts
const userDirEnv = `-e NPM_CONFIG_PREFIX=/home/node/.npm-global \
  -e GOPATH=/home/node/go \
  -e PATH=/home/node/.npm-global/bin:/home/node/go/bin:/home/node/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`

const cmd = `${docker} run -d \
  ${userDirEnv} \
  ...`

好处

  • npm install -g 安装到 /home/node/.npm-global,不影响宿主机的 npm
  • go install 安装到 /home/node/go/bin,不影响宿主机的 Go 环境
  • 容器删除后,所有工具自动清理

ArmorClaw 还实现了工具安装清单机制,即使容器被删除,也能自动重新安装工具:

// installed-tools.ts
export function generateReinstallCommands(): string[] {
  const tools = listInstalledTools()
  const commands: string[] = []

  for (const tool of tools) {
    switch (tool.kind) {
      case 'node':
        commands.push(`npm install -g ${tool.package}`)
        break
      case 'go':
        commands.push(`go install ${tool.package}`)
        break
      case 'uv':
        commands.push(`uv tool install ${tool.package}`)
        break
    }
  }

  return commands
}

// docker-manager.ts: reinstallToolsFromManifest()
private async reinstallToolsFromManifest(): Promise<void> {
  const commands = generateReinstallCommands()
  for (const cmd of commands) {
    await execAsync(
      `${docker} exec ${CONTAINER_NAME} sh -c "${cmd}"`,
      { ...this.execOptions, timeout: 300000 }
    )
  }
}

6. 完整的 docker run 命令

将以上所有配置组合,就是 ArmorClaw 中实际使用的完整命令:

// docker-manager.ts: startContainerFromImage()
private async startContainerFromImage(image: string): Promise<boolean> {
  const proxyEnv = getDockerEnvVars()
  const useHostNetwork = shouldUseHostNetwork()
  const addHostFlag = needsAddHostFlag()
    ? '--add-host host.docker.internal:host-gateway'
    : ''
  const networkFlag = useHostNetwork ? '--network host' : `-p ${CONTAINER_PORT}:${CONTAINER_PORT}`
  const dataDir = getOpenClawDataDir()
  const logDir = getOpenClawLogDir()
  const dnsFlags = await this.getHostDnsFlags()
  const resources = getContainerResourceConfig()

  const cacheVolumes = `\
    -v armorclaw-npm-cache:/home/node/.npm \
    -v armorclaw-go-cache:/home/node/go/pkg/mod/cache \
    -v armorclaw-uv-cache:/home/node/.cache/uv`

  const containerLogsVolume = `-v "${dataDir}/container-logs:/tmp/openclaw"`

  const userDirEnv = `-e NPM_CONFIG_PREFIX=/home/node/.npm-global \
    -e GOPATH=/home/node/go \
    -e PATH=/home/node/.npm-global/bin:/home/node/go/bin:/home/node/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`

  const cmd = `${docker} run -d \
    --name ${CONTAINER_NAME} \
    --restart unless-stopped \
    ${networkFlag} \
    -v "${dataDir}:/home/node/.openclaw" \
    ${containerLogsVolume} \
    ${cacheVolumes} \
    -e OPENCLAW_GATEWAY_TOKEN=${GATEWAY_TOKEN} \
    -e OPENCLAW_PROXY_BASE_URL=${proxyEnv.baseUrl} \
    -e GOPROXY=https://goproxy.cn,direct \
    ${userDirEnv} \
    ${addHostFlag} \
    ${dnsFlags} \
    --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
    --cap-drop ALL \
    --cap-add NET_BIND_SERVICE \
    --security-opt no-new-privileges \
    --cpus=${resources.cpus} --memory=${resources.memoryMB}m --memory-swap=${resources.memoryMB * 2}m \
    --pids-limit=${resources.pidsLimit} \
    --ulimit nofile=${resources.nofileLimit}:${resources.nofileLimit} \
    ${image}`

  await execAsync(cmd, { ...this.execOptions, timeout: 120000 })
  return true
}

参数解析

参数作用安全意义
--name openclaw容器名称便于管理和引用
--restart unless-stopped自动重启策略服务可用性
-p 18789:18789端口映射只暴露必要端口
-v ...数据卷挂载最小化挂载范围
-e ...环境变量配置代理和工具路径
--add-host主机名解析容器访问宿主机
--dnsDNS 配置限制网络访问
--log-driver日志配置日志轮转,防止磁盘占满
--cap-drop ALL移除所有 capabilities权限最小化
--cap-add NET_BIND_SERVICE添加绑定端口权限最小必要权限
--security-opt no-new-privileges禁止权限提升防止提权
--cpusCPU 限制防止资源耗尽
--memory内存限制防止 OOM
--memory-swapSwap 限制防止过度使用 swap
--pids-limit进程数限制防止 fork 炸弹
--ulimit nofile文件描述符限制防止资源耗尽

7. 效果验证:模拟攻击场景

下面我们通过 6 种常见攻击场景,对比「裸奔」容器和「沙箱」容器的安全表现:

攻击场景裸奔容器ArmorClaw 沙箱
尝试 mount 挂载宿主机✅ 成功❌ 拒绝 (cap-drop ALL)
利用 setuid 提权✅ 可能成功❌ 阻止 (no-new-privileges)
Fork 炸弹耗尽进程✅ 系统崩溃❌ 进程数受限 (pids-limit=50)
无限循环耗尽内存✅ OOM 崩溃❌ 内存限制生效 (memory=2GB)
扫描宿主机网络✅ 可访问全部❌ 仅 DNS 可达
绑定宿主机端口✅ 可能冲突❌ bridge 网络隔离

实际测试

# 测试 1:尝试 mount
docker exec openclaw mount -t tmpfs none /mnt
# mount: /mnt: permission denied

# 测试 2:fork 炸弹
docker exec openclaw bash -c ':(){ :|:& };:'
# bash: fork: retry: Resource temporarily unavailable

# 测试 3:无限循环
docker exec openclaw bash -c 'while true; do :; done'
# 容器被 OOM Killer 终止,不影响宿主机

8. 资源监控与动态调整

ArmorClaw 提供了实时的容器资源监控接口:

// docker-manager.ts: getContainerResources()
async getContainerResources(): Promise<{
  limits: { cpus: number; memoryMB: number; pidsLimit: number; nofileLimit: number; diskLimitMB: number }
  usage: { cpuPercent: number; memoryUsageMB: number; memoryPercent: number; pids: number; netIO: string; blockIO: string; diskUsageMB: number }
  security: { capDrop: string[]; capAdd: string[]; securityOpt: string[]; networkMode: string; readOnly: boolean; user: string }
}> {
  const resourceConfig = getContainerResourceConfig()

  // 获取资源限制 + 安全配置
  const { stdout: inspectOut } = await execAsync(
    `${docker} inspect --format "{{json .HostConfig}}" ${CONTAINER_NAME}`,
    this.execOptions
  )
  const hostConfig = JSON.parse(inspectOut.trim())

  const limits = {
    cpus: hostConfig.NanoCpus ? hostConfig.NanoCpus / 1e9 : 0,
    memoryMB: hostConfig.Memory ? Math.round(hostConfig.Memory / 1024 / 1024) : 0,
    pidsLimit: hostConfig.PidsLimit || 0,
    nofileLimit: hostConfig.Ulimits?.find((u: { Name: string }) => u.Name === 'nofile')?.Hard || 0,
    diskLimitMB: resourceConfig.diskLimitMB || 5120,
  }

  const security = {
    capDrop: hostConfig.CapDrop || [],
    capAdd: hostConfig.CapAdd || [],
    securityOpt: hostConfig.SecurityOpt || [],
    networkMode: hostConfig.NetworkMode || 'default',
    readOnly: hostConfig.ReadonlyRootfs || false,
    user: '',
  }

  // 获取实时使用情况
  const { stdout: statsOut } = await execAsync(
    `${docker} stats ${CONTAINER_NAME} --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.PIDs}}|{{.NetIO}}|{{.BlockIO}}"`,
    this.execOptions
  )

  const parts = statsOut.trim().split('|')
  const usage = {
    cpuPercent: parseFloat(parts[0].replace('%', '')) || 0,
    memoryUsageMB: parseMemoryUsage(parts[1]),
    memoryPercent: parseFloat(parts[2].replace('%', '')) || 0,
    pids: parseInt(parts[3]) || 0,
    netIO: parts[4]?.trim() || '--',
    blockIO: parts[5]?.trim() || '--',
    diskUsageMB: 0,
  }

  // 获取磁盘使用
  const { stdout: duOut } = await execAsync(
    `${docker} exec ${CONTAINER_NAME} du -sm /home/node/.openclaw`,
    this.execOptions
  )
  const duMatch = duOut.trim().match(/^(\d+)/)
  if (duMatch) {
    usage.diskUsageMB = parseInt(duMatch[1]) || 0
  }

  return { limits, usage, security }
}

客户端 UI 可以实时显示容器的资源使用情况,并根据需要动态调整资源限制。

9. 容器生命周期管理

ArmorClaw 提供了完整的容器生命周期管理:

启动容器

// docker-manager.ts: startContainer()
async startContainer(): Promise<boolean> {
  const provider = await this.selectProvider()
  return provider.start()
}

停止容器

// docker-manager.ts: stopContainer()
async stopContainer(): Promise<boolean> {
  try {
    // 停止前提交快照,保留运行时安装的工具
    if (await this.isContainerRunning()) {
      try {
        await execAsync(
          `${docker} commit ${CONTAINER_NAME} ${SNAPSHOT_IMAGE}`,
          { ...this.execOptions, timeout: 120000 }
        )
      } catch (commitErr) {
        log.warn('Failed to commit snapshot before stop:', commitErr)
      }
    }
    await execAsync(`${docker} stop ${CONTAINER_NAME}`, this.execOptions)
    return true
  } catch (error) {
    log.error('Stop container failed:', error)
    return false
  }
}

重启容器

// docker-manager.ts: restartContainer()
async restartContainer(): Promise<{ success: boolean; message: string }> {
  try {
    // 提交快照保留工具
    let useSnapshotImage = false
    if (await this.containerExists()) {
      try {
        await execAsync(
          `${docker} commit ${CONTAINER_NAME} ${SNAPSHOT_IMAGE}`,
          { ...this.execOptions, timeout: 120000 }
        )
        useSnapshotImage = true
      } catch (commitErr) {
        log.warn('Failed to commit snapshot:', commitErr)
      }
    }

    // 删除容器
    await execAsync(`${docker} rm -f ${CONTAINER_NAME}`, { ...this.execOptions, timeout: 60000 })

    // 等待容器真正消失
    for (let i = 0; i < 20; i++) {
      if (!(await this.containerExists())) break
      await new Promise(r => setTimeout(r, 500))
    }

    // 从快照或基础镜像重建
    const baseImage = await this.getImageName()
    const started = await this.startContainerFromImage(useSnapshotImage ? SNAPSHOT_IMAGE : baseImage)

    return { success: started, message: started ? '容器已重启' : '重启失败' }
  } catch (error) {
    return { success: false, message: `重启失败:${error}` }
  }
}

10. 总结

ArmorClaw 通过 Docker 容器化运行 openclaw,提供了生产级的安全隔离:

  1. 最小权限原则--cap-drop ALL + --cap-add NET_BIND_SERVICE
  2. 防止提权--security-opt no-new-privileges
  3. 资源限制:CPU、内存、进程数、文件描述符限制
  4. 网络隔离:bridge 模式 + 端口映射 + DNS 配置
  5. 文件系统隔离:最小化 volume 挂载 + 工具安装隔离
  6. 实时监控:资源使用监控 + 动态调整
  7. 生命周期管理:启动、停止、重启 + 快照备份

这些配置共同构成了一个「外柔内刚」的沙箱环境:对外提供 AI Agent 所需的全部能力,对内将安全风险降到最低。

立即开始:访问 ArmorClaw 官网 下载安装,体验容器化的OpenClaw。