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 | 主机名解析 | 容器访问宿主机 |
| --dns | DNS 配置 | 限制网络访问 |
| --log-driver | 日志配置 | 日志轮转,防止磁盘占满 |
| --cap-drop ALL | 移除所有 capabilities | 权限最小化 |
| --cap-add NET_BIND_SERVICE | 添加绑定端口权限 | 最小必要权限 |
| --security-opt no-new-privileges | 禁止权限提升 | 防止提权 |
| --cpus | CPU 限制 | 防止资源耗尽 |
| --memory | 内存限制 | 防止 OOM |
| --memory-swap | Swap 限制 | 防止过度使用 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,提供了生产级的安全隔离:
- 最小权限原则:
--cap-drop ALL+--cap-add NET_BIND_SERVICE - 防止提权:
--security-opt no-new-privileges - 资源限制:CPU、内存、进程数、文件描述符限制
- 网络隔离:bridge 模式 + 端口映射 + DNS 配置
- 文件系统隔离:最小化 volume 挂载 + 工具安装隔离
- 实时监控:资源使用监控 + 动态调整
- 生命周期管理:启动、停止、重启 + 快照备份
这些配置共同构成了一个「外柔内刚」的沙箱环境:对外提供 AI Agent 所需的全部能力,对内将安全风险降到最低。
立即开始:访问 ArmorClaw 官网 下载安装,体验容器化的OpenClaw。