3620 字约 12 分钟
云原生构建支持在流水线中签发 OIDC id_token,向支持 OIDC 联合身份验证的服务 (云厂商、自建服务)证明「本次构建的身份」,进而换取短期凭证, 避免在仓库中存放长期密钥。
为什么使用 OIDC
传统做法是把云厂商的 SecretId / SecretKey 配置为仓库 密钥, 存在密钥集中存放、泄露面大、难以轮换等问题。使用 OIDC 后:
- 无长期密钥:仓库中不再存放云凭证,流水线运行时按需换取短期凭证。
- 认证与授权分离:谁能换取什么权限,由目标服务侧的信任策略与权限策略控制, 可精确到仓库、分支与事件。
- 凭证自动轮换:
id_token有效期 5 分钟,换取的临时凭证到期自动失效,无需人工轮换。
工作原理
① 一次性配置:在目标服务侧把 CNB 注册为可信任的 OIDC 身份提供商,并配置信任策略
② 流水线运行时:用 $CNB_TOKEN 调 CNB OpenAPI /{repo}/-/id_token 换取 id_token(JWT)
③ 插件或脚本把 id_token 交给云厂商(如腾讯云 STS AssumeRoleWithWebIdentity)或自有服务
④ 目标服务用 CNB 的 JWKS 公钥验签,并校验 aud / sub / exp 等声明
⑤ 校验通过后下发短期凭证,或返回业务数据CNB 侧提供的标准端点:
| 端点 | 说明 |
|---|---|
/.well-known/openid-configuration | OIDC 发现文档,含 issuer、jwks_uri 等 |
/.well-known/jwks.json | 签名公钥集合(RS256),供目标服务验签 |
站点的签发者(issuer)即实例地址 https://cnb.cool,对应端点为:
- 发现文档:
https://cnb.cool/.well-known/openid-configuration - 签名公钥集合(JWKS):
https://cnb.cool/.well-known/jwks.json
前提条件
- 所在实例已启用 OIDC 能力。
- 流水线由允许签发的事件触发(见下表)。PR、Issue、定时任务等事件的 ref 不可信或无换证场景, 平台会直接拒绝签发。
- 触发者具备仓库 开发者及以上权限。
- 请求中的
aud已在该实例注册。audience 需由管理员在管理端 「OIDC 配置 - 授权目标(Audience)」中创建,腾讯云场景填写sts.tencentcloudapi.com。
允许签发的事件
| 事件 | 触发来源 | 权限门槛 | sub 示例 |
|---|---|---|---|
push | Git 推送(平台签发) | — | my-group/my-repo:main:push |
tag_push | 推送 Tag(平台签发) | — | my-group/my-repo:v1.0:tag_push |
tag_deploy.{env} | 页面 / API 部署 | 推送代码 + 推送 Tag 权限 | my-group/my-repo:v1.0:tag_deploy.prod |
api_trigger[_{name}] | OpenAPI | 仓库写权限 | my-group/my-repo:main:api_trigger |
web_trigger[_{name}] | 页面按钮 | 仓库写权限 | my-group/my-repo:main:web_trigger |
id_token 声明
id_token 是一个 RS256 签名的 JWT,示例:
{
"iss": "https://cnb.cool",
"aud": "sts.tencentcloudapi.com",
"sub": "my-group/my-repo:main:push",
"iat": 1692000000,
"exp": 1692000300,
"jti": "b1f0e2a4-8c3d-4f6a-9e7b-2c5d8a1f3e09",
"repository": "my-group/my-repo",
"ref": "main",
"event_name": "push",
"actor": "robin",
"run_id": "5368227742",
"sha": "abc123..."
}| 声明 | 说明 |
|---|---|
iss | 签发者,实例的对外公网 HTTPS 地址 |
aud | 受众,即换取 id_token 时传入的 aud |
sub | 主体,三段式 {slug}:{ref}:{event},由服务端生成 |
iat / exp | 签发时间 / 过期时间,id_token 有效期 5 分钟 |
jti | 令牌唯一 ID,可用于防重放 |
repository | 仓库完整路径,同 CNB_REPO_SLUG |
ref | 分支名或 Tag 名(短名,无 refs/ 前缀) |
event_name | 触发事件名 |
actor | 触发者用户名 |
run_id | 流水线 ID |
sha | 当前提交的 SHA |
sub 三段式
| 段 | 含义 | 示例 |
|---|---|---|
{slug} | 仓库完整路径,同 CNB_REPO_SLUG | my-group/my-repo |
{ref} | 分支名或 Tag 名(短名,无 refs/ 前缀) | main / v1.0 |
{event} | 触发事件名 | push / tag_push / tag_deploy.prod |
sub 已包含仓库、引用、事件三个维度,目标服务只需匹配 sub 即可实现分支级、事件级的精确授权。 各段均由服务端依据流水线上下文生成,调用方无法覆盖。
注意
aud 只标识「凭证给谁用」,同一 audience 下所有仓库签发的 id_token 的 aud 完全相同, 无法区分是哪个仓库、哪个分支发起的请求。目标服务的信任策略必须同时校验 sub, 否则等于把权限开放给该实例上的任意仓库。
在流水线中获取 id_token
接口
POST {CNB_API_ENDPOINT}/{repo}/-/id_token
Authorization: Bearer $CNB_TOKEN
Content-Type: application/json
{"aud":"sts.tencentcloudapi.com"}请求体只允许传 aud,sub 及其余声明全部由服务端依据流水线上下文生成,调用方不可覆盖。
响应:
{
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}提示
响应体不返回 expires_in。id_token 的实际有效期以 JWT 内的 exp 声明为准, 调用方应解析 exp 判断剩余有效期。
示例
main:
push:
- stages:
- name: 换取 id_token
image: alpine
script: |
apk add --no-cache curl jq
ID_TOKEN=$(curl -sS -X POST "$CNB_API_ENDPOINT/$CNB_REPO_SLUG/-/id_token" \
-H "Authorization: Bearer $CNB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"aud":"sts.tencentcloudapi.com"}' | jq -r '.id_token')
# 不要把 ID_TOKEN 打印到构建日志单条流水线 1 分钟内最多签发 10 次 id_token,超出将返回 429。
与腾讯云集成
使用 tencentcloud-oidc-auth 插件, 可在流水线中免密获取腾讯云 CAM 临时密钥,无需在仓库配置 SecretId / SecretKey。
步骤一:腾讯云侧配置(一次性)
1. 创建 OIDC 身份提供商
前往新建身份提供商(CAM 控制台 → 身份提供商 → 角色 SSO → 新建身份提供商), 按控制台表单填写:
| 配置项 | 取值 |
|---|---|
| 提供商类型 | 选择 OIDC |
| 身份提供商名称 | 建议使用域名(如 cnb.example.com),便于区分同一 CAM 账号下的多个 CNB 实例;该值即提供商标识,需与下文信任策略的 oidc-provider/<名称> 及插件 provider_id 参数保持一致 |
| 身份提供商 URL | CNB 的 issuer 地址,必须与 id_token 的 iss 完全一致,即 https://cnb.cool |
| 客户端 ID | sts.tencentcloudapi.com |
| 身份提供商公钥 | 访问本站 JWKS 端点 https://cnb.cool/.well-known/jwks.json,复制公钥内容粘贴至此 |
2. 创建 CAM 角色并配置信任策略
创建「身份提供商」类型的角色,选择上一步创建的提供商,信任策略如下:
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": ["sts:AssumeRoleWithWebIdentity"],
"principal": {
"federated": ["qcs::cam::uin/<主账号ID>:oidc-provider/cnb.example.com"]
},
"condition": {
"string_equal": {
"oidc:aud": "sts.tencentcloudapi.com",
"oidc:sub": "my-group/my-repo:main:push"
}
}
}
]
}按粒度从粗到细:
- 仓库级(最低要求):
"oidc:sub": "my-group/my-repo:*",使用string_like - 分支级:
"oidc:sub": "my-group/my-repo:main:*",使用string_like - 精确锁定:
"oidc:sub": "my-group/my-repo:main:push",使用string_equal
需要授权多个分支或事件时,可在 statement 数组中配置多条, 或对分支段使用 string_like 通配(如 my-group/my-repo:release-*:push)。
警告
信任策略必须配置 oidc:sub 条件,且至少精确到仓库级别({slug})。
oidc:aud 只能校验受众是否为 sts.tencentcloudapi.com,所有 CNB 流水线的 id_token 都相同, 无法区分签发主体。缺少 oidc:sub 限制时,任何 CNB 用户都能用自己任意仓库签发的 id_token 扮演你的角色,获取该角色名下的全部云资源权限。禁止使用裸 % 通配。
3. 为角色绑定权限策略
按最小权限原则为角色绑定策略,如 QcloudCOSDataWriteOnly、QcloudCDNPushOnly 或自定义策略。
步骤二:流水线配置
main:
push:
- stages:
- name: 腾讯云 OIDC 免密
image: tencentcom/tencentcloud-oidc-auth
settings:
role_arn: qcs::cam::uin/123456789:roleName/cnb-deploy
provider_id: cnb.example.com
region: ap-guangzhou
- name: 准备 Python 环境
image: python:3-slim
script: |
python -m venv "$CNB_BUILD_WORKSPACE/.venv"
"$CNB_BUILD_WORKSPACE/.venv/bin/pip" install -q --disable-pip-version-check tccli
- name: 验证凭证
image: python:3-slim
script: |
. "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
"$CNB_BUILD_WORKSPACE/.venv/bin/tccli" sts GetCallerIdentity
- name: 部署
image: python:3-slim
script: |
. "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
"$CNB_BUILD_WORKSPACE/.venv/bin/tccli" cos PutBucket --Bucket my-bucket --Region "$TENCENTCLOUD_REGION"
- name: 示例 coscli
image: tencentcom/coscli
script: |
. "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
coscli ls cos://my-bucket --init-skip \
-e "cos.${TENCENTCLOUD_REGION}.myqcloud.com" \
-i "$TENCENTCLOUD_SECRET_ID" \
-k "$TENCENTCLOUD_SECRET_KEY" \
--token "$TENCENTCLOUD_TOKEN" \
--limit 10提示
coscli 与 tccli / SDK 不同,不读取 TENCENTCLOUD_* 环境变量,需用 -i / -k / --token 显式传入凭证,并用 --init-skip 跳过交互式初始化;cos:// 后须为完整桶名(含 APPID)。 ls 需要 bucket 级权限(cos:HeadBucket / cos:GetBucket), 仅授予对象级权限(如 cos:GetObject)会导致 ls 返回 403。
插件参数
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
role_arn | 是 | — | 要扮演的 CAM 角色 ARN,须为 qcs::cam::uin/<主账号ID>:roleName/<角色名> 格式,格式不合法插件启动即报错 |
provider_id | 是 | — | 腾讯云侧 OIDC 提供商的标识,即控制台「身份提供商名称」,须与信任策略 oidc-provider/<名称> 一致(本文示例为 cnb.example.com) |
region | 是 | — | 腾讯云 API 3.0 必填公共参数(STS 请求地域),同时写入 TENCENTCLOUD_REGION |
role_session_name | 否 | cnb-${CNB_BUILD_USER} | STS 会话名,用于云审计;无触发者时为 cnb,超 64 字符自动截断 |
duration_seconds | 否 | 3600 | 临时凭证有效期(秒),超出区间自动收紧到 [900, 7200] |
audience | 否 | sts.tencentcloudapi.com | id_token 受众,须与腾讯云提供商的 Client ID 一致 |
cnb_api | 否 | $CNB_API_ENDPOINT | CNB API 地址(覆盖用),必须为 https:// 开头,插件拒绝 http:// 明文传输 |
凭证文件
插件把临时凭证写入工作空间的 .tencentcloud-credentials(权限 0600), 文件内容为 shell-safe 的 export 语句(值用单引号包裹),后续任务通过 source 载入:
| 变量名 | 说明 |
|---|---|
TENCENTCLOUD_SECRET_ID | 临时 SecretId |
TENCENTCLOUD_SECRET_KEY | 临时 SecretKey |
TENCENTCLOUD_TOKEN | 临时 Token |
TENCENTCLOUD_SECURITY_TOKEN | 同 Token,供 Pulumi 等工具读取 |
TENCENTCLOUD_REGION | 地域 |
文件内容示例:
export TENCENTCLOUD_SECRET_ID='AKID...'
export TENCENTCLOUD_SECRET_KEY='...'
export TENCENTCLOUD_TOKEN='...'
export TENCENTCLOUD_SECURITY_TOKEN='...'
export TENCENTCLOUD_REGION='ap-guangzhou'每个插件任务只换取一组凭证,多账号场景可配置多个插件任务(使用不同的 role_arn)。
错误排查
| 错误提示 | 原因与处理 |
|---|---|
CNB_TOKEN is empty | 未在流水线环境中运行(本地调试无 token) |
403: event not allowed for oidc id_token | 当前事件不在白名单,改用 push / tag 等事件触发 |
403: pipeline token missing ref context | 流水线 token 缺少 ref 元数据,检查构建服务版本 |
403: audience not allowed | aud 未在该实例注册,联系管理员在管理端创建该 Audience |
STS UnauthorizedOperation | 角色信任策略的 oidc:sub 与当前流水线不匹配,对照插件日志中的实际 sub 修改 |
STS InvalidParameter.WebIdentityTokenError | 腾讯云提供商的 Provider URL / Client ID 与 CNB 的 issuer / audience 不一致 |
STS InvalidParameter.ValueTooLarge | duration_seconds 超过 7200,插件已自动收紧到 7200,检查是否有其他调用方 |
PLUGIN_ROLE_ARN 格式错误 | role_arn 不符合 qcs::cam::uin/<主账号ID>:roleName/<角色名> 格式,插件启动即拒绝 |
CNB API 地址必须使用 https:// | cnb_api 使用了 http://,插件拒绝明文传输 CNB_TOKEN |
与自有服务集成
如果目标是自建的发布系统、配置中心或内部 API,可以让流水线把 id_token 作为 Bearer 令牌传入, 服务端按标准 OIDC 流程校验后即可信任本次构建的身份,无需为流水线单独发放长期 API Key。
步骤一:注册 audience
由管理员在管理端「OIDC 配置 - 授权目标(Audience)」中创建 audience, 取值须为域名格式(如 deploy.example.com),长度不超过 128 字符。 该值将作为 id_token 的 aud 声明,也是服务端校验的依据。
步骤二:流水线换取并传递 id_token
main:
push:
- stages:
- name: 调用自有发布服务
image: curlimages/curl
script: |
ID_TOKEN=$(curl -sS -X POST "$CNB_API_ENDPOINT/$CNB_REPO_SLUG/-/id_token" \
-H "Authorization: Bearer $CNB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"aud":"deploy.example.com"}' | jq -r '.id_token')
curl -sS -X POST https://deploy.example.com/api/deploy \
-H "Authorization: Bearer $ID_TOKEN" \
-d "version=$CNB_BRANCH"步骤三:服务端校验 id_token
服务端需完成以下校验,缺一不可:
- 获取公钥:从
{iss}/.well-known/openid-configuration读取jwks_uri, 再拉取 JWKS 公钥集合。公钥会轮换,建议按kid匹配并缓存,定期刷新。 - 验签:用
kid对应的公钥校验 RS256 签名,算法白名单只允许RS256。 - 校验标准声明:
iss等于预期的 CNB 实例地址aud等于本服务注册的 audienceexp未过期,必要时校验nbf
- 校验
sub:与允许调用的仓库 / 分支 / 事件白名单比对,至少精确到仓库级别。 - 防重放(可选):记录
jti,拒绝重复使用。
注意
不要跳过 aud 与 sub 校验,也不要在校验通过前根据 repository、ref 等声明做授权决策—— 这些声明只有在验签通过后才是可信的。
示例:Node.js(使用 jose)
import express from 'express'
import { createRemoteJWKSet, jwtVerify } from 'jose'
const ISSUER = 'https://cnb.cool'
const AUDIENCE = 'deploy.example.com'
// 仅允许 my-group/my-repo 的 main 分支 push 事件调用
const ALLOWED_SUBS = [/^my-group\/my-repo:main:push$/]
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`))
async function verifyRequest(req) {
const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, '')
if (!token) throw new Error('missing bearer token')
// 校验签名 + iss + aud + exp,算法限定 RS256
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256'],
})
// 校验 sub,至少精确到仓库级别
if (!ALLOWED_SUBS.some((re) => re.test(payload.sub))) {
throw new Error(`sub not allowed: ${payload.sub}`)
}
return payload
}
const app = express()
app.post('/api/deploy', async (req, res) => {
try {
const claims = await verifyRequest(req)
res.json({ ok: true, repository: claims.repository, ref: claims.ref })
} catch (err) {
res.status(401).json({ ok: false, message: err.message })
}
})
app.listen(3000)其他语言可使用对应的 OIDC 库,例如 Go 的 github.com/coreos/go-oidc、 Python 的 pyjwt + jwcrypto、Java 的 spring-security-oauth2-jose。
安全建议
- 必须校验
sub:目标服务的信任策略至少要限定到仓库级别,建议进一步精确到分支与事件。 - 最小权限:为每个环境(dev / staging / prod)创建独立角色,按需绑定权限策略。
- 避免凭证泄露:不要把
id_token或临时凭证打印到构建日志, 也不要通过set-output传递;需要跨任务传递时使用带脱敏的set-secret或文件。 - 优先使用专用插件:云厂商场景尽量使用官方/社区插件,避免自行处理凭证。
- 插件凭据链路不落日志:
tencentcloud-oidc-auth的 stdout 只输出凭证 sha256 前 8 位与过期时间, 凭证经工作空间文件(0600)传递,不经过set-output。 - CNB API 强制 HTTPS:插件参数
cnb_api必须为https://,插件会拒绝http://, 避免CNB_TOKEN与id_token明文传输;也不要把cnb_api指向不可信地址。 - 换证后前置校验:插件拿到
id_token后会立即校验aud与期望一致、exp未过期, 把错误 token 挡在 STS 之前;自行实现换证逻辑时同样要做这两项校验。 - 关注事件白名单:仅 push、tag_push、tag_deploy、api_trigger、web_trigger 事件可签发
id_token, 自定义事件名中不要包含:,否则会被拒绝签发。
限制
- 仅支持已启用 OIDC 能力的实例。
id_token有效期固定为 5 分钟,需在有效期内完成换证。- 单条流水线 1 分钟内最多签发 10 次
id_token。 aud需由平台管理员预先注册,取值须为域名格式。- PR、Issue、定时任务等事件触发的流水线无法换取
id_token。