适用场景
代码仓库、CI 日志、工单截图或聊天记录中出现了生产 API 密钥。密钥仍在被多个服务使用,直接禁用可能造成业务中断,但继续保留又会扩大攻击窗口。
本文面向服务到服务调用的静态 API 密钥,给出一套可执行的处置流程:先确认影响范围并限制风险,再创建权限更小的新密钥,通过短暂的双密钥窗口迁移调用方,验证新密钥流量后撤销旧密钥,最后完成审计和防复发。数据库密码、云平台访问密钥也可参考这套生命周期,但具体吊销方法应以对应平台的官方文档为准。
现象描述
常见告警包括:
- Secret Scanning 在提交历史中发现疑似令牌。
- CI 输出了完整环境变量或请求头。
- 应用日志记录了
Authorization、查询参数或完整异常请求。 - 同一密钥突然从陌生 IP、地域或 User-Agent 发起调用。
- 密钥的调用量、失败率或访问资源范围明显偏离基线。
发现密钥后,第一原则是把它视为已经泄漏。删除当前文件或重写 Git 历史不能让已经复制出去的密钥失效,真正结束风险必须依赖服务端撤销或轮换。
先判断:立即吊销还是短暂并行
处置顺序取决于是否存在正在滥用的证据:
- 已确认滥用或密钥权限极高:立即撤销旧密钥,即使会造成短时故障;随后恢复调用方。此时安全止损优先于无停机。
- 仅发现暴露、尚无滥用迹象:先创建新密钥并完成调用方迁移,再撤销旧密钥。并行窗口应以分钟或小时计算,不应无限延长。
- 平台不支持多个有效密钥:准备维护窗口或引入代理层完成切换,不要用“暂时不处理”代替方案。
OWASP 将创建、轮换、撤销和过期视为密钥生命周期的必要环节,并强调密钥应可快速撤销。GitHub 的 Secret Scanning 文档也建议在发现暴露凭据后立即轮换;是否短暂并行,只是降低迁移中断的工程手段,不改变旧密钥必须失效的结论。
可能的泄漏路径
1. 硬编码进入仓库
密钥可能存在于当前文件、历史提交、分支、Tag、Issue 或构建产物中。只搜索主分支最新版本会漏掉历史暴露。
2. 日志或可观测系统记录敏感头
反向代理、APM、异常追踪和调试中间件都可能采集完整请求头。日志保存周期通常长于应用容器生命周期,删除容器并不能删除日志副本。
3. 权限和使用范围过大
多个服务共享同一把管理员密钥时,既无法判断泄漏来源,也无法只撤销单个调用方。跨环境复用还会让测试环境泄漏直接影响生产。
4. 轮换缺少消费者清单
团队知道如何创建新密钥,却不知道哪些定时任务、脚本和第三方系统仍使用旧密钥,最终只能长期保留两把密钥。
处置流程
第一步:建立事件记录并保存必要证据
记录发现时间、密钥标识、所属系统、权限范围、暴露位置和负责人。证据中只保留密钥指纹,不复制完整密钥。可以用 HMAC 生成内部排查指纹,避免直接对低熵凭据做裸哈希:
"""生成仅用于事件关联的 API 密钥指纹。"""
import hashlib
import hmac
def build_key_fingerprint(api_key: str, audit_hmac_key: bytes) -> str:
"""返回不可用于认证的短指纹。"""
if len(audit_hmac_key) < 32:
raise ValueError("audit_hmac_key 至少需要 32 字节")
if not api_key:
raise ValueError("api_key 不能为空")
digest = hmac.new(
audit_hmac_key,
api_key.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return digest[:16]
audit_hmac_key 必须来自密钥管理系统,与业务 API 密钥分开保存。日志只记录 key_id 或上述指纹,禁止记录原始值、Authorization 头和完整请求体。
第二步:盘点消费者和权限
建立最小迁移清单:
| 字段 | 示例 | 用途 |
|---|---|---|
consumer |
billing-worker |
明确调用方所有者 |
environment |
production |
防止跨环境复用 |
key_id |
key_20260915_a |
审计与撤销定位 |
scopes |
invoice:read |
校验最小权限 |
deployment |
billing-worker-v42 |
确认哪个版本完成切换 |
last_seen_at |
UTC 时间 | 判断旧密钥是否仍有流量 |
如果调用方无法通过 key_id 区分,应先升级认证协议。只有一个无标识的密钥字符串时,服务端很难安全审计和灰度轮换。
第三步:创建权限更小的新密钥
新密钥应满足以下约束:
- 每个调用方、每个环境使用独立密钥。
- 只授予当前接口需要的 scope,不复制旧密钥的全部权限。
- 设置明确过期时间和负责人。
- 仅在创建瞬间展示明文,服务端保存不可逆校验值。
- 通过密钥管理系统或工作负载身份交付,不通过聊天和工单传递。
下面是一个 PostgreSQL 元数据表。示例只保存 secret_digest,不保存明文:
CREATE TABLE api_credentials (
key_id text PRIMARY KEY,
consumer text NOT NULL,
environment text NOT NULL,
secret_digest bytea NOT NULL,
scopes text[] NOT NULL,
status text NOT NULL CHECK (status IN ('active', 'grace', 'revoked')),
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz,
last_seen_at timestamptz
);
CREATE UNIQUE INDEX api_credentials_active_consumer_key
ON api_credentials (consumer, environment, key_id)
WHERE status IN ('active', 'grace');
grace 表示仅用于短暂迁移的旧密钥。生产实现还应记录创建人、撤销原因和审计事件,并限制只有认证服务能读取校验值。
第四步:让认证端短暂接受新旧两把密钥
请求应同时携带公开的 key_id 和私密的 secret,例如:
Authorization: ApiKey key_20260915_a.REDACTED_SECRET
服务端先按 key_id 查询候选记录,再使用恒定时间比较校验密钥,避免遍历全部凭据。以下示例演示核心边界:
"""校验带 key_id 的 API 密钥。"""
import hashlib
import hmac
from dataclasses import dataclass
from datetime import UTC, datetime
@dataclass(frozen=True, slots=True)
class Credential:
"""表示认证服务读取到的凭据元数据。"""
key_id: str
secret_digest: bytes
status: str
expires_at: datetime
def digest_secret(secret: str, server_hmac_key: bytes) -> bytes:
"""生成服务端保存的密钥校验值。"""
return hmac.new(
server_hmac_key,
secret.encode("utf-8"),
hashlib.sha256,
).digest()
def verify_api_key(
presented_secret: str,
credential: Credential,
server_hmac_key: bytes,
now: datetime | None = None,
) -> bool:
"""校验状态、过期时间和密钥内容。"""
current_time = now or datetime.now(UTC)
if credential.status not in {"active", "grace"}:
return False
if credential.expires_at <= current_time:
return False
presented_digest = digest_secret(presented_secret, server_hmac_key)
return hmac.compare_digest(presented_digest, credential.secret_digest)
示例中的服务端 HMAC 密钥同样需要由密钥管理系统托管。认证失败日志应包含 key_id、调用方、来源网络、状态码和 trace_id,但不得包含 presented_secret。
第五步:分批迁移调用方
建议按以下顺序切换:
- 将新密钥写入密钥管理系统的新版本,不覆盖旧版本。
- 先部署一个调用方实例或小比例任务。
- 确认新
key_id的成功率、延迟和权限拒绝符合预期。 - 逐批更新剩余实例、定时任务和灾备环境。
- 查询旧
key_id的最后使用时间,确认超过最长任务周期和连接缓存时间。
不要把新密钥放进镜像、Git 配置文件或普通环境清单。环境变量虽然比硬编码好,但仍可能出现在进程转储、诊断页面或错误日志中;具备条件时应使用短期动态凭据或工作负载身份。
第六步:撤销旧密钥并验证
迁移完成后把旧密钥标记为 revoked,不要只从调用方配置中删除。验证至少覆盖:
- 使用新密钥调用允许的接口返回成功。
- 新密钥访问未授权资源返回 403。
- 旧密钥无论来自哪个实例都返回 401。
- 旧
key_id再次出现时触发安全告警。 - 线上不存在仍引用旧密钥版本的实例、任务或灾备配置。
可用以下单元测试验证认证边界:
"""验证 API 密钥轮换期间的认证规则。"""
import unittest
from datetime import UTC, datetime, timedelta
from api_key_auth import Credential, digest_secret, verify_api_key
class ApiKeyAuthTest(unittest.TestCase):
"""覆盖有效、撤销、过期和错误密钥场景。"""
def setUp(self) -> None:
self.hmac_key = b"a" * 32
self.now = datetime(2026, 9, 15, tzinfo=UTC)
self.secret = "example-secret-for-test-only"
def build_credential(self, status: str = "active") -> Credential:
return Credential(
key_id="key_test_a",
secret_digest=digest_secret(self.secret, self.hmac_key),
status=status,
expires_at=self.now + timedelta(hours=1),
)
def test_active_key_is_accepted(self) -> None:
self.assertTrue(
verify_api_key(
self.secret,
self.build_credential(),
self.hmac_key,
self.now,
)
)
def test_revoked_key_is_rejected(self) -> None:
self.assertFalse(
verify_api_key(
self.secret,
self.build_credential("revoked"),
self.hmac_key,
self.now,
)
)
def test_wrong_key_is_rejected(self) -> None:
self.assertFalse(
verify_api_key(
"wrong-secret",
self.build_credential(),
self.hmac_key,
self.now,
)
)
if __name__ == "__main__":
unittest.main()
运行:
python -m unittest -v test_api_key_auth.py
第七步:清理暴露位置
只有在旧密钥已经撤销后,才进入清理阶段:
- 从当前代码、CI 变量、Wiki、工单、制品和日志中删除明文。
- 评估是否需要重写 Git 历史;这会改变提交哈希并影响所有协作者,必须单独制定计划。
- 检查仓库 Fork、缓存、镜像层和下载制品等副本。
- 保留不含秘密的事件时间线、指纹和审计记录。
历史清理不是撤销的替代品。即使无法彻底删除所有副本,只要服务端已撤销旧密钥,副本就不能继续认证。
监控与告警
轮换过程至少观察以下指标:
api_auth_requests_total{key_id,status_code}
api_auth_last_seen_timestamp_seconds{key_id}
api_auth_revoked_key_attempts_total{key_id}
api_auth_permission_denied_total{consumer,scope}
key_id 可以记录,原始密钥不可以作为标签。标签中也不要放用户 ID、完整 URL 查询参数或其他高基数字段。旧密钥撤销后仍有请求,可能是遗漏的合法消费者,也可能是攻击者;两种情况都必须调查。
预防措施
- 开启仓库 Secret Scanning 和 Push Protection,在提交进入共享仓库前阻断已知格式的密钥。
- CI 日志默认脱敏敏感变量,禁止
set -x、打印完整环境或记录 Authorization 头。 - 每个调用方和环境独立发放凭据,落实最小权限、过期时间和所有者。
- 优先采用短期动态凭据、工作负载身份或可自动轮换的密钥管理系统。
- 每季度演练“创建新密钥、灰度迁移、撤销旧密钥、回滚”的完整流程,并记录耗时。
- 为每把静态密钥维护消费者清单和最后使用时间,发现长期未使用时主动撤销。
- 对撤销密钥的再次使用建立高优先级告警,但日志只保留
key_id和必要上下文。
参考资料
总结
API 密钥泄漏处置的核心不是“把那一行代码删掉”,而是缩短凭据仍可被利用的时间。没有活动攻击迹象时,可以通过新旧密钥短暂并行实现无停机迁移;一旦确认滥用,则应立即撤销旧密钥,把安全止损放在可用性之前。
成熟的方案必须覆盖完整生命周期:独立身份、最小权限、安全交付、短期并行、流量验证、服务端撤销、审计告警和防泄漏控制。只有旧密钥在服务端明确失效,并且新密钥的权限和使用范围得到验证,轮换才算真正完成。
Discussion
评论