For the complete documentation index, see llms.txt.

Grant Chainguard roles from identity provider groups

How to map groups from a custom identity provider to Chainguard roles so access follows group membership.
  11 min read

Chainguard can grant roles based on a user’s groups in your identity provider (IdP). You map an IdP group to a Chainguard role once, and from then on any user who logs in with that group in their token receives the role for that session. Access follows group membership, so you manage who gets what in your IdP instead of assigning roles to each user in Chainguard.

This guide covers Okta and Microsoft Entra ID. The Chainguard-side steps (2 through 4) are the same for both providers; only how you emit group membership in Step 1 differs.

Prerequisites

To complete this guide, you need the following:

The rest of this guide refers to your organization and identity provider by their UIDPs, stored in the ORGANIZATION and IDENTITY_PROVIDER environment variables. Retrieve and set them with the following commands.

Store your organization’s UIDP in the ORGANIZATION variable:

export ORGANIZATION=$(chainctl iam organizations list -o json | jq -r '.items[0].id')

Store your identity provider’s UIDP in the IDENTITY_PROVIDER variable:

export IDENTITY_PROVIDER=$(chainctl iam identity-providers list -o json | jq -r '.items[0].id')

These commands select the first result each list returns. If your account can access more than one organization or identity provider, replace .items[0] with a filter that matches the one you want, or set each variable to the UIDP directly.

How IdP group mappings work

Before you configure any mappings, it helps to understand how group-derived roles behave:

  • Mappings are additive. A mapping grants a role on top of whatever access a user already has. It never removes existing access.

  • Group-derived roles are session-scoped, not standing grants. They apply to the logged-in session and are re-evaluated at each login. They do not appear in chainctl iam role-bindings list or the Console’s role-bindings view. Instead, each login that resolves group-derived roles emits a Chainguard CloudEvent recording the roles granted, along with the identity provider and groups they came from. To observe effective group-derived access, subscribe to Chainguard’s CloudEvents stream.

    As a result, no single command or view answers “who holds a group-derived role right now?” The CloudEvents stream is a log of each login as it happens, not a current-state table. During an access review or an incident, reconstruct effective access by combining that event history with current group membership in your IdP and the mappings from chainctl iam external-group-role-mappings list.

  • Changes take effect within an hour. A session that includes group-derived roles is short-lived by design. Its access token lasts at most one hour, and no refresh token is issued, so when the token expires the user logs in again and that login re-reads their current group membership. As a result:

    • Removing a user from a group takes effect at their next login, within one hour.
    • Deleting or changing a mapping takes effect within one hour, because Chainguard reads mappings fresh on every login.
    • A user whose access is only group-derived re-authenticates at least hourly.

Step 1: Emit group membership from your IdP

Configure your identity provider to include the user’s group memberships in the OIDC token it issues to Chainguard. The exact Console steps differ by provider and change over time, so follow your provider’s own documentation. This guide relies only on the result:

  • The token carries a claim listing the user’s groups, named whatever you set in Step 2. That value format is what you supply as --external-group-id in Step 3. This guide uses groups.
  • The token carries only the groups you map, not every group a user belongs to. The IdP drops oversized group sets from the token, as described in Limits.
ProviderConfiguring the groups claimValues you map on
OktaCustomize tokens with a groups claimGroup names (for example, app-admins)
Microsoft Entra ID (Group Claim)Configure group claimsGroup Object IDs (GUIDs), by default
Microsoft Entra ID (App Roles)Configure app rolesApp Role Value, by default

To use group display names in Entra ID instead of GUIDs, configure the claim to emit cloud-group display names. This requires restricting the claim to groups assigned to the application, which is also the recommended way to stay under the group limit.

Step 2: Point Chainguard at the groups claim

Update the identity provider you use to log in so Chainguard requests the groups claim and knows which claim carries group membership:

chainctl iam identity-providers update $IDENTITY_PROVIDER \
  --oidc-additional-scopes=groups \
  --oidc-groups-claim=groups

--oidc-additional-scopes=groups should not be set when using Entra ID.

  • --oidc-additional-scopes=groups tells Chainguard to request the groups claim.
  • --oidc-groups-claim=groups tells Chainguard which claim carries group membership, using the name from Step 1. An empty value turns group mapping off for this provider.

Step 3: Map a group to a role

Create a mapping from an IdP group to a Chainguard role:

chainctl iam external-group-role-mappings create \
  --external-group-id "GROUP" \
  --role editor \
  --scope $ORGANIZATION \
  --idp $IDENTITY_PROVIDER
  • --external-group-id is the value the IdP emits: the group name for Okta (for example, app-admins), or the group Object ID (GUID) for Entra ID.
  • --role is the role to grant, by name or UIDP, such as viewer or editor.
  • --scope is the UIDP of the organization where the role applies.
  • --idp is the identity provider that owns the mapping.

To review the mappings you’ve configured, run the list subcommand:

chainctl iam external-group-role-mappings list --parent $ORGANIZATION

Each command creates one mapping. To map many groups at once, see Automate mappings with the API.

Step 4: Verify the mapping

  1. Have a user who belongs to the mapped group log in to Chainguard through your IdP.
  2. Confirm that the user can perform actions the granted role allows. This access doesn’t appear in chainctl iam role-bindings list, because it’s session-scoped.
  3. Have a user who doesn’t belong to a mapped group log in and confirm that they receive no additional access.

Manage access

Once mappings are in place, you can adjust access by changing group membership within your identity provider, or by changing the mappings themselves:

  • Grant more than one role to a group. Create one mapping per role for the same group. The capabilities combine.

  • Remove one user’s group-derived access. Remove that user from the group in your IdP, or unassign them from the application. Only that user is affected; everyone else in the group keeps the mapped role.

  • Remove a role from everyone in a group. Delete the mapping. This revokes the mapped role for all users in that group and leaves their other access intact:

    chainctl iam external-group-role-mappings delete $MAPPING_ID

    Be sure to replace $MAPPING_ID with the mapping’s UIDP, which you can find by running the chainctl iam external-group-role-mappings list command shown in Step 3.

  • Remove every mapping for an identity provider. When offboarding a provider, delete all of its mappings in one command. chainctl asks you to confirm first; include the --yes flag to skip the prompt:

    chainctl iam external-group-role-mappings delete --all --idp $IDENTITY_PROVIDER

    This revokes the mapped roles for all users across that IdP’s groups and leaves their other access intact. Note that this cleanup step is optional; deleting the identity provider also deletes any mappings that still exist. Running it first is still worth doing — the confirmation shows exactly how many mappings you’re revoking before the provider itself is removed.

Each of these changes takes effect at the affected user’s next login, within one hour.

Revoke access urgently

When you need to revoke a user’s access immediately, as with a compromised account or a departing employee, deleting the mapping alone is not enough. A session that already resolved the group-derived role keeps it until the access token expires, up to an hour, and that token can’t be revoked mid-session. The fix is to stop the user from re-deriving the role at their next login:

  1. In your identity provider, disable the user’s account or remove them from the mapped group. This prevents their next login from granting the role.
  2. Delete the mapping if you also want to revoke the role for everyone else in the group.

The existing session still expires on its own within the hour, and the IdP change blocks re-authorization after that.

Automate mappings with the API

Each chainctl command in this guide handles one mapping. When your IdP has dozens or hundreds of groups to map, or when you manage Chainguard access from a pipeline, use the Chainguard API instead. Its ExternalGroupRoleMappings endpoints create, list, and delete the same mappings chainctl does, and the results behave identically: additive, session-scoped, and re-evaluated at each login.

These examples reuse the ORGANIZATION and IDENTITY_PROVIDER variables from the prerequisites. Add a token and the API host:

export TOKEN=$(chainctl auth token)
export API=https://console-api.enforce.dev

That token is your own and expires within an hour. To run these calls from a pipeline, authenticate as an assumable identity that holds the access described in Permissions for the calling identity.

Look up the role UIDP

chainctl accepts a role name, but the API takes the role’s UIDP. Retrieve it by name:

export ROLE=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "$API/iam/v2/roles?name=editor" | jq -r '.roles[0].uid')

Create one mapping

The identity provider owns the mapping, so its UIDP goes in the request path and the body carries the rest. The fields correspond to the flags in Step 3:

chainctl flagAPI equivalent
--idpThe identity provider UIDP in the request path
--external-group-idexternalGroupId
--roleroleUid, which takes the role’s UIDP rather than its name
--scopescope
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/iam/v2/externalGroupRoleMappings/$IDENTITY_PROVIDER" \
  -d "{\"externalGroupId\": \"app-admins\", \"roleUid\": \"$ROLE\", \"scope\": \"$ORGANIZATION\"}" | jq .
{
  "uid": "d9e2f1a0.../4b0a7c19c3e2f8d1/e54a7ea6f02e5dff",
  "identityProviderUid": "d9e2f1a0.../4b0a7c19c3e2f8d1",
  "externalGroupId": "app-admins",
  "roleUid": "0e4b93c2...",
  "scope": "d9e2f1a0...",
  "createTime": "2026-09-10T18:04:21.968Z"
}

Keep the uid from the response. It’s the mapping’s own UIDP, rooted under the identity provider, and deleting the mapping later requires it.

Create many mappings

The API has no batch create, so loop over your groups. This example maps every group listed in groups.txt to one role and reports the result of each call:

while read -r group; do
  status=$(curl -s -o response.json -w '%{http_code}' \
    -X POST -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    "$API/iam/v2/externalGroupRoleMappings/$IDENTITY_PROVIDER" \
    -d "{\"externalGroupId\": \"$group\", \"roleUid\": \"$ROLE\", \"scope\": \"$ORGANIZATION\"}")
  case "$status" in
    200) echo "created: $group" ;;
    409) echo "exists:  $group" ;;
    *)   echo "failed:  $group (HTTP $status)"; jq -c . response.json ;;
  esac
done < groups.txt

A mapping that already exists returns HTTP 409 rather than a second record, so you can re-run the loop after fixing a failure without creating anything twice. To grant more than one role, run the loop once per role with its own group list.

List mappings

Read back every mapping under an identity provider:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$API/iam/v2/externalGroupRoleMappings?identity_provider_uid=$IDENTITY_PROVIDER" \
  | jq '{totalCount, mappings: [.externalGroupRoleMappings[] | {uid, externalGroupId, roleUid}]}'

totalCount reports how many mappings match. Results are paginated, so a nextPageToken in the response means more pages remain, as described in Pagination.

Delete mappings

Delete a single mapping by its UIDP:

curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  "$API/iam/v2/externalGroupRoleMappings/$MAPPING_UID"

A successful delete returns an empty JSON object, and a mapping that’s already gone returns HTTP 404.

To remove several mappings in one call, pass their UIDPs to the :batchDelete endpoint along with the identity provider they belong to:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/iam/v2/externalGroupRoleMappings:batchDelete" \
  -d "{\"parent\": \"$IDENTITY_PROVIDER\", \"names\": [\"$MAPPING_UID_1\", \"$MAPPING_UID_2\"]}" | jq .

The response lists the mappings it deleted. Names that no longer exist are skipped without error, so repeating a teardown is safe. Every name must belong to the identity provider named in parent; one that doesn’t fails the whole call with INVALID_ARGUMENT and deletes nothing.

The API has no equivalent of the --all flag. To clear every mapping for a provider you’re offboarding, list them first and feed their UIDPs to :batchDelete:

MAPPINGS=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "$API/iam/v2/externalGroupRoleMappings?identity_provider_uid=$IDENTITY_PROVIDER" \
  | jq -c '[.externalGroupRoleMappings[].uid]')

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/iam/v2/externalGroupRoleMappings:batchDelete" \
  -d "{\"parent\": \"$IDENTITY_PROVIDER\", \"names\": $MAPPINGS}" | jq .

This revokes the mapped roles for every group under that provider, so review the list output before you run the delete.

Permissions for the calling identity

Creating a mapping grants a role, so the API enforces an anti-escalation rule. The identity making the call needs permission to create identity providers and role bindings, and it must already hold every capability the mapped role grants. An identity can’t create a mapping that grants access it doesn’t have itself. The owner role satisfies all three requirements.

Limits

Identity providers cap how many groups a token can carry. Past that limit, the IdP stops sending the inline groups claim. This means Chainguard no longer receives the user’s groups, and their mappings don’t resolve. Keep the emitted set small by sending only the groups you map:

  • Okta: Filter the groups claim in Step 1 so the token carries only the groups you map rather than every group a user belongs to.
  • Microsoft Entra ID: Entra ID omits the groups claim once a user belongs to more than 200 groups (the JWT and OIDC limit; the SAML limit is 150). Past the limit, Entra ID emits an overage claim (_claim_names and _claim_sources) that points to Microsoft Graph instead of the inline list, and Chainguard doesn’t follow it. Avoid the overage by emitting only groups assigned to the application, as described in Step 1, or by using fewer, coarser groups for access.

Last updated: 2026-09-10 18:12