Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions src/content/authentication/dynamic-oidc-mapping.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
---
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).

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

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

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 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.

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

### 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)

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

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

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

<Note variant="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.
</Note>

Create a dynamic provider configuration with a `POST` to the OIDC endpoint:

```bash
curl --request POST \
--url https://api.cloudsmith.io/orgs/<org-slug>/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](https://docs.cloudsmith.com/api/openid-connect/dynamic-mappings/list).

## 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

- **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 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.
4 changes: 4 additions & 0 deletions src/content/authentication/openid-connect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ To configure a provider, you must provide:

Changes will be applied immediately.

<Note variant="note">
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).
</Note>

## Provider Documentation

### Bitbucket Pipelines
Expand Down
4 changes: 4 additions & 0 deletions src/content/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/highlight/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export async function getHighlighter() {
'scss',
'ruby',
'csv',
'hcl',
],
});
}
Expand Down