API Key 管理与安全 - Agent 的密钥生命周期的管理 / API Key Lifecycle Management and Security for Agents
📅 创建时间:2026-05-08 🏷️ 标签:#APIKey #SecretsManagement #密钥轮换 #环境变量 #安全最佳实践 📚 前置知识:[[10-权限与门卫]]
📋 本章目标
- 理解 Agent 中 API Key 管理的特殊挑战
- 掌握密钥注入、环境变量隔离、密钥轮换的方法
- 理解密钥在沙箱和权限系统中的配合使用
- 掌握生产环境的密钥安全策略
- 理解密钥泄露的检测和响应
第1部分:Agent 中密钥管理的特殊挑战
1.1 为什么 Agent 让密钥管理更复杂?
┌─────────────────────────────────────────────────────────────┐
│ Agent 场景下的密钥风险 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 挑战1:Agent 可能"无意泄露"密钥 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Agent 读取了 .env 文件 │ │
│ │ 然后在解释任务时把整个文件内容发给了 LLM │ │
│ │ LLM 的 API 请求日志 → 密钥出现在日志中 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 挑战2:Agent 可能在输出中暴露密钥 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Agent 执行了 curl https://api.example.com │ │
│ │ 命令中包含 --header "Authorization: Bearer sk-xxx" │ │
│ │ 这个命令被记录到了 Agent 的执行日志中 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 挑战3:Agent 可能把密钥当作普通内容处理 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Agent 分析日志时说:"这个请求使用了 sk-xxx 密钥" │ │
│ │ 把密钥明文写进了摘要或报告 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 挑战4:密钥需要注入沙箱但又要隔离 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Agent 在 Docker 沙箱中运行 │ │
│ │ 但需要访问宿主机上的密钥 │ │
│ │ 直接挂载 → 沙箱外泄 │ │
│ │ 不挂载 → Agent 无法使用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 密钥类型与风险分级
┌─────────────────────────────────────────────────────────────┐
│ Agent 中的密钥类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 高风险(必须严格保护) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LLM API Keys(OpenAI / Anthropic / Google) │ │
│ │ 数据库密码 │ │
│ │ 云服务凭证(AWS / GCP / Azure) │ │
│ │ SSH 私钥 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 中风险(需要审计跟踪) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GitHub Token │ │
│ │ Slack / Discord Bot Token │ │
│ │ 第三方 API Key │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 低风险(可配置但需最小权限) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 搜索引擎 API │ │
│ │ 地图 API │ │
│ │ 天气 API │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:安全存储方案
2.1 密钥存储层次
┌─────────────────────────────────────────────────────────────┐
│ 密钥存储的五个层次 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 层次1:环境变量(基础) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ .env 文件 → 启动时注入到环境变量 │ │
│ │ 环境变量 → 进程内存 │ │
│ │ 优点:简单 │ │
│ │ 缺点:ps aux 可泄露 /proc/environ 可读 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 层次2:密钥管理服务(生产推荐) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault │ │
│ │ Agent 在运行时按需获取 │ │
│ │ 优点:集中管理、审计、轮换 │ │
│ │ 缺点:需要网络访问、有延迟 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 层次3:本地加密存储(轻量方案) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 系统密钥环(macOS Keychain / Linux keyctl) │ │
│ │ 操作系统级保护 │ │
│ │ 优点:无需网络、OS 级安全 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 .env 文件管理
┌─────────────────────────────────────────────────────────────┐
│ .env 文件管理策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 禁止行为: │
│ ❌ 将 .env 提交到 Git │
│ ❌ 在代码中硬编码密钥 │
│ ❌ 把 .env 路径作为日志内容输出 │
│ ❌ 在错误消息中包含密钥值 │
│ │
│ 正确做法: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # .env.local(不上 Git) │ │
│ │ OPENAI_API_KEY=sk-xxx │ │
│ │ ANTHROPIC_API_KEY=sk-ant-xxx │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # .env.example(必须提交,作为模板) │ │
│ │ OPENAI_API_KEY= ← 空值,标注需要的值格式 │ │
│ │ ANTHROPIC_API_KEY= │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # .gitignore │ │
│ │ .env │ │
│ │ .env.local │ │
│ │ .env.*.local │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.3 密钥注入到沙箱
import os
from pathlib import Path
import docker
class SecureKeyInjector:
"""安全地将密钥注入到 Agent 沙箱"""
# 只允许注入这些密钥(白名单)
ALLOWED_KEYS = {
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
"SEARCH_API_KEY",
}
def inject_to_docker(self, container: docker.models.containers.Container):
"""安全注入环境变量到 Docker 容器"""
env_to_inject = {}
for key in self.ALLOWED_KEYS:
value = os.environ.get(key)
if value:
# 检查值是否为空或 placeholder
if value and not value.startswith("${") and "YOUR_" not in value:
env_to_inject[key] = value
if env_to_inject:
container.exec_run(
f"env {' '.join(f'{k}={v}' for k, v in env_to_inject.items())}",
workdir="/"
)
def inject_to_bwrap(self, allowed_keys: list[str] = None) -> list[str]:
"""为 bwrap 生成安全的环境变量参数"""
keys = allowed_keys or list(self.ALLOWED_KEYS)
bwrap_args = []
for key in keys:
value = os.environ.get(key)
if value and not value.startswith("${"):
# 通过 --setenv 注入(不会出现在 ps aux 中)
bwrap_args.extend(["--setenv", key, value])
return bwrap_args
def create_readonly_env_file(self, output_path: str, allowed_keys: list[str] = None):
"""创建只读的 env 文件用于挂载"""
keys = allowed_keys or list(self.ALLOWED_KEYS)
lines = []
for key in keys:
value = os.environ.get(key, "")
if value and not value.startswith("${"):
lines.append(f"{key}={value}")
env_content = "\n".join(lines)
env_path = Path(output_path)
env_path.write_text(env_content)
env_path.chmod(0o400) # 只读,只有所有者可读第3部分:防止密钥泄露
3.1 审计日志脱敏
import re
import logging
from typing import Any
class SecretsObfuscator:
"""敏感信息脱敏器"""
PATTERNS = {
"openai_key": r"sk-[A-Za-z0-9_-]{20,}",
"anthropic_key": r"sk-ant-[A-Za-z0-9_-]{20,}",
"github_token": r"(ghp_|gho_|ghu_|ghs_|ghr_)[A-Za-z0-9_]{36,}",
"aws_key": r"(AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}",
"generic_bearer": r"Bearer [A-Za-z0-9_.-]{20,}",
"generic_api_key": r"['\"]?[a-zA-Z_]+[a-zA-Z0-9_]*['\"]?:\s*['\"]?[A-Za-z0-9_/-]{20,}['\"]?",
}
@classmethod
def mask(cls, text: str, show_prefix: int = 4) -> str:
"""脱敏文本,显示前缀"""
result = text
for label, pattern in cls.PATTERNS.items():
def replace(match):
value = match.group(0)
prefix = value[:show_prefix]
suffix = "*" * (len(value) - show_prefix)
return f"{prefix}{suffix} [REDACTED:{label}]"
result = re.sub(pattern, replace, result, flags=re.IGNORECASE)
return result
@classmethod
def check_for_secrets(cls, text: str) -> list[str]:
"""检测文本中是否包含密钥"""
found = []
for label, pattern in cls.PATTERNS.items():
if re.search(pattern, text, re.IGNORECASE):
found.append(label)
return found
class SafeLogger:
"""安全的日志记录器"""
def __init__(self, logger: logging.Logger):
self.logger = logger
def log(self, level: int, message: str, **kwargs: Any):
"""日志时自动脱敏"""
# 脱敏所有参数
safe_kwargs = {}
for k, v in kwargs.items():
if isinstance(v, str):
safe_kwargs[k] = SecretsObfuscator.mask(v)
else:
safe_kwargs[k] = v
self.logger.log(level, SecretsObfuscator.mask(message), **safe_kwargs)3.2 输出过滤器
class AgentOutputFilter:
"""Agent 输出过滤器,防止密钥泄露"""
def __init__(self):
self.obfuscator = SecretsObfuscator()
def filter_output(self, output: str, context: dict) -> str:
"""过滤 Agent 输出中的敏感信息"""
# 1. 脱敏处理
filtered = self.obfuscator.mask(output)
# 2. 检查是否无意中暴露了配置
if self._contains_exposed_config(output):
# 替换为提示
filtered = filtered.replace(
self._extract_exposed_line(output),
"# [配置内容已脱敏,请检查实际值]"
)
return filtered
def filter_command(self, command: str) -> str:
"""过滤 bash 命令中的密钥"""
# curl -H "Authorization: Bearer sk-xxx"
command = re.sub(
r'(-H\s+["\']Authorization:\s*)Bearer\s+[A-Za-z0-9_.-]+',
r'\1Bearer [REDACTED]',
command,
flags=re.IGNORECASE
)
# export KEY=value
command = re.sub(
r'(export\s+\w+\s*=\s*)[^\s]+',
r'\1[REDACTED]',
command
)
return command
def _contains_exposed_config(self, text: str) -> bool:
"""检查是否包含暴露的配置"""
patterns = [
r"api_key\s*=\s*['\"][A-Za-z0-9_-]{20,}['\"]",
r"token\s*=\s*['\"][A-Za-z0-9_]{20,}['\"]",
r"secret\s*=\s*['\"][A-Za-z0-9_]{20,}['\"]",
]
return any(re.search(p, text) for p in patterns)第4部分:密钥轮换与最小权限
4.1 密钥轮换策略
┌─────────────────────────────────────────────────────────────┐
│ 密钥轮换策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 轮换触发条件: │
│ 1. 定期轮换(建议每 90 天) │
│ 2. 密钥泄露事件后 │
│ 3. 人员变动(离职/转岗) │
│ 4. 疑似被滥用的迹象 │
│ │
│ 轮换流程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 生成新密钥 │ │
│ │ 2. 部署到密钥管理器(不直接替换旧密钥) │ │
│ │ 3. 观察期(1-2 周),确保旧密钥不被使用 │ │
│ │ 4. 旧密钥标记为"待废弃" │ │
│ │ 5. 确认所有系统已切换到新密钥 │ │
│ │ 6. 旧密钥正式作废 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 最小权限原则
# GitHub Token 的最小权限配置
MINIMAL_GITHUB_TOKEN_SCOPES = {
# 只读访问(分析任务)
"只读分析": ["repo:read", "read:user", "read:org"],
# 读写访问(代码修改)
"代码修改": [
"repo", # 完全访问公开仓库
# 不需要 repo:wite + admin,因为 Agent 不应该管理仓库设置
],
# PR 评论(审核任务)
"PR 审核": ["repo:read", "pull_requests:write", "issues:read"],
# 发布(发布任务)
"发布": ["repo", "packages:write"],
}
# AWS IAM 最小权限示例
MINIMAL_AWS_POLICY = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::agent-workspace/*", # 只在工作区桶
"arn:aws:s3:::agent-workspace" # 列出权限
]
},
{
"Effect": "Deny",
"NotAction": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::*" # 禁止操作其他桶
}
]
}第5部分:OpenClaw 的密钥管理
┌─────────────────────────────────────────────────────────────┐
│ OpenClaw 凭证存储 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 凭证存储位置: │
│ ~/.openclaw/credentials/ │
│ │
│ 存储格式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 每个 channel 的凭证独立存储 │ │
│ │ whatsapp-credentials.json.enc │ │
│ │ telegram-bot-token.json.enc │ │
│ │ github-oauth.json.enc │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 加密方式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文件使用 agent 启动时的会话密钥加密 │ │
│ │ 密钥不持久化在磁盘 │ │
│ │ 服务重启 → 需要重新认证 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 安全写入模式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 先写临时文件 → 验证 JSON 有效 → 原子替换旧文件 │ │
│ │ 如果写入失败,不覆盖已有的正确凭证 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第6部分:密钥泄露检测与响应
6.1 泄露检测
import time
from collections import defaultdict
class SecretLeakDetector:
"""检测 Agent 操作中的密钥泄露"""
def __init__(self):
self.alert_threshold = 3 # 同一密钥出现 3 次触发告警
self.seen = defaultdict(list)
def monitor(self, event_type: str, content: str, context: dict):
"""监控事件并检测泄露"""
found = SecretsObfuscator.check_for_secrets(content)
for secret_type in found:
self.seen[secret_type].append({
"time": time.time(),
"event": event_type,
"context": context.get("source", "unknown")
})
if len(self.seen[secret_type]) >= self.alert_threshold:
self._send_alert(secret_type, self.seen[secret_type])
def _send_alert(self, secret_type: str, events: list):
"""发送泄露告警"""
# 应该接入告警系统(Slack / PagerDuty / 邮件)
print(f"[CRITICAL] 检测到可能的密钥泄露:{secret_type}")
print(f"出现次数:{len(events)}")
for e in events[-3:]:
print(f" - {e['context']} @ {time.ctime(e['time'])}")6.2 泄露响应流程
┌─────────────────────────────────────────────────────────────┐
│ 密钥泄露响应流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 立即行动(1小时内) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 撤销/轮换泄露的密钥 │ │
│ │ • 确认泄露范围(哪些系统受影响) │ │
│ │ • 启动审计日志分析 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 影响评估(24小时内) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 密钥被访问了多少次? │ │
│ │ • 有没有被第三方使用? │ │
│ │ • 数据是否已泄露? │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 3: 修复措施 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 加强 Agent 的密钥隔离 │ │
│ │ • 添加输出过滤 │ │
│ │ • 更新检测规则 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 4: 复盘改进 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 分析根本原因 │ │
│ │ • 更新安全流程 │ │
│ │ • 培训团队 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:密钥管理原则
不暴露:密钥永远不出现在日志、输出、错误消息中
不硬编码:使用环境变量或密钥管理器
最小权限:只授予完成任务所需的最小范围
定期轮换:建立自动化的轮换机制总结2:安全注入优先级
生产环境:密钥管理服务(AWS Secrets Manager / Vault)
中等规模:系统密钥环(macOS Keychain / keyctl)
快速原型:加密 .env + 只读挂载总结3:泄露防护三道关
输入过滤 → Agent 无法读取敏感路径的密钥
执行过滤 → 命令和输出中的密钥被脱敏
审计监控 → 密钥异常访问触发告警章节测试
测试1:.env 安全
以下哪个做法是正确的? A. 把 .env 文件提交到 Git 作为模板 B. .env 文件中写入占位符如 OPENAI_API_KEY=${OPENAI_API_KEY} C. 在日志中使用 logger.info(f"API Key: {api_key}") D. 把 .env 路径记录在错误消息中
测试2:脱敏
如果密钥是 sk-ant-abc123xyz456789,显示前缀 4 位后,脱敏结果是什么?
测试3:存储层次
生产环境应该使用哪种密钥存储方案?
测试4:最小权限
Agent 只需要读取 GitHub 仓库内容,不需要写入,应该授予哪些权限? A. repo(完全仓库访问) B. repo:read(只读) C. admin:repo_hook(管理 Webhook) D. read:user(读取用户信息)
测试5:OpenClaw 凭证
OpenClaw 的凭证文件为什么使用会话密钥加密而不是持久密钥?
参考答案
测试1答案
答案:B
解析:A 危险(.env 不应提交到 Git);B 正确(使用占位符作为模板);C 危险(日志中暴露密钥);D 危险(路径也是敏感信息)。
测试2答案
答案:sk-an**** [REDACTED:anthropic_key]
解析:显示前缀 4 位 sk-an,后面用 * 替代,加上脱敏标记。
测试3答案
答案:AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault 等专用密钥管理服务。这些服务提供集中管理、审计日志、自动轮换和细粒度访问控制。
测试4答案
答案:B(repo:read)和 D(read:user)
解析:只读任务应只授予 repo:read 和必要的 read:user,不应授予 repo(包含写入权限)或 admin:repo_hook。
测试5答案
答案:会话密钥不持久化在磁盘,Agent 重启后需要重新认证。这意味着即使攻击者获取了加密的凭证文件,也无法解密(除非同时获取了会话密钥),提供了额外的安全层。
相关笔记
- [[10-权限与门卫]] - 权限系统与密钥的配合
- [[09-安全沙箱]] - 沙箱中的密钥隔离
- [[12-提示词注入防护]] - 提示词注入可能用于窃取密钥
下一步学习
- [ ] 阅读 21 - 提示词注入防护
学习状态:🟡 开始学习