OIDC Integration
About 2773 wordsAbout 9 min
Cloud Native Build can issue an OIDC id_token in a pipeline to prove "the identity of this build" to services that support OIDC federation (cloud providers or self-hosted services), and exchange it for short-lived credentials so that no long-lived secrets need to be stored in the repository.
Why Use OIDC
Traditionally, you store a cloud provider's SecretId / SecretKey as a repository secret, which means secrets are centralized, widely exposed, and hard to rotate. With OIDC:
- No long-lived secrets: no cloud credentials stored in the repository; the pipeline requests short-lived credentials on demand.
- Authentication separated from authorization: who can obtain which permissions is controlled by the trust policy and permission policy on the target service side, down to repository, branch, and event granularity.
- Automatic credential rotation: the
id_tokenis valid for 5 minutes and the exchanged credentials expire automatically, so no manual rotation is needed.
How It Works
1. One-time setup: register CNB as a trusted OIDC identity provider on the target service side
and configure its trust policy.
2. At pipeline runtime: call the CNB OpenAPI /{repo}/-/id_token with $CNB_TOKEN to get an id_token (JWT).
3. A plugin or script passes the id_token to the cloud provider
(for example, Tencent Cloud STS AssumeRoleWithWebIdentity) or to your own service.
4. The target service verifies the signature using the CNB JWKS public keys
and validates the aud / sub / exp claims.
5. Once verified, short-lived credentials are issued, or business data is returned.Standard endpoints provided by CNB:
| Endpoint | Description |
|---|---|
/.well-known/openid-configuration | OIDC discovery document, including issuer and jwks_uri |
/.well-known/jwks.json | Signing public key set (RS256) used by the target service to verify signatures |
The issuer of the site is the instance address https://cnb.cool, and the endpoints are:
- Discovery document:
https://cnb.cool/.well-known/openid-configuration - Signing public key set (JWKS):
https://cnb.cool/.well-known/jwks.json
Prerequisites
- OIDC is enabled on the instance.
- The pipeline is triggered by an allowed event (see the table below). For events such as pull requests, issues, and scheduled tasks, the ref is not trustworthy or there is no token-exchange scenario, so the platform rejects the request.
- The user who triggers the pipeline has developer permission or above.
- The
audin the request is registered on the instance. Audiences are created by an administrator under OIDC Settings - Audience in the admin console; usests.tencentcloudapi.comfor Tencent Cloud.
Events Allowed to Issue Tokens
| Event | Trigger Source | Permission Requirement | sub Example |
|---|---|---|---|
push | Git push (issued by the platform) | — | my-group/my-repo:main:push |
tag_push | Tag push (issued by the platform) | — | my-group/my-repo:v1.0:tag_push |
tag_deploy.{env} | Deployment from the UI / API | Push code + push tag permission | my-group/my-repo:v1.0:tag_deploy.prod |
api_trigger[_{name}] | OpenAPI | Repository write permission | my-group/my-repo:main:api_trigger |
web_trigger[_{name}] | UI button | Repository write permission | my-group/my-repo:main:web_trigger |
id_token Claims
The id_token is an RS256-signed JWT, for example:
{
"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..."
}| Claim | Description |
|---|---|
iss | Issuer, the public HTTPS address of the instance |
aud | Audience, the aud passed in when requesting the id_token |
sub | Subject, in the three-segment form {slug}:{ref}:{event}, generated by the server |
iat / exp | Issued-at / expiration time. The id_token is valid for 5 minutes |
jti | Unique token ID, which can be used for replay protection |
repository | Full repository path, same as CNB_REPO_SLUG |
ref | Branch or tag name (short name, without the refs/ prefix) |
event_name | Triggering event name |
actor | Username of the user who triggered the pipeline |
run_id | Pipeline ID |
sha | SHA of the current commit |
Three-Segment sub
| Segment | Meaning | Example |
|---|---|---|
{slug} | Full repository path, same as CNB_REPO_SLUG | my-group/my-repo |
{ref} | Branch or tag name (short name, without the refs/ prefix) | main / v1.0 |
{event} | Triggering event name | push / tag_push / tag_deploy.prod |
Because sub already contains the repository, ref, and event dimensions, the target service only needs to match sub to enforce branch-level and event-level authorization. All segments are generated by the server based on the pipeline context and cannot be overridden by the caller.
Warning
aud only identifies "who the credential is for". All id_tokens issued under the same audience have an identical aud, which cannot distinguish which repository or branch initiated the request. The trust policy of the target service must also validate sub; otherwise you are granting permissions to any repository on the instance.
Requesting the id_token in a Pipeline
Endpoint
POST {CNB_API_ENDPOINT}/{repo}/-/id_token
Authorization: Bearer $CNB_TOKEN
Content-Type: application/json
{"aud":"sts.tencentcloudapi.com"}Only aud may be passed in the request body. The sub claim and all other claims are generated by the server from the pipeline context and cannot be overridden by the caller.
Response:
{
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}Tips
The response does not return expires_in. The actual validity period of the id_token is determined by the exp claim inside the JWT, so parse exp to determine the remaining lifetime.
Example
main:
push:
- stages:
- name: Request 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')
# Never print ID_TOKEN to the build logA single pipeline can request at most 10 id_tokens per minute. Exceeding the limit returns 429.
Integrating with Tencent Cloud
Use the tencentcloud-oidc-auth plugin to obtain Tencent Cloud CAM temporary credentials in a pipeline without configuring SecretId / SecretKey in the repository.
Step 1: Configure Tencent Cloud (one-time)
1. Create an OIDC identity provider
Open Create Identity Provider (CAM Console → Identity Provider → Role SSO → Create Identity Provider) and fill in the form as follows:
| Field | Value |
|---|---|
| Provider type | Select OIDC |
| Identity provider name | A domain name (for example cnb.example.com) is recommended so that multiple CNB instances under the same CAM account can be told apart. This value is the provider identifier and must be consistent with oidc-provider/<name> in the trust policy below and with the plugin's provider_id parameter |
| Identity provider URL | The CNB issuer, which must match the iss of the id_token exactly — https://cnb.cool |
| Client ID | sts.tencentcloudapi.com |
| Identity provider public key | Open this site's JWKS endpoint https://cnb.cool/.well-known/jwks.json and paste the public key content here |
2. Create a CAM role and configure its trust policy
Create an identity-provider role, select the provider created above, and use the following trust policy:
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": ["sts:AssumeRoleWithWebIdentity"],
"principal": {
"federated": [
"qcs::cam::uin/<ACCOUNT_ID>:oidc-provider/cnb.example.com"
]
},
"condition": {
"string_equal": {
"oidc:aud": "sts.tencentcloudapi.com",
"oidc:sub": "my-group/my-repo:main:push"
}
}
}
]
}From coarse to fine granularity:
- Repository level (minimum requirement):
"oidc:sub": "my-group/my-repo:*"withstring_like - Branch level:
"oidc:sub": "my-group/my-repo:main:*"withstring_like - Exact match:
"oidc:sub": "my-group/my-repo:main:push"withstring_equal
To authorize multiple branches or events, add multiple entries to the statement array, or use string_like wildcards on the branch segment (for example my-group/my-repo:release-*:push).
Caution
The trust policy must include an oidc:sub condition, and it must be at least precise to the repository level ({slug}).
oidc:aud only verifies that the audience is sts.tencentcloudapi.com, which is identical for every CNB pipeline and cannot identify the issuing subject. Without the oidc:sub restriction, any CNB user can use an id_token issued by any of their own repositories to assume your role and obtain all cloud resource permissions granted to that role. Never use a bare % wildcard.
3. Attach permission policies to the role
Follow the principle of least privilege, for example QcloudCOSDataWriteOnly, QcloudCDNPushOnly, or a custom policy.
Step 2: Configure the pipeline
main:
push:
- stages:
- name: Tencent Cloud OIDC auth
image: tencentcom/tencentcloud-oidc-auth
settings:
role_arn: qcs::cam::uin/123456789:roleName/cnb-deploy
provider_id: cnb.example.com
region: ap-guangzhou
- name: Prepare Python env
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: Verify credentials
image: python:3-slim
script: |
. "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
"$CNB_BUILD_WORKSPACE/.venv/bin/tccli" sts GetCallerIdentity
- name: Deploy
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 (example)
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 10Tips
Unlike tccli / the SDK, coscli does not read the TENCENTCLOUD_* environment variables — pass the credentials explicitly with -i / -k / --token and use --init-skip to skip interactive initialization. The cos:// target must be a full bucket name (including the APPID). ls requires bucket-level permissions (cos:HeadBucket / cos:GetBucket); granting only object-level permissions (such as cos:GetObject) makes ls return 403.
Plugin Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
role_arn | Yes | — | ARN of the CAM role to assume, must match qcs::cam::uin/<ACCOUNT_ID>:roleName/<ROLE_NAME>; a malformed value fails fast at plugin startup |
provider_id | Yes | — | Identifier of the OIDC provider in Tencent Cloud, that is, the "Identity provider name" in the console. It must match oidc-provider/<name> in the trust policy (the example here uses cnb.example.com) |
region | Yes | — | Required Tencent Cloud API 3.0 common parameter (STS request region), also written to TENCENTCLOUD_REGION |
role_session_name | No | cnb-${CNB_BUILD_USER} | STS session name used for cloud audit; falls back to cnb when there is no triggerer, auto-truncated beyond 64 characters |
duration_seconds | No | 3600 | Credential validity in seconds, clamped to [900, 7200] |
audience | No | sts.tencentcloudapi.com | id_token audience, must match the Client ID of the Tencent Cloud provider |
cnb_api | No | $CNB_API_ENDPOINT | CNB API address (override), must start with https://; plaintext http:// is rejected |
Credentials File
The plugin writes temporary credentials to .tencentcloud-credentials in the workspace with permission 0600. The file contains shell-safe export statements (values wrapped in single quotes), and subsequent jobs load them with source:
| Variable | Description |
|---|---|
TENCENTCLOUD_SECRET_ID | Temporary SecretId |
TENCENTCLOUD_SECRET_KEY | Temporary SecretKey |
TENCENTCLOUD_TOKEN | Temporary token |
TENCENTCLOUD_SECURITY_TOKEN | Same as the token, for tools such as Pulumi |
TENCENTCLOUD_REGION | Region |
File content example:
export TENCENTCLOUD_SECRET_ID='AKID...'
export TENCENTCLOUD_SECRET_KEY='...'
export TENCENTCLOUD_TOKEN='...'
export TENCENTCLOUD_SECURITY_TOKEN='...'
export TENCENTCLOUD_REGION='ap-guangzhou'Each plugin job exchanges only one set of credentials. For multi-account scenarios, configure multiple plugin jobs with different role_arn values.
Troubleshooting
| Error | Cause and Fix |
|---|---|
CNB_TOKEN is empty | Not running in a pipeline environment (no token when debugging locally) |
403: event not allowed for oidc id_token | The current event is not allowed; trigger with push / tag events instead |
403: pipeline token missing ref context | The pipeline token lacks ref metadata; check the build service version |
403: audience not allowed | The aud is not registered on the instance; ask an administrator to create it |
STS UnauthorizedOperation | The oidc:sub in the role trust policy does not match the current pipeline; compare it with the actual sub printed in the plugin log |
STS InvalidParameter.WebIdentityTokenError | The Provider URL / Client ID in Tencent Cloud do not match the CNB issuer / audience |
STS InvalidParameter.ValueTooLarge | duration_seconds exceeds 7200; the plugin already clamps it to 7200, check for other callers |
PLUGIN_ROLE_ARN is malformed | role_arn does not match qcs::cam::uin/<ACCOUNT_ID>:roleName/<ROLE_NAME>; the plugin rejects it at startup |
CNB API endpoint must use https:// | cnb_api uses http://; the plugin rejects plaintext transmission of CNB_TOKEN |
Integrating with Your Own Service
If the target is your own release system, configuration center, or internal API, the pipeline can pass the id_token as a Bearer token. After validating it with the standard OIDC flow, the server can trust the identity of this build without issuing a dedicated long-lived API key for the pipeline.
Step 1: Register the audience
An administrator creates the audience under OIDC Settings - Audience in the admin console. The value must be a domain name (for example deploy.example.com) and no longer than 128 characters. This value becomes the aud claim of the id_token and the basis for server-side validation.
Step 2: Request and pass the id_token in the pipeline
main:
push:
- stages:
- name: Call the internal deploy service
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"Step 3: Validate the id_token on the server
The server must complete all of the following checks:
- Fetch public keys: read
jwks_urifrom{iss}/.well-known/openid-configuration, then fetch the JWKS key set. Keys are rotated, so match bykid, cache the keys, and refresh periodically. - Verify the signature: verify the RS256 signature with the public key matching
kid, and allow onlyRS256in the algorithm allowlist. - Validate standard claims:
issequals the expected CNB instance addressaudequals the audience registered for this serviceexphas not passed, and validatenbfwhen necessary
- Validate
sub: compare it against the allowlist of repositories / branches / events, at least precise to the repository level. - Replay protection (optional): record
jtiand reject reuse.
Warning
Do not skip the aud and sub validation, and do not make authorization decisions based on claims such as repository or ref before the signature is verified — those claims are only trustworthy after signature verification succeeds.
Example: Node.js (using jose)
import express from 'express'
import { createRemoteJWKSet, jwtVerify } from 'jose'
const ISSUER = 'https://cnb.cool'
const AUDIENCE = 'deploy.example.com'
// Only allow push events on the main branch of my-group/my-repo
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')
// Verify signature + iss + aud + exp, restricted to RS256
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256'],
})
// Validate sub, at least precise to the repository level
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)Other languages can use their own OIDC libraries, such as github.com/coreos/go-oidc for Go, pyjwt with jwcrypto for Python, and spring-security-oauth2-jose for Java.
Security Recommendations
- Always validate
sub: the trust policy of the target service must at least restrict it to the repository level, and preferably to the branch and event level. - Least privilege: create a separate role for each environment (dev / staging / prod) and attach only the required policies.
- Avoid credential leaks: never print the
id_tokenor temporary credentials to the build log, and never pass them throughset-output. Use maskedset-secretor files for cross-job passing. - Prefer dedicated plugins: for cloud providers, use official or community plugins instead of handling credentials yourself.
- The plugin keeps credentials out of logs:
tencentcloud-oidc-authprints only the first 8 characters of the credential sha256 hash plus the expiration time, and passes credentials through a workspace file (0600) instead ofset-output. - CNB API HTTPS is enforced: the plugin parameter
cnb_apimust usehttps://; the plugin rejectshttp://so thatCNB_TOKENand theid_tokenare never sent in plaintext. Never pointcnb_apiat an untrusted address. - Pre-validate after exchange: right after obtaining an
id_token, the plugin checks thataudmatches the expected value and thatexphas not passed, keeping bad tokens away from STS. Do the same two checks if you implement the exchange yourself. - Mind the event allowlist: only
push,tag_push,tag_deploy,api_trigger, andweb_triggerevents can issue anid_token. Custom event names must not contain:, otherwise issuance is rejected.
Limitations
- Only instances with OIDC enabled are supported.
- The
id_tokenis valid for a fixed 5 minutes; complete the token exchange within that window. - A single pipeline can request at most 10
id_tokens per minute. - The
audmust be registered in advance by a platform administrator and must be a domain name. - Pipelines triggered by pull request, issue, or scheduled events cannot obtain an
id_token.