OpenID Connect
Dynamic OIDC mapping
Early Access
Dynamic OIDC mapping is in Early Access. Breaking changes are possible as we receive feedback. To use this feature, contact us.
Cloudsmith's standard OIDC configuration creates a one-to-one trust relationship between a single provider configuration and a fixed set of service accounts. Authentication succeeds for any token that matches the required claims, and every authenticated request is mapped to the same service accounts.
This works well at small scale, but doesn't scale to scenarios where each pipeline, repository, or project needs its own service account. A separate provider configuration per service account quickly produces hundreds (or thousands) of near-identical entries, every one of which differs only by a single claim value and the service account it points to.
Dynamic OIDC mapping condenses these into a single provider configuration. You nominate one claim from the incoming JSON Web Token (JWT) as the mapping claim, and Cloudsmith uses its value to decide which service account a token can authenticate as. The CI job names the service account it wants, and the mapping claim decides whether that request is allowed.
When to use it
Use dynamic mapping when all of the following are true:
- You want a dedicated Cloudsmith service account per pipeline, repository, project, or other CI-side scope.
- All of those service accounts authenticate via the same OIDC provider (for example, all GitHub Actions, all GitLab CI, or all Buildkite).
- The CI provider's JWT contains a claim whose value uniquely identifies each scope (for example,
repository,project_path, orpipeline_slug).
Use the standard (static) configuration when you only need a small number of providers, or when a single provider should always authenticate as the same service account.
To find the claims that your CI provider includes in its JWT, see the provider's token reference:
How it works
A dynamic configuration adds two fields on top of the standard provider configuration:
| Field | Description |
|---|---|
mapping_claim | The name of a claim in the incoming JWT, such as repository. Cloudsmith reads the value of this claim from each token. |
dynamic_mappings | A list of claim_value → service_account pairs. |
Both fields are part of the provider configuration in Cloudsmith. They are not sent in the JWT.
claims gates which tokens are eligible to authenticate at all. mapping_claim and dynamic_mappings decide which service account a token can assume from within that boundary.
The request names the service account it wants to authenticate as, the same as a static configuration does. Dynamic mapping decides whether the token is allowed to act as that account.
On every authentication request, Cloudsmith:
- Verifies the JWT against the provider URL, exactly as it does for a static configuration.
- Verifies that the JWT contains every claim listed in
claims, with matching values. - Reads the value of the JWT claim that
mapping_claimnames, and checks thatdynamic_mappingsmaps that value to the requested service account. If it does, Cloudsmith issues a Cloudsmith token for that service account.
The request is rejected if the claim value does not match any entry in dynamic_mappings, or if it maps to a different service account than the one requested. As with all OIDC token exchange failures, the error returned is deliberately generic and does not indicate which check failed.
Examples
GitHub Actions
A token issued by GitHub Actions includes a repository claim of the form owner/repo. A configuration that gives each repository its own service account looks like this:
{
"name": "GitHub Actions",
"provider_url": "https://token.actions.githubusercontent.com",
"enabled": true,
"claims": {
"repository_owner": "my-org"
},
"mapping_claim": "repository",
"dynamic_mappings": [
{ "claim_value": "my-org/service-a", "service_account": "service-a-ci" },
{ "claim_value": "my-org/service-b", "service_account": "service-b-ci" },
{ "claim_value": "my-org/service-c", "service_account": "service-c-ci" }
],
"service_accounts": []
}claims still scopes the configuration — here, only tokens whose repository_owner is my-org are eligible at all. dynamic_mappings then decides which service account each repository can authenticate as. service_accounts is left empty, because a dynamic configuration authorizes service accounts through dynamic_mappings instead.
A workflow in the my-org/service-a repository must request service-a-ci when it exchanges its token. See Exchange the OIDC token.
GitHub Actions (per workflow)
A GitHub Actions JWT also includes a workflow claim, which lets you scope authentication more tightly than per repository. This is useful when a single repository contains workflows that need different levels of access, for example, a release workflow that needs to publish, and a CI workflow that only needs to pull:
{
"name": "GitHub Actions (per workflow)",
"provider_url": "https://token.actions.githubusercontent.com",
"enabled": true,
"claims": {
"repository": "my-org/my-repo"
},
"mapping_claim": "workflow",
"dynamic_mappings": [
{ "claim_value": "release", "service_account": "my-repo-release" },
{ "claim_value": "ci", "service_account": "my-repo-ci" }
],
"service_accounts": []
}GitLab CI
GitLab tokens contain a sub claim that combines the project path with the branch reference (project_path:my-group/my-project:ref_type:branch:ref:main). For per-project mapping, use the project_path claim instead:
{
"name": "GitLab CI",
"provider_url": "https://gitlab.com",
"enabled": true,
"claims": {
"namespace_path": "my-group"
},
"mapping_claim": "project_path",
"dynamic_mappings": [
{ "claim_value": "my-group/service-a", "service_account": "service-a-ci" },
{ "claim_value": "my-group/service-b", "service_account": "service-b-ci" }
],
"service_accounts": []
}Buildkite
Buildkite's JWT includes a pipeline_slug claim, which is the most common mapping target:
{
"name": "Buildkite",
"provider_url": "https://agent.buildkite.com",
"enabled": true,
"claims": {
"organization_slug": "my-org"
},
"mapping_claim": "pipeline_slug",
"dynamic_mappings": [
{ "claim_value": "deploy-prod", "service_account": "deploy-prod-ci" },
{ "claim_value": "deploy-staging", "service_account": "deploy-staging-ci" }
],
"service_accounts": []
}Exchange the OIDC token
The token exchange request is the same as for a static configuration. The request must include service_slug. Cloudsmith does not select the service account from the JWT claims.
The service_slug must be the service account that dynamic_mappings maps to the token's mapping claim value. For the GitHub Actions example, a workflow in the my-org/service-a repository sends this request:
curl --request POST \
--url https://api.cloudsmith.io/openid/<WORKSPACE>/ \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{
"service_slug": "service-a-ci",
"oidc_token": "<JWT_FROM_CI_PROVIDER>"
}'If the same workflow requests service-b-ci, Cloudsmith rejects the request. The token's repository claim value is my-org/service-a, which maps only to service-a-ci.
For provider-specific request examples, see Token exchange. If you use the Cloudsmith CLI, set CLOUDSMITH_SERVICE_SLUG to the mapped service account.
Configuring via API
Note
Dynamic OIDC mapping is configurable via the API and the Cloudsmith Terraform provider only.
Create a dynamic provider configuration with a POST to the OIDC Create endpoint:
curl --request POST \
--url https://api.cloudsmith.io/orgs/<WORKSPACE>/openid-connect/ \
--header 'X-Api-Key: <API_KEY>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{
"name": "GitHub Actions",
"provider_url": "https://token.actions.githubusercontent.com",
"enabled": true,
"claims": { "repository_owner": "my-org" },
"mapping_claim": "repository",
"dynamic_mappings": [
{ "claim_value": "my-org/service-a", "service_account": "service-a-ci" },
{ "claim_value": "my-org/service-b", "service_account": "service-b-ci" }
]
}'You can list the dynamic mappings on an existing provider with the list dynamic mappings endpoint.
Configuring via Terraform
The cloudsmith_oidc resource accepts mapping_claim and dynamic_mappings from provider version v0.0.63 onwards. A typical pattern is to drive dynamic_mappings from a for_each over the repositories or pipelines you manage elsewhere in your Terraform configuration:
resource "cloudsmith_oidc" "github_actions" {
namespace = data.cloudsmith_organization.my_org.slug
name = "GitHub Actions"
provider_url = "https://token.actions.githubusercontent.com"
enabled = true
claims = {
repository_owner = "my-org"
}
mapping_claim = "repository"
dynamic "dynamic_mappings" {
for_each = var.repositories
content {
claim_value = "my-org/${dynamic_mappings.value.name}"
service_account = cloudsmith_service.this[dynamic_mappings.value.name].slug
}
}
}This collapses what would otherwise be one cloudsmith_oidc resource per repository into a single resource whose mappings are generated from the same data structure that provisions the service accounts.
Notes and limitations
- One mapping claim per provider. Each provider configuration uses exactly one
mapping_claim. If you need to route on different claims for different sets of pipelines, create separate provider configurations. - Static and dynamic are mutually exclusive on a single configuration. A provider is either static (uses
service_accounts) or dynamic (usesmapping_claim+dynamic_mappings). Omitservice_accountsor set it to an empty array when using dynamic mapping. - Claim values match exactly. Each
claim_valuemust exactly match the value of the mapping claim in the JWT. Wildcards are not supported inclaim_value, unlike inclaims. - One service account per claim value. Each
claim_valuecan appear only once in a provider configuration. You can map more than one claim value to the same service account. - Client log enrichment. Client logs currently record only that a service account was used; they do not record the OIDC claim values that were exchanged for that token.