每个流水线里都躺着一对 AWS AK/SK,或者一个万年不换的 kubeconfig------这不是段子,是绝大多数团队的现状。本文聊聊如何用 OIDC 把这些长期凭证从流水线里彻底清出去。
一、先算一笔账:长期密钥的真实成本
把云厂商的 Access Key 写进 CI/CD 变量里,是很多团队「让流水线能部署」的最快路径。但这条路径的隐性成本会随时间复利式增长:
- 泄露面不可控 。密钥一旦进入 CI/CD 变量体系,就会被带进作业环境、日志、镜像构建层,甚至被
env一句命令打出来。流水线日志的访问面远比想象中大。 - 轮换是永久负债。密钥需要定期轮换,而轮换意味着同时更新 N 个项目的变量、重启 N 条流水线、祈祷没有遗漏。
- 权限粒度对不上。AK/SK 绑定的是 IAM 用户,与「哪个项目、哪个分支、哪次部署」没有天然对应关系。一个拥有生产写入权限的密钥被测试流水线复用,是最常见的越权姿势。
- 审计只能事后追。日志里只看得到同一个 AK 在操作,分不清是哪个项目哪条流水线发的。
业界对这个问题已经有共识性答案:OIDC(OpenID Connect)联合认证 。CI 系统不持有任何长期密钥,而是在每次作业运行时签发一个短生命周期的 JWT 令牌,云厂商验证令牌后换发临时凭证。极狐GitLab CI/CD 从 15.7 起原生支持这一机制(使用 ID 令牌进行 OIDC 认证)。
二、原理:一次没有密钥的握手
整个流程里没有任何静态凭证,信任建立在 JWT 的签名与声明之上:
关键在于两个声明:
aud(audience) :令牌的预期受众,在.gitlab-ci.yml里显式声明,云厂商侧配置拒绝受众不匹配的令牌;sub(subject) :作业身份的唯一描述,默认格式为project_path:{group}/{project}:ref_type:{type}:ref:{branch_name},云侧信任策略据此做细粒度过滤。
这个设计的精髓是:「身份」不再是密钥本身,而是这次作业的上下文------哪个项目、哪个分支、是不是受保护分支,全都写进了不可伪造的令牌里。
三、落地:三步完成改造
第一步:在流水线里声明 ID Token
ID 令牌通过 id_tokens 关键字配置,一个作业可以按不同受众声明多个令牌:
yaml
job_with_id_tokens:
id_tokens:
FIRST_ID_TOKEN:
aud: https://first.service.com
SECOND_ID_TOKEN:
aud: https://second.service.com
script:
- first-service-authentication-script.sh $FIRST_ID_TOKEN
- second-service-authentication-script.sh $SECOND_ID_TOKEN
aud 绑定是纵深防御的关键:即便令牌泄露,它也只能通过受众匹配的那一个服务验证,把爆炸半径压到最小。
需要注意的历史包袱:老的 CI_JOB_JWT / CI_JOB_JWT_V2 变量在 15.9 已弃用、17.0 起移除。如果你的流水线里还有这两个变量的引用,升级版本前必须先迁移到 id_tokens,否则作业会在需要令牌的场景直接失败。
第二步:在云侧建立 OIDC 身份提供方与条件角色
以通用流程为例(AWS、Azure、GCP、HashiCorp Vault 的具体配置见官方连接到云服务文档):
- 在云厂商侧注册一个 OIDC 身份提供方,信任源指向极狐GitLab 实例的 JWKS 端点;
- 创建一个条件角色,其信任策略校验 JWT 的
sub与aud。
sub 的过滤规则非常灵活:
| 过滤目标 | 示例 |
|---|---|
| 仅 main 分支 | project_path:mygroup/myproject:ref_type:branch:ref:main |
| 任意分支 | project_path:mygroup/myproject:ref_type:branch:ref:* |
| 群组下所有项目 | project_path:mygroup/*:ref_type:branch:ref:main |
| 某个 tag | project_path:mygroup/*:ref_type:tag:ref:1.0 |
这意味着你可以精确到「只有 mygroup/myproject 的 main 分支能换取生产环境的凭证」,测试分支的作业在云侧直接被拒绝------粒度是「分支级」,远超 IAM 用户模型。
当作业声明了 environment 时,sub 声明中还可以包含环境相关字段(如 environment_protected、deployment_tier),信任策略可以进一步要求「只有部署到受保护环境的作业才能拿到生产凭证」,把 CI 权限模型与环境模型打通。
第三步:作业内换取临时凭证并使用
以 AWS 为例的典型作业:
yaml
publish:
stage: publish
id_tokens:
AWS_OIDC_TOKEN:
aud: sts.amazonaws.com
script:
- >
aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::<account>:role/gitlab-deploy
--role-session-name "gitlab-$CI_PIPELINE_ID"
--web-identity-token $AWS_OIDC_TOKEN
> /tmp/aws_creds.json
- export AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId /tmp/aws_creds.json)
- export AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey /tmp/aws_creds.json)
- export AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken /tmp/aws_creds.json)
- ./deploy.sh
凭证只在本次作业的生命周期内有效,作业结束即失效,不存在「忘了删」的问题。对于 HashiCorp Vault 场景,还可以直接使用 secrets 关键字,Runner 会自动用 ID Token 完成认证并注入密钥,连脚本都不用写。
四、踩坑复盘
改造过程中高频出现的几个问题:
- aud 不匹配是最常见的失败原因 。云侧身份提供方配置的 audience 必须与
id_tokens中声明的aud逐字一致。排查时先echo $OIDC_TOKEN | cut -d. -f2 | base64 -d解出 payload 对着看。 sub匹配是字符串精确匹配(除通配符外) 。项目改名、转移群组都会导致sub变化、信任策略失配。GitLab 提供了通过项目 API 将sub的首段切换为project_id的选项,用不可变 ID 绑定信任策略,可彻底规避路径变更问题。- 别忽略供应链审查 。官方文档明确提示:为流水线配置 OIDC,意味着所有流水线都获得了对目标环境的 JWT 访问能力。上线前应做一次供应链安全审查,重点是
sub过滤范围不要用过于宽泛的通配符。 - 临时凭证也有权限上限。换来的凭证继承条件角色的权限,角色策略仍要遵循最小权限------OIDC 解决的是「凭证怎么来」,不豁免「凭证能干什么」的治理。
五、写在最后
把长期密钥从流水线里清出去,本质上是一次信任模型的重构:从「持有秘密的人可以部署」变成「声明了正确上下文的作业可以部署」。前者靠流程约束,后者靠密码学保证。改造的工作量是一次性的(注册 IdP、建角色、改流水线),收益是永久性的(零轮换、零驻留、分支级粒度、天然审计字段)。
如果你的团队正在做安全合规整改,或者即将升级版本(旧 JWT 变量已移除),这正好是一个动手的窗口期。完整文档见:ID 令牌 OIDC 认证、连接到云服务。