From 2774a633c8255860dda3e52d95d3b19b39469e9e Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 17:45:10 +0100 Subject: [PATCH 1/7] New page and links --- .../authentication/dynamic-oidc-mapping.mdx | 196 ++++++++++++++++++ src/content/authentication/openid-connect.mdx | 4 + src/content/menu.json | 4 + 3 files changed, 204 insertions(+) create mode 100644 src/content/authentication/dynamic-oidc-mapping.mdx diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx new file mode 100644 index 00000000..f53c652d --- /dev/null +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -0,0 +1,196 @@ +--- +title: Dynamic OIDC Mapping +description: Map a single OIDC provider configuration to many Cloudsmith service accounts based on a claim value from the incoming JWT. +--- + +# Dynamic OIDC Mapping [#dynamic-oidc-mapping] + +Cloudsmith's [standard OIDC configuration](/authentication/openid-connect) 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 account(s). + +This works well at small scale. It does not 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 JWT as the _mapping claim_, and Cloudsmith routes each request to a service account based on that claim's value. + +## When to use it [#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 (e.g. all GitHub Actions, all GitLab CI, all Buildkite). +- The CI provider's JWT contains a claim whose value uniquely identifies each scope (e.g. `repository`, `project_path`, `pipeline_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(s). + +## How it works [#how-it-works] + +A dynamic configuration adds two fields on top of the standard provider configuration: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------- | +| `mapping_claim` | The claim in the incoming JWT whose value Cloudsmith will use to choose a service account. | +| `dynamic_mappings` | A list of `claim_value` → `service_account` pairs. | + +`claims` remains the security boundary that gates which tokens are eligible to authenticate at all. `mapping_claim` and `dynamic_mappings` are the routing logic that selects a service account from within that boundary. + +On every authentication request, Cloudsmith: + +1. Verifies the JWT against the provider URL, exactly as it does for a static configuration. +2. Verifies that the JWT contains every claim listed in `claims` (these still act as a gate — typically `iss` and any organisation-level scoping). +3. Reads the value of `mapping_claim` from the JWT. +4. Looks that value up in `dynamic_mappings` and, if a match is found, issues a Cloudsmith token for the mapped service account. + +If the value of `mapping_claim` does not match any entry in `dynamic_mappings`, the request is rejected. As with all OIDC token exchange failures, the error returned is deliberately generic and does not indicate which check failed. + +## Examples [#examples] + +### GitHub Actions [#example-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: + +```json +{ + "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. `mapping_claim` and `dynamic_mappings` then select the service account from the candidates inside that scope. `service_accounts` is left empty, because the service account is now chosen per request. + +### GitHub Actions (per workflow) [#example-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: + +```json +{ + "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 [#example-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: + +```json +{ + "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 [#example-buildkite] + +Buildkite's JWT includes a `pipeline_slug` claim, which is the most common mapping target: + +```json +{ + "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": [] +} +``` + +## Configuring via API [#configuring-via-api] + +> **Note** +> +> Dynamic OIDC mapping is currently configurable via the API and the [Cloudsmith Terraform provider](https://registry.terraform.io/providers/cloudsmith-io/cloudsmith/latest/docs/resources/oidc) only. A UI for managing dynamic mappings is on the roadmap. + +Create a dynamic provider configuration with a `POST` to the OIDC endpoint: + +```bash +curl --request POST \ + --url https://api.cloudsmith.io/orgs//openid-connect/ \ + --header 'X-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](https://docs.cloudsmith.com/api/openid-connect/dynamic-mappings/list). + +## Configuring via Terraform [#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: + +```hcl +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 [#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 (uses `mapping_claim` + `dynamic_mappings`). Set `service_accounts` to an empty array when using dynamic mapping. +- **Claim values must match exactly.** Trailing-wildcard syntax (`prefix.*`) is supported in the same way as it is for `claims` values. +- **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. Enriched logging for OIDC token exchanges is on the roadmap. \ No newline at end of file diff --git a/src/content/authentication/openid-connect.mdx b/src/content/authentication/openid-connect.mdx index d9d640a2..b290a22d 100644 --- a/src/content/authentication/openid-connect.mdx +++ b/src/content/authentication/openid-connect.mdx @@ -63,6 +63,10 @@ To configure a provider, you must provide: Changes will be applied immediately. + + If you need a single provider configuration to authenticate as many different service accounts based on a claim value, see [Dynamic OIDC Mapping](/authentication/dynamic-oidc-mapping). + + ## Provider Documentation ### Bitbucket Pipelines diff --git a/src/content/menu.json b/src/content/menu.json index 4ee32340..ab7e98d2 100644 --- a/src/content/menu.json +++ b/src/content/menu.json @@ -281,6 +281,10 @@ { "title": "Jenkins", "path": "/authentication/setup-jenkins-to-authenticate-to-cloudsmith-using-oidc" + }, + { + "title": "Dynamic OIDC Mapping", + "path": "/authentication/dynamic-oidc-mapping" } ] } From c0ccd435f6438086965868433796fefb7a28a50f Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 17:51:28 +0100 Subject: [PATCH 2/7] Adding hcl as a language --- src/lib/highlight/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/highlight/server.ts b/src/lib/highlight/server.ts index caa5e23a..a7979ecf 100644 --- a/src/lib/highlight/server.ts +++ b/src/lib/highlight/server.ts @@ -33,6 +33,7 @@ export async function getHighlighter() { 'scss', 'ruby', 'csv', + 'hcl', ], }); } From 51034ff35db758bf987c34cfa7457d46ba5842c6 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 18:09:55 +0100 Subject: [PATCH 3/7] fixed headings --- .../authentication/dynamic-oidc-mapping.mdx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx index f53c652d..e0fcf0c8 100644 --- a/src/content/authentication/dynamic-oidc-mapping.mdx +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -3,7 +3,7 @@ title: Dynamic OIDC Mapping description: Map a single OIDC provider configuration to many Cloudsmith service accounts based on a claim value from the incoming JWT. --- -# Dynamic OIDC Mapping [#dynamic-oidc-mapping] +# Dynamic OIDC Mapping Cloudsmith's [standard OIDC configuration](/authentication/openid-connect) 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 account(s). @@ -11,7 +11,7 @@ This works well at small scale. It does not scale to scenarios where each pipeli **Dynamic OIDC mapping** condenses these into a single provider configuration. You nominate one claim from the incoming JWT as the _mapping claim_, and Cloudsmith routes each request to a service account based on that claim's value. -## When to use it [#when-to-use-it] +## When to use it Use dynamic mapping when **all** of the following are true: @@ -21,7 +21,7 @@ Use dynamic mapping when **all** of the following are true: 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(s). -## How it works [#how-it-works] +## How it works A dynamic configuration adds two fields on top of the standard provider configuration: @@ -41,9 +41,9 @@ On every authentication request, Cloudsmith: If the value of `mapping_claim` does not match any entry in `dynamic_mappings`, the request is rejected. As with all OIDC token exchange failures, the error returned is deliberately generic and does not indicate which check failed. -## Examples [#examples] +## Examples -### GitHub Actions [#example-github-actions] +### 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: @@ -67,7 +67,7 @@ A token issued by GitHub Actions includes a `repository` claim of the form `owne `claims` still scopes the configuration — here, only tokens whose `repository_owner` is `my-org` are eligible at all. `mapping_claim` and `dynamic_mappings` then select the service account from the candidates inside that scope. `service_accounts` is left empty, because the service account is now chosen per request. -### GitHub Actions (per workflow) [#example-github-actions-per-workflow] +### 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: @@ -88,7 +88,7 @@ A GitHub Actions JWT also includes a `workflow` claim, which lets you scope auth } ``` -### GitLab CI [#example-gitlab-ci] +### 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: @@ -109,7 +109,7 @@ GitLab tokens contain a `sub` claim that combines the project path with the bran } ``` -### Buildkite [#example-buildkite] +### Buildkite Buildkite's JWT includes a `pipeline_slug` claim, which is the most common mapping target: @@ -130,7 +130,7 @@ Buildkite's JWT includes a `pipeline_slug` claim, which is the most common mappi } ``` -## Configuring via API [#configuring-via-api] +## Configuring via API > **Note** > @@ -159,7 +159,7 @@ curl --request POST \ You can list the dynamic mappings on an existing provider with the [list dynamic mappings endpoint](https://docs.cloudsmith.com/api/openid-connect/dynamic-mappings/list). -## Configuring via Terraform [#configuring-via-terraform] +## 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: @@ -188,7 +188,7 @@ resource "cloudsmith_oidc" "github_actions" { 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 [#notes-and-limitations] +## 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 (uses `mapping_claim` + `dynamic_mappings`). Set `service_accounts` to an empty array when using dynamic mapping. From 20336965a81ea9e6f63d5a697efeee6789f32ac6 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 18:11:24 +0100 Subject: [PATCH 4/7] remove roadmap comment --- src/content/authentication/dynamic-oidc-mapping.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx index e0fcf0c8..f6788c3e 100644 --- a/src/content/authentication/dynamic-oidc-mapping.mdx +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -134,7 +134,7 @@ Buildkite's JWT includes a `pipeline_slug` claim, which is the most common mappi > **Note** > -> Dynamic OIDC mapping is currently configurable via the API and the [Cloudsmith Terraform provider](https://registry.terraform.io/providers/cloudsmith-io/cloudsmith/latest/docs/resources/oidc) only. A UI for managing dynamic mappings is on the roadmap. +> Dynamic OIDC mapping is currently configurable via the API and the [Cloudsmith Terraform provider](https://registry.terraform.io/providers/cloudsmith-io/cloudsmith/latest/docs/resources/oidc) only. Create a dynamic provider configuration with a `POST` to the OIDC endpoint: From d84ab617b5e94207e36101f01e634abed430f161 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 20:16:55 +0100 Subject: [PATCH 5/7] Note fix --- src/content/authentication/dynamic-oidc-mapping.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx index f6788c3e..dc1e3507 100644 --- a/src/content/authentication/dynamic-oidc-mapping.mdx +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -3,6 +3,8 @@ title: Dynamic OIDC Mapping description: Map a single OIDC provider configuration to many Cloudsmith service accounts based on a claim value from the incoming JWT. --- +import { Note } from '@/components' + # Dynamic OIDC Mapping Cloudsmith's [standard OIDC configuration](/authentication/openid-connect) 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 account(s). @@ -132,9 +134,9 @@ Buildkite's JWT includes a `pipeline_slug` claim, which is the most common mappi ## Configuring via API -> **Note** -> -> Dynamic OIDC mapping is currently configurable via the API and the [Cloudsmith Terraform provider](https://registry.terraform.io/providers/cloudsmith-io/cloudsmith/latest/docs/resources/oidc) only. + + Dynamic OIDC mapping is currently configurable via the API and the [Cloudsmith Terraform provider](https://registry.terraform.io/providers/cloudsmith-io/cloudsmith/latest/docs/resources/oidc) only. + Create a dynamic provider configuration with a `POST` to the OIDC endpoint: @@ -193,4 +195,4 @@ This collapses what would otherwise be one `cloudsmith_oidc` resource per reposi - **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 (uses `mapping_claim` + `dynamic_mappings`). Set `service_accounts` to an empty array when using dynamic mapping. - **Claim values must match exactly.** Trailing-wildcard syntax (`prefix.*`) is supported in the same way as it is for `claims` values. -- **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. Enriched logging for OIDC token exchanges is on the roadmap. \ No newline at end of file +- **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. \ No newline at end of file From 2f185e3e232562217ebdd14c316f994bdd212cfd Mon Sep 17 00:00:00 2001 From: Ollie Strachan Date: Tue, 19 May 2026 20:37:19 +0100 Subject: [PATCH 6/7] US spelling Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/content/authentication/dynamic-oidc-mapping.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx index dc1e3507..b54080a0 100644 --- a/src/content/authentication/dynamic-oidc-mapping.mdx +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -37,7 +37,7 @@ A dynamic configuration adds two fields on top of the standard provider configur On every authentication request, Cloudsmith: 1. Verifies the JWT against the provider URL, exactly as it does for a static configuration. -2. Verifies that the JWT contains every claim listed in `claims` (these still act as a gate — typically `iss` and any organisation-level scoping). +2. Verifies that the JWT contains every claim listed in `claims` (these still act as a gate — typically `iss` and any organization-level scoping). 3. Reads the value of `mapping_claim` from the JWT. 4. Looks that value up in `dynamic_mappings` and, if a match is found, issues a Cloudsmith token for the mapped service account. From efef7a2b30c17b88c71d7c2d47a532aa82e6bc36 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 19 May 2026 20:39:03 +0100 Subject: [PATCH 7/7] Copilot fix --- src/content/authentication/dynamic-oidc-mapping.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/authentication/dynamic-oidc-mapping.mdx b/src/content/authentication/dynamic-oidc-mapping.mdx index dc1e3507..6bbe63cf 100644 --- a/src/content/authentication/dynamic-oidc-mapping.mdx +++ b/src/content/authentication/dynamic-oidc-mapping.mdx @@ -194,5 +194,5 @@ This collapses what would otherwise be one `cloudsmith_oidc` resource per reposi - **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 (uses `mapping_claim` + `dynamic_mappings`). Set `service_accounts` to an empty array when using dynamic mapping. -- **Claim values must match exactly.** Trailing-wildcard syntax (`prefix.*`) is supported in the same way as it is for `claims` values. +- **Claim values match literally.** Each `claim_value` is compared as a literal string against the incoming JWT, except for the trailing-wildcard syntax (`prefix.*`), which is supported in the same way as it is for `claims` values. - **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. \ No newline at end of file