把 API Key 写进 CI 变量,本质上是在所有能读仓库的人、所有能看流水线配置的人、以及所有第三方 Action 之间共享一把长期有效的钥匙。这篇文章给出把它换成短期令牌的完整路径。
为什么 CI 里的密钥总是会漏
最常见的泄漏路径有五条,而且几乎每条都不是「被人黑进来看到了」:一是密钥被写死在 workflow 文件里,提交历史永久留存;二是 fork 仓库的 PR 自动拿到了父仓库的环境变量;三是第三方 Action 在运行时代码里可以读到环境变量并外传;四是调试日志把环境变量整体打印出来;五是密钥被复制进镜像层或构建产物。
这些问题之所以反复发生,是因为长期密钥「用起来太方便」——写一次,之后所有流水线都能用,没有人在意它是否应该还在有效期内。而当一个密钥长期有效且静态不变时,泄露到被利用之间的时间窗口,可以是几个月。
正确的方向是反过来设计:让流水线不再持有长期密钥,而是每次运行时临时换取一个短时效、范围受限、可审计的凭据。
- 五类常见泄漏路径:硬编码、Fork PR、第三方 Action、调试日志、镜像层
- 长期静态密钥把「泄露」放大成「长期可利用」
- 解决思路:运行期换取短期凭据
五步完成改造
第一步,盘点:搜索仓库里所有硬编码的凭证,以及所有配置了密钥的环境变量,列成清单。第二步,创建机器账号:在源钥里为你的 CI 创建独立的机器账号,只授予它真正需要的集合与操作,不复用个人账号的权限。第三步,换用 OIDC:用 OIDC 让流水线以工作负载身份换取短期令牌,而不是携带预置密钥。第四步,改造 Action:把取密钥的动作从「读环境变量」改为「调用源钥 API 换取令牌」。第五步,吊销旧密钥:确认新链路跑通后,把历史密钥全部轮换一遍。
这五步里最容易忽略的是第五步。改造完成但旧密钥没吊销,等于什么都没做——攻击者手里仍然握着那张长期有效的牌,只是你多了一条新路。
如果你的 CI 平台暂时不支持 OIDC,也可以先用源钥的短期访问令牌做过渡:令牌有效期设为 15 分钟,用完即弃,泄露窗口从「几个月」压缩到「几分钟」。
- 盘点 → 机器账号 → OIDC → 改造 Action → 吊销旧密钥
- 最后一步最容易被跳过,也最关键
- 无法上 OIDC 时,先用 15 分钟有效的短期令牌过渡
# 旧写法:长期密钥躺在环境变量里
- run: curl -H "Authorization: $DEPLOY_TOKEN" https://api.example.com/deploy
# 新写法:运行期换取短期令牌
- uses: fountkey/fk-action@v2
with:
secret-id: prod/deploy-token
permission: deployOIDC 联合与短期令牌
OIDC 联合(federation)的思路是:让 CI 平台用自己的身份向源钥证明「我就是那个工作负载」,源钥验证通过后签发一张短期访问令牌。令牌本身带有明确的范围(scope)、受众(audience)与过期时间,通常 5 到 15 分钟。
这带来三个直接好处。第一,不需要任何长期密钥存放在 CI 平台,密钥泄露面从「所有能看环境变量的人」缩小为「没有」。第二,令牌一旦过期即失效,即使在运行过程中被日志打印出来,攻击窗口也只有几分钟。第三,每次签发都会记录调用方、工作负载身份与用途,合规审计时可以直接调取。
验证的关键是「受信的身份提供者」:只有你在源钥里显式登记过的 CI 平台与仓库组合,才能换取令牌。别名冒充或 fork 仓库默认不在受信列表内,需要为 fork 场景单独配置更窄的权限。
- CI 用自身身份换令牌,平台侧不保存任何长期密钥
- 令牌带 scope、audience 与 5–15 分钟有效期
- 只有显式登记的受信平台与仓库才能换取令牌
机器账号与权限边界
机器账号是给程序用的身份,它和人的账号在模型上完全一致:有独立的主体标识、有独立的权限集合、有独立的审计记录,也有独立的吊销入口。区别只在于它用短期令牌而不是主密码登录。
权限边界要按「仓库 × 环境 × 操作」三个维度切分。举例:测试环境的构建只需要读取一组非生产配置;生产部署需要写权限,但只限于一个集合里的三个条目;数据库轮换任务需要单独的凭据条目,不与部署令牌共享。
我们建议遵守「一个流水线一个身份」的原则。共享身份会让审计日志失去意义——当出事时,你无法回答「是哪一个流水线拿走了这条密钥」。
- 机器账号与人账号模型一致,可独立吊销
- 按仓库 × 环境 × 操作三维切分权限
- 一个流水线一个身份,保证审计可追溯
Webhook、轮换与自动吊销
源钥提供 Webhook:条目被创建、修改、轮换完成时,事件会推送到你指定的服务端点。这让「密钥被读取之后立刻吊销」成为可能——当轮换流水线把新凭据写回目标系统后,旧条目即可作废。
数据库凭据可以交给源钥的自动密码轮换能力:按策略定期生成新密码,加密后写回目标系统,全程不以明文落盘;失败时自动保留旧密码并告警,避免把生产锁死。
动态密钥是更进一步的做法:按需生成数据库用户或短期 API 令牌,用完自动吊销,暴露窗口从「凭据存在的时间」缩短到「一次调用的时间」。
- Webhook 通知变更,便于写回后立刻吊销旧凭据
- 自动轮换全程加密回写,失败自动回滚并告警
- 动态密钥把暴露窗口压缩到单次调用
审计与告警
每次取用都会记录调用方、用途、时间与来源 IP。这条记录是发现异常的第一道线索:如果某个流水线在凌晨三点从陌生地址读取了一条生产凭据,你希望这件事立刻触发告警,而不是三个月后审计时才发现。
建议至少设置三条告警规则:取用来源不在白名单、短时间内的取用次数异常、以及某条密钥在极短时间内被多个不同主体使用。配合团队的审计日志外送,这些信号可以直接进 SIEM。
最后一点提醒:审计日志本身也是敏感数据。它记录了条目 ID、调用方与时间,虽然不含密码,但足以勾勒出你的系统结构。对外共享时要按组织维度裁剪。
- 取用记录包含调用方、用途、时间与来源 IP
- 建议三条告警:非白名单来源、频率异常、多主体共用
- 审计日志本身敏感,外送时按组织维度裁剪
落地清单
如果你只有半小时,按这个顺序做:先把硬编码的密钥挪进源钥(15 分钟),再给 CI 创建一个只读机器账号(10 分钟),最后确认所有仓库的密钥都不再出现在提交历史里(5 分钟)。这三步做完,你已经消除了绝大部分风险。
如果还有时间,继续做 OIDC 联合(半天)、改造 Action(半天)、轮换全部历史密钥(一天)。这部分工作量主要在与第三方 Action 的兼容性排查上。
需要评估你的流水线现状,可以直接写信到 [email protected],我们用中文回复,并可以提供一份针对主流 CI 平台的改造示例。
- 30 分钟:挪走硬编码密钥、创建只读机器账号、清理提交历史
- 1 天:OIDC 联合、改造 Action、轮换历史密钥
- 需要示例方案可联系 [email protected]
把这篇文章分享给同事
如果你正在推动团队迁移到通行密钥,这篇可能正好用得上。
沈亦舟
创始人兼 CEO
十年后端与分布式系统工程师,做过支付风控,也写过三年密码学工具。
关注方向:产品与工程