目标:配置安全策略,使用沙箱隔离,防止 AI 操作带来的风险
预计时间:35 分钟
对应官方文档:Security、Sandboxing、Sandbox Environments
安全威胁模型
AI 代理的风险
| 风险 | 示例 | 防护措施 |
|---|---|---|
| 数据泄露 | AI 上传代码到外部 | 网络隔离、DLP |
| 权限提升 | AI 修改系统文件 | 沙箱、权限控制 |
| 供应链攻击 | AI 安装恶意包 | 包管理审查 |
| 意外破坏 | AI 删除生产数据 | 备份、只读模式 |
| 提示注入 | 恶意输入诱导 AI | 输入验证 |
安全威胁模型图
沙箱选项对比
| 方案 | 隔离级别 | 复杂度 | 适用场景 |
|---|---|---|---|
| 内置 Bash 沙箱 | 进程级 | 低 | 日常开发 |
| Dev Container | 容器级 | 中 | 团队统一环境 |
| Docker | 容器级 | 中 | 自定义环境 |
| VM / 云沙箱 | 系统级 | 高 | 高安全要求 |
内置沙箱配置
默认限制
# Claude Code 内置沙箱默认限制:
- 不能访问 $HOME 以外的目录(项目目录除外)
- 不能修改系统文件
- 网络访问受限
- 环境变量隔离自定义沙箱规则
# .claude/sandbox.yaml
sandbox:
# 文件系统
filesystem:
allowed_paths:
- ./src
- ./tests
- /tmp/claude-work
denied_paths:
- ./secrets/
- ~/.ssh/
- /etc/
# 网络
network:
mode: restricted
allowed_hosts:
- pypi.org
- npmjs.org
- github.com
denied_hosts:
- "*"
# 命令
commands:
allowed:
- python
- pytest
- black
- git
denied:
- rm -rf /
- curl *
- wget *
require_confirmation:
- pip install *
- npm install *
- docker *Dev Container 配置
创建配置
// .devcontainer/devcontainer.json
{
"name": "Secure Claude Environment",
"image": "mcr.microsoft.com/devcontainers/python:3.11",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": ["anthropic.claude-code"]
}
},
"postCreateCommand": "pip install -r requirements.txt",
"remoteUser": "vscode",
// 安全加固
"runArgs": [
"--cap-drop=ALL",
"--security-opt=no-new-privileges"
],
// 挂载限制
"mounts": [
"source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
]
}使用
# VS Code 自动检测并启动 Dev Container
# 或手动启动
devcontainer up --workspace-folder .
# 在容器内运行 Claude Code
claude安全策略最佳实践
1. 最小权限原则
# 创建专用用户(不要 root)
useradd -m claude-worker
su - claude-worker
# 限制目录权限
chmod 700 /home/claude-worker/projects
chmod 500 /home/claude-worker/.claude2. 网络隔离
# 使用网络命名空间
sudo unshare --net --pid --fork --mount-proc /bin/bash
# 或使用 Firejail
firejail --net=none --private=. claude3. 命令审计
# 记录所有执行的命令
export PROMPT_COMMAND='history -a'
export HISTFILE=/var/log/claude-commands.log
# 实时监控
tail -f /var/log/claude-commands.log | grep -E "(rm|curl|wget|pip|npm)"4. 备份策略
# 自动备份(pre-session)
#!/bin/bash
# .claude/hooks/session-start.sh
BACKUP_DIR="/backups/claude/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
git bundle create "$BACKUP_DIR/repo.bundle" --all安全插件
安装安全审查插件
claude plugin install security-guidance配置安全规则
# .claude/security-rules.yaml
rules:
- id: no-hardcoded-secrets
pattern: '(password|secret|key|token)\s*=\s*["\'][^"\']+["\']'
severity: error
message: "禁止硬编码敏感信息"
- id: no-sql-injection
pattern: 'execute\s*\(\s*["\'].*%s'
severity: error
message: "可能存在 SQL 注入风险"
- id: check-dependency-vulnerabilities
command: "safety check"
severity: warning
- id: scan-for-secrets
command: "git-secrets --scan"
severity: error应急响应
发现异常时
# 1. 立即停止所有 Claude 进程
killall claude
# 2. 检查修改了哪些文件
git status
git diff
# 3. 回滚到安全状态
git reset --hard HEAD
# 4. 审查日志
cat ~/.claude/logs/latest.log | grep -i "error\|warning\|denied"
# 5. 报告安全团队
# 保存相关日志和 diff生产级安全加固方案
Docker Compose 完整配置
# docker-compose.security.yml
version: '3.8'
services:
claude-secure:
image: anthropic/claude-code:latest
container_name: claude-sandbox
# 安全选项
security_opt:
- no-new-privileges:true
- seccomp:./seccomp-claude.json
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
# 只读根文件系统
read_only: true
# 临时文件系统
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /home/claude/.cache:size=50m
# 网络隔离
networks:
- claude-isolated
# 资源限制
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# 健康检查
healthcheck:
test: ["CMD", "claude", "--version"]
interval: 30s
timeout: 10s
retries: 3
volumes:
# 只读挂载项目代码
- ./project:/workspace:ro
# 可写挂载(限制范围)
- ./workspace:/workspace/output:rw
# 配置文件
- ./security/claude-sandbox.yaml:/etc/claude/config.yaml:ro
networks:
claude-isolated:
driver: bridge
internal: true # 无外网访问Seccomp 配置文件
// seccomp-claude.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
"syscalls": [
{
"names": [
"accept", "bind", "clone", "close", "connect",
"execve", "exit", "exit_group", "fcntl", "fork",
"fstat", "getpid", "getrandom", "ioctl", "mmap",
"munmap", "open", "openat", "poll", "read",
"recvfrom", "sendto", "socket", "wait4", "write"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["chroot", "mount", "umount", "pivot_root"],
"action": "SCMP_ACT_KILL"
}
]
}多层防御纵深架构
生产环境不应依赖单一沙箱。建议把「网络 → 进程 → 文件 → 命令 → 审计」组成五层防御:
关键原则:每一层都假设上一层会被绕过。L1 只放 API、L2 拒绝 mount/ptrace/setns、L3 把 ~/.ssh 和 ~/.aws 排除在视野之外、L4 阻断 curl/wget、L5 把所有 ToolUse 事件实时送到 SIEM。
提示注入防御链路
大多数「AI 代理被劫持」的案例都源自外部内容(README、issue、网页、MCP server 返回值)夹带了对模型的指令。下面的检测链路把它当作「输入污染」处理:
配套的 UserPromptSubmit Hook 实现:
# .claude/hooks/prompt_injection_guard.py
"""对用户输入和外部内容做提示注入检测。
Claude Code 在 UserPromptSubmit 与 PreToolUse 都会调用本脚本,
通过 stdin 传入 JSON:{ "text": "...", "source": "user|tool" }
返回非 0 表示阻断。
"""
import json
import re
import sys
from pathlib import Path
SUSPICIOUS_PATTERNS = [
# 经典注入指令
r"(?i)ignore\s+(all\s+)?previous\s+instructions",
r"(?i)disregard\s+the\s+system\s+prompt",
r"(?i)you\s+are\s+now\s+(dan|developer\s+mode)",
# 试图触发危险工具
r"(?i)run\s+`?\s*(rm\s+-rf|curl\s+http|wget\s+http)",
r"(?i)exfiltrat\w*\s+(secrets?|tokens?|keys?)",
# 试图越权读取
r"(?i)cat\s+/etc/(passwd|shadow)",
r"(?i)~\/\.ssh\/id_(rsa|ed25519)",
# 隐写:零宽 / Unicode tag
r"[\u200b-\u200f\ufeff\ue0000-\ue007f]",
]
ALLOW_FILE = Path(".claude/security/prompt_allowlist.txt")
def load_allowlist() -> list[re.Pattern]:
if not ALLOW_FILE.exists():
return []
return [re.compile(line.strip()) for line in ALLOW_FILE.read_text().splitlines() if line.strip()]
def scan(text: str) -> list[str]:
hits = []
for pat in SUSPICIOUS_PATTERNS:
for m in re.finditer(pat, text):
hits.append(f"{pat} -> {m.group(0)[:80]}")
return hits
def main() -> int:
payload = json.loads(sys.stdin.read() or "{}")
text = payload.get("text", "")
source = payload.get("source", "user")
allow = load_allowlist()
if any(p.search(text) for p in allow):
return 0
hits = scan(text)
if not hits:
return 0
decision = {
"action": "block" if source == "user" else "flag",
"reason": "prompt-injection suspicion",
"hits": hits[:5],
}
sys.stderr.write(json.dumps(decision, ensure_ascii=False))
# 用户输入直接拒绝;外部内容只标记,让模型继续但加 system reminder
return 2 if source == "user" else 0
if __name__ == "__main__":
raise SystemExit(main())注册到 .claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{ "command": "python3 .claude/hooks/prompt_injection_guard.py" }
],
"PreToolUse": [
{ "matcher": "WebFetch|WebSearch|mcp__.*",
"command": "python3 .claude/hooks/prompt_injection_guard.py" }
]
}
}命令白名单 + 二次确认守门 Hook
沙箱再严,最容易出事的还是 Bash 工具。下面这个 Hook 同时实现:白名单、危险命令拦截、不可逆操作要求二次确认。
# .claude/hooks/bash_guard.py
import json
import os
import re
import shlex
import sys
from pathlib import Path
ALLOW = {
"git", "python", "python3", "pip", "pytest", "node", "npm", "pnpm",
"ls", "cat", "grep", "rg", "find", "head", "tail", "wc", "jq",
"black", "ruff", "mypy", "go", "cargo", "make",
}
DESTRUCTIVE = [
re.compile(r"\brm\s+-rf?\s+/"),
re.compile(r"\bdd\s+if=.+of=/dev/(sd|nvme|hd)"),
re.compile(r"\bmkfs\."),
re.compile(r"(?i)\b(curl|wget)\s+[^|]*\|\s*(sh|bash|zsh|python)\b"),
re.compile(r"\bgit\s+push\s+.*--force"),
re.compile(r"\bdocker\s+system\s+prune\s+.*--volumes"),
]
NETWORK_HOSTS_ALLOW = {"api.anthropic.com", "github.com", "pypi.org", "registry.npmjs.org"}
CONFIRM_FILE = Path(os.environ.get("CLAUDE_CONFIRM_FILE", ".claude/.last_confirmed_command"))
def classify(cmd: str) -> tuple[str, str]:
tokens = shlex.split(cmd, posix=True)
if not tokens:
return "allow", "empty"
head = os.path.basename(tokens[0])
if any(p.search(cmd) for p in DESTRUCTIVE):
return "deny", f"destructive-pattern: {cmd[:80]}"
if head not in ALLOW:
return "confirm", f"command not in allowlist: {head}"
if head in {"curl", "wget"}:
host_match = re.search(r"https?://([^/\s]+)", cmd)
host = host_match.group(1) if host_match else ""
if host not in NETWORK_HOSTS_ALLOW:
return "deny", f"network host not allowlisted: {host}"
if "rm" in head and ("-r" in tokens or "-rf" in tokens or "--recursive" in tokens):
return "confirm", "recursive deletion"
return "allow", "ok"
def main() -> int:
payload = json.loads(sys.stdin.read() or "{}")
cmd = payload.get("command") or payload.get("tool_input", {}).get("command", "")
decision, reason = classify(cmd)
if decision == "allow":
return 0
if decision == "deny":
sys.stderr.write(json.dumps({"action": "deny", "reason": reason}, ensure_ascii=False))
return 2
# confirm: 校验当前命令是否与上一次「人工确认」一致
last = CONFIRM_FILE.read_text().strip() if CONFIRM_FILE.exists() else ""
if last == cmd:
CONFIRM_FILE.unlink(missing_ok=True)
return 0
CONFIRM_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIRM_FILE.write_text(cmd)
sys.stderr.write(json.dumps({
"action": "confirm",
"reason": reason,
"hint": "再次发送相同命令以确认执行;或修改命令重新提交。",
}, ensure_ascii=False))
return 2
if __name__ == "__main__":
raise SystemExit(main())注册:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "command": "python3 .claude/hooks/bash_guard.py" }
]
}
}企业级实战场景
场景一:金融行业「PII 数据零外发」工作站
业务背景:某券商风控团队希望让分析师用 Claude Code 写策略代码,但合规要求客户身份信息(PII)和持仓数据不得离开内网。
威胁建模:
完整配置:
# /etc/claude/policy.yaml —— 通过 settings.json 引用
permissions:
network:
egress_proxy: "http://egress.corp.local:3128"
enforce_proxy: true
allow_hosts:
- api.anthropic.com
filesystem:
deny_globs:
- "**/customer_*.csv"
- "**/positions_*.parquet"
- "**/.env*"
- "**/secrets/**"
redaction:
# PreToolUse 时自动脱敏发送给模型的内容
patterns:
- name: id_card
regex: '\b\d{17}[\dXx]\b'
replacement: "[REDACTED_ID]"
- name: bank_card
regex: '\b\d{16,19}\b'
replacement: "[REDACTED_CARD]"
- name: phone_cn
regex: '\b1[3-9]\d{9}\b'
replacement: "[REDACTED_PHONE]"
hooks:
UserPromptSubmit:
- command: "python3 /opt/claude/hooks/dlp_inline.py"
PreToolUse:
- matcher: "Read|Write|Edit|Bash"
command: "python3 /opt/claude/hooks/dlp_inline.py"
audit:
sink: "syslog://siem.corp.local:6514"
include_tool_io: true
redact_secrets: true# /opt/claude/hooks/dlp_inline.py
import json
import re
import sys
PII = [
re.compile(r"\b\d{17}[\dXx]\b"), # 身份证
re.compile(r"\b1[3-9]\d{9}\b"), # 手机号
re.compile(r"\b\d{16,19}\b"), # 银行卡
re.compile(r"(?i)customer_[a-z0-9]+"),
]
def main() -> int:
data = json.loads(sys.stdin.read() or "{}")
blob = json.dumps(data, ensure_ascii=False)
for p in PII:
if p.search(blob):
sys.stderr.write(json.dumps({
"action": "deny",
"reason": "DLP: PII detected in tool I/O",
"pattern": p.pattern,
}, ensure_ascii=False))
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())部署清单:
- 出口仅放行
api.anthropic.com,所有请求经 Squid + ICAP DLP; - 工位本地
claude二进制由 SCCM 推送,settings.json 指向/etc/claude/policy.yaml,普通用户无写权限; - 运行账号在
/etc/sudoers.d/claude中显式禁用 sudo; - 每日 02:00 巡检脚本对比 hash,发现 settings 被改自动告警并恢复。
场景二:开源仓库「不可信 PR 自动审查」沙箱
业务背景:某开源项目维护者希望让 Claude Code 自动给外部 PR 写 review 评论,但 PR 里的 package.json、构建脚本、issue 模板都可能藏提示注入。
核心策略:所有外部内容一律视为 untrusted,跑在专用网络命名空间里,禁止接触主仓库 secrets。
# scripts/review-pr.sh —— 由 GitHub Actions 触发
#!/usr/bin/env bash
set -euo pipefail
PR_NUMBER="$1"
WORKDIR="$(mktemp -d /tmp/pr-review-XXXX)"
trap 'rm -rf "$WORKDIR"' EXIT
git clone --depth=1 --branch "pr-${PR_NUMBER}" \
https://github.com/example/project.git "$WORKDIR/src"
# 用 unshare 创建独立网络命名空间 + 只读挂载
exec unshare --net --pid --fork --mount-proc \
--user --map-root-user \
bwrap \
--ro-bind /usr /usr \
--ro-bind /lib /lib --ro-bind /lib64 /lib64 \
--ro-bind /bin /bin --ro-bind /etc /etc \
--tmpfs /tmp --proc /proc --dev /dev \
--bind "$WORKDIR/src" /work \
--chdir /work \
--setenv ANTHROPIC_API_KEY "$REVIEWER_KEY" \
--setenv CLAUDE_CONFIG_DIR /work/.claude-runtime \
--unshare-net \
-- \
claude --print --dangerously-skip-permissions=false \
--allowed-tools "Read,Grep,Glob" \
--max-turns 10 \
"$(cat /opt/review-prompt.md)"// .claude-runtime/settings.json(仅启动时生成、ro 挂载)
{
"permissions": {
"tools": {
"allow": ["Read", "Grep", "Glob"],
"deny": ["Bash", "Write", "Edit", "WebFetch", "WebSearch"]
}
},
"hooks": {
"UserPromptSubmit": [
{ "command": "python3 /opt/claude/hooks/prompt_injection_guard.py" }
],
"PreToolUse": [
{ "matcher": "Read",
"command": "python3 /opt/claude/hooks/path_guard.py" }
]
}
}# /opt/claude/hooks/path_guard.py —— 阻止读取仓库以外的路径
import json, os, sys
ROOT = os.path.abspath("/work")
def main() -> int:
data = json.loads(sys.stdin.read() or "{}")
path = data.get("tool_input", {}).get("file_path", "")
real = os.path.realpath(path) if path else ""
if not real.startswith(ROOT):
sys.stderr.write(json.dumps({
"action": "deny",
"reason": f"path escape: {real}",
}, ensure_ascii=False))
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())落地要点:
- API Key 通过 GitHub Encrypted Secrets 注入,且只授予
claude.ai/codereview 配额,不要复用主项目 key; --unshare-net配合--allowed-tools把WebFetch/WebSearch/Bash全锁死,模型即使被注入也无法外联;- review 结果通过 stdout 收集,再由外层 GitHub Actions 用
gh pr comment写回,AI 进程本身没有写仓库的权限。