Agent 输出里夹带了 secret

Agent 在生成代码、文档或日志时,把 API key、密码、token 或其他敏感凭证输出到了对话记录、trace 日志或下游系统里,造成安全风险。本文分析 secret 泄漏的触发路径并给出检测和阻断方案。

你的 Claude Code Agent 生成了一段数据库连接代码,里面有 DB_PASSWORD = "prod-secret-2026"——这个密码来自 Agent 读取的 .env 文件,它「帮忙」把配置内联进了代码。或者在 LangGraph 流水线里,一个 Debug Agent 为了帮助排查问题,把完整的 HTTP 请求(包含 Authorization: Bearer sk-... 头)输出到了 Langfuse trace 里,而 trace 数据对团队所有成员可见。更隐蔽的情况:Agent 在生成配置文件模板时,把从环境变量读到的真实值当成「示例值」填进了模板,下游把这个「模板」提交到了 git 仓库。

常见原因

1. Agent 读取了包含 secret 的上下文文件

Claude Code 或其他 Agent 使用了 Read 工具读取 .envconfig.yamlsecrets.json 等含有凭证的文件,这些文件的内容进入了 LLM 的上下文窗口,模型可能在后续的输出里引用或复现这些值。

怎么判断:检查 Agent 的 Read 工具调用记录,是否有对敏感文件的读取。在 CLAUDE.md 里搜索是否有「read .env」或「read secrets」的指令。

2. 错误日志或调试输出包含了请求内容

Agent 在调用外部 API 失败后,把完整的请求(包括认证 header)写进了日志或 Trace span。Authorization: Bearer sk-live-abc123 这样的字符串出现在 Langfuse dashboard 里,对有访问权限的所有人可见。

怎么判断:在 Trace 数据里搜索 Bearerapi_keypasswordsecret 等关键词,检查是否有凭证字符串出现在 span 的 inputoutputmetadata 字段里。

3. 代码生成时把环境变量的实际值内联进代码

Agent 收到「生成一个连接 PostgreSQL 的配置」的任务,它读取了当前环境的 DATABASE_URL,然后把这个 URL(包含密码)内联到了生成的代码里,而不是使用 os.environ.get("DATABASE_URL") 引用。

怎么判断:检查 Agent 生成的代码里是否有 postgres://user:password@host:port/db 这样的硬编码 DSN,而不是环境变量引用。

4. 对话历史没有脱敏就被发送给另一个 Agent

在多 Agent 流水线里,Agent A 的完整对话历史(包含用户消息里的凭证信息)被作为上下文传给 Agent B,Agent B 的输出里可能引用了这些凭证,并被记录到新的 Trace span 里。

怎么判断:检查 Agent 之间 handoff 的 payload,是否包含了原始对话历史而不是经过脱敏的摘要。

5. Prompt 里有「记录所有内容用于调试」的指令

系统 prompt 里有 总是在输出里包含你使用的所有配置信息,方便调试 这样的指令,导致 Agent 在每次输出时都附带了它读取到的配置(可能包含 secret)。

怎么判断:检查所有 Agent 的 system prompt,是否有「记录」、「输出配置」、「包含上下文」等可能导致 secret 外泄的指令。

6. 生成的代码被直接提交,没有经过 secret 扫描

Agent 生成了包含硬编码 secret 的代码,Promotion 流程没有做 secret 扫描,代码直接被提交到 git 仓库,触发了 GitHub 的 secret scanning 告警(或者更糟,没有告警,凭证就这样留在历史提交里)。

怎么判断:在 git 历史里运行 git log -p | grep -E 'sk-|password=|api_key=',检查是否有敏感字符串进入了提交记录。

最短修复路径

Step 1:在 Agent 输出写入 Trace 前做自动脱敏

import re

# 常见 secret 模式
SECRET_PATTERNS = [
    (re.compile(r'sk-[a-zA-Z0-9]{20,}'), 'sk-***REDACTED***'),
    (re.compile(r'Bearer\s+[a-zA-Z0-9\-_.]{20,}'), 'Bearer ***REDACTED***'),
    (re.compile(r'password["\s:=]+["\']?[^\s"\']{6,}["\']?', re.IGNORECASE), 'password=***REDACTED***'),
    (re.compile(r'(api[_-]?key["\s:=]+["\']?)[a-zA-Z0-9\-_]{16,}', re.IGNORECASE), r'\1***REDACTED***'),
    (re.compile(r'postgres://[^@]+@'), 'postgres://***:***@'),
    (re.compile(r'mysql://[^@]+@'), 'mysql://***:***@'),
]

def redact_secrets(text: str) -> str:
    """从字符串里脱敏所有已知的 secret 模式。"""
    for pattern, replacement in SECRET_PATTERNS:
        text = pattern.sub(replacement, text)
    return text

# 在所有 Trace span 写入前调用
def safe_log_span(span, input_text: str, output_text: str):
    span.update(
        input=redact_secrets(input_text),
        output=redact_secrets(output_text)
    )

Step 2:在 Claude Code 的 CLAUDE.md 里明确禁止读取 secret 文件

## 安全规则(不可违反)

- 禁止读取以下文件:.env, .env.local, .env.prod, secrets.yaml, credentials.json, *.pem, *.key
- 生成的代码中,所有配置值必须通过环境变量引用(如 os.environ.get("DB_PASSWORD")),禁止内联实际值
- 禁止在输出里包含任何看起来像 API key、密码或 token 的字符串
- 如果需要展示配置示例,使用占位符(如 "your-api-key-here")而不是真实值

Step 3:在代码提交前做 secret 扫描

# 安装 pre-commit hook,使用 trufflehog 或 gitleaks 扫描
pip install pre-commit
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
EOF
pre-commit install

# 手动扫描已有提交
gitleaks detect --source . --verbose

# 或用 trufflehog
trufflehog git file://. --only-verified

Step 4:在 Promotion 节点加 secret 检查

def check_output_for_secrets(output: str) -> list[str]:
    """检查 Agent 输出里是否包含疑似 secret。"""
    violations = []
    
    high_entropy_pattern = re.compile(r'[a-zA-Z0-9+/]{32,}={0,2}')
    for match in high_entropy_pattern.finditer(output):
        candidate = match.group()
        if shannon_entropy(candidate) > 4.5:  # 高熵字符串可能是 secret
            violations.append(f"高熵字符串(疑似 secret):{candidate[:8]}...")
    
    for pattern, _ in SECRET_PATTERNS:
        if pattern.search(output):
            violations.append(f"匹配到 secret 模式:{pattern.pattern[:40]}")
    
    return violations

def promotion_check_secrets(output: str, task_type: str) -> None:
    if task_type not in ("code_generation", "config_generation", "documentation"):
        return
    
    violations = check_output_for_secrets(output)
    if violations:
        raise SecurityViolation(
            f"Agent 输出包含疑似 secret,已阻断 Promotion:\n" +
            "\n".join(f"  - {v}" for v in violations)
        )

def shannon_entropy(text: str) -> float:
    """计算字符串的 Shannon 熵,高熵字符串可能是随机生成的 secret。"""
    import math
    from collections import Counter
    freq = Counter(text)
    length = len(text)
    return -sum((c/length) * math.log2(c/length) for c in freq.values())

Step 5:配置 git 仓库的 secret push protection

# GitHub 的 secret scanning(Enterprise 功能)
gh api repos/{owner}/{repo} --method PATCH \
  -f security_and_analysis.secret_scanning.status=enabled \
  -f security_and_analysis.secret_scanning_push_protection.status=enabled

# 本地:用 .gitignore 防止 secret 文件被意外提交
cat >> .gitignore << 'EOF'
.env
.env.*
!.env.example
secrets/
*.pem
*.key
credentials.json
EOF

Step 6:如果已经发生泄漏,立即轮换凭证

# 已知凭证泄漏的处理步骤(按优先级排序):
# 1. 立即在提供商控制台撤销泄漏的 key(GitHub: Settings > Developer Settings > PAT)
# 2. 生成新的 key 并更新所有依赖它的服务
# 3. 从 git 历史里删除 secret(git filter-branch 或 BFG)
# 4. 强制 push 清理后的历史(需要团队配合)
# 5. 通知可能受影响的用户

# 用 BFG 删除 git 历史里的 secret(比 filter-branch 快)
java -jar bfg.jar --replace-text secrets-to-remove.txt my-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive

预防建议

  • CLAUDE.md 里明确声明禁止读取的文件(.env、secrets.json 等),并说明配置应该通过环境变量引用
  • 所有 Trace span 写入前自动脱敏,使用已知 secret 模式的正则 + 高熵字符串检测双重保障
  • 在 Promotion 节点加 secret 检查,发现疑似 secret 时阻断输出
  • 安装 git pre-commit hook(gitleaks 或 trufflehog),防止包含 secret 的文件被意外提交
  • 启用 GitHub/GitLab 的 secret scanning push protection,作为最后一道防线
  • 定期对 Trace 数据做 secret 扫描,发现历史泄漏及时处理
  • 把 Agent 可读的文件范围限制在任务所需的最小集合里(principle of least privilege)

常见问答 (FAQ)

Q: Langfuse 存储的 Trace 数据如果包含了 secret,如何补救? A: Langfuse 支持通过 API 删除特定的 trace 或 observation 记录。立即通过 Langfuse API 删除包含 secret 的 trace(DELETE /api/public/traces/{traceId}),并检查是否有其他 trace 包含相同的 secret(批量搜索 + 删除)。同时撤销泄漏的凭证,因为无法确认 Langfuse 数据库在删除前是否被他人访问。

Q: 高熵字符串检测会不会误报(把合法的 Base64 内容当成 secret)? A: 会有误报,尤其是内容包含 Base64 编码的图片或文档时。可以通过以下方式降低误报率:1)只对特定上下文(如 key=value 格式)里的高熵字符串报警;2)维护一个「已知安全」的高熵字符串白名单(如常见的公开 CDN URL 的哈希值);3)对误报设置反馈机制,逐步完善规则。

Q: Agent 读取 .env 文件是正常的工作需求(如检查配置是否正确),如何在保证安全的前提下允许? A: 允许读取(用于检查),但禁止在输出里复现实际值。在 CLAUDE.md 里加入规则:「如果需要检查环境变量是否存在,只输出变量名和是否已设置(是/否),不输出变量的值」。同时在 Agent 的工具层对 .env 文件的读取做封装,只返回「key 是否存在」而不是 key 的值。

Q: 如果 secret 已经进入了 git 历史但还没被 push,能直接 git reset 清掉吗? A: git reset 只移动 branch 指针,不会删除历史提交里的数据。正确的做法是:1)git reset HEAD~1(撤销最近一次提交);2)git add .gitignore(把 secret 文件加入 .gitignore);3)git stash(暂存其他改动);4)重新提交时不包含 secret 文件。如果 secret 已经在多个提交里,必须用 BFG 或 git filter-repo 完整清理历史。

相关阅读

标签: #AI 编程 #Agents #排查