# Grant Chainguard roles from identity provider groups

URL: https://chainguard-docs-preview-git-fork-eslerm-eslerm-custom-id-f0ac5e.chainguard.app/platform/administration/custom-idps/grant-roles-from-groups.md
Last Modified: October 2, 2026
Tags: Chainguard Containers, Procedural

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

Chainguard can grant roles based on a user&rsquo;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:
A custom identity provider (such as Okta or Microsoft Entra ID) already configured for login to Chainguard. If you haven&rsquo;t set one up yet, refer to our guide Using custom identity providers to authenticate to Chainguard. An IAM role that can manage identity providers and role-bindings in your organization, such as the owner role. chainctl installed on your local machine. You must also authenticate with chainctl auth login. 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&rsquo;s UIDP in the ORGANIZATION variable:
export ORGANIZATION=$(chainctl iam organizations list -o json | jq -r &#39;.items[0].id&#39;)Store your identity provider&rsquo;s UIDP in the IDENTITY_PROVIDER variable:
export IDENTITY_PROVIDER=$(chainctl iam identity-providers list -o json | jq -r &#39;.items[0].id&#39;)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&rsquo;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&rsquo;s CloudEvents stream.
As a result, no single command or view answers &ldquo;who holds a group-derived role right now?&rdquo; 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&rsquo;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&rsquo;s own documentation. This guide relies only on the result:
The token carries a claim listing the user&rsquo;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. Provider Configuring the groups claim Values you map on Okta Customize tokens with a groups claim Group names (for example, app-admins) Microsoft Entra ID (Group Claim) Configure group claims Group Object IDs (GUIDs), by default Microsoft Entra ID (App Roles) Configure app roles App Role Value, by default Microsoft Entra ID Entra ID shows the application you registered for Chainguard in two places: under App registrations, and under Enterprise applications, where you assign users and groups to it. Chainguard reads groups only from tokens that Entra ID issues for that application. Check the following:
Configure the groups claim on the application your users sign in through. That&rsquo;s the application whose Application (client) ID you passed to --oidc-client-id. In App registrations, open that application, select Token configuration, and click Add groups claim. If your tenant has more than one Chainguard application, a groups claim configured on any other one doesn&rsquo;t reach Chainguard. To check, compare the client ID on the application you configured with the one in your Chainguard identity provider. Emit only groups assigned to the application, and assign each mapped group to it. When you add the groups claim, select Groups assigned to the application. Then, in Enterprise applications, open the same application, select Users and groups, and assign each mapped group. With this option, a group appears in the token only when it&rsquo;s assigned to the application and the user is a direct member of it. Membership in an unassigned group, or through a nested group, doesn&rsquo;t put the group in the token. This option is required to emit cloud-group display names instead of GUIDs, and it&rsquo;s the recommended way to stay under the group limit. If you provision users with SCIM, assign groups to the SSO application as well. The Entra ID SCIM guide creates a separate enterprise application for provisioning. Assigning a group to the SCIM application provisions its members but doesn&rsquo;t add the group to their login tokens. Verify the claim with a real login to Chainguard, as described in Step 4. The Test button in Token configuration (under App registrations) uses a test flow rather than a real sign-in, so it can show a groups claim that real logins don&rsquo;t carry. The same is true of any token you request outside a Chainguard login, such as a test sign-in through jwt.ms.
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 &#34;GROUP&#34; \ --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&rsquo;ve configured, run the list subcommand:
chainctl iam external-group-role-mappings list --parent $ORGANIZATIONEach command creates one mapping. To map many groups at once, see Automate mappings with the API.
Step 4: Verify the mapping Have a user who belongs to the mapped group log in to Chainguard through your IdP. Use a real login; a token from your IdP&rsquo;s test tools doesn&rsquo;t confirm what Chainguard receives. If the user is already logged in, have them log out first. Chainguard applies group-derived roles at login, so refreshing an existing session doesn&rsquo;t pick up a new mapping.
Confirm that the user received the granted role. This access doesn&rsquo;t appear in chainctl iam role-bindings list, because it&rsquo;s session-scoped. Instead, have the user log in with chainctl and inspect their session:
chainctl auth logout chainctl auth login --org-name &lt;organization-name&gt; chainctl auth statusThe Capabilities row lists each organization and role in the session, such as example.com: owner. In the Console, the user can confirm the role by performing an action it allows.
Have a user who doesn&rsquo;t belong to a mapped group log in and confirm that they receive no additional access.
If the user logs in but doesn&rsquo;t receive the mapped role, check that their token carries the group value you mapped in Step 3. For Entra ID, start with the application settings in Microsoft Entra ID.
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&rsquo;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_IDBe sure to replace $MAPPING_ID with the mapping&rsquo;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_PROVIDERThis revokes the mapped roles for all users across that IdP&rsquo;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&rsquo;re revoking before the provider itself is removed.
Each of these changes takes effect at the affected user&rsquo;s next login, within one hour.
Revoke access urgently When you need to revoke a user&rsquo;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&rsquo;t be revoked mid-session. The fix is to stop the user from re-deriving the role at their next login:
In your identity provider, disable the user&rsquo;s account or remove them from the mapped group. This prevents their next login from granting the role. 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.devThat 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&rsquo;s UIDP. Retrieve it by name:
export ROLE=$(curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2/roles?name=editor&#34; | jq -r &#39;.roles[0].uid&#39;) 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 flag API equivalent --idp The identity provider UIDP in the request path --external-group-id externalGroupId --role roleUid, which takes the role&rsquo;s UIDP rather than its name --scope scope curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings/$IDENTITY_PROVIDER&#34; \ -d &#34;{\&#34;externalGroupId\&#34;: \&#34;app-admins\&#34;, \&#34;roleUid\&#34;: \&#34;$ROLE\&#34;, \&#34;scope\&#34;: \&#34;$ORGANIZATION\&#34;}&#34; | jq .{ &#34;uid&#34;: &#34;d9e2f1a0.../4b0a7c19c3e2f8d1/e54a7ea6f02e5dff&#34;, &#34;identityProviderUid&#34;: &#34;d9e2f1a0.../4b0a7c19c3e2f8d1&#34;, &#34;externalGroupId&#34;: &#34;app-admins&#34;, &#34;roleUid&#34;: &#34;0e4b93c2...&#34;, &#34;scope&#34;: &#34;d9e2f1a0...&#34;, &#34;createTime&#34;: &#34;2026-09-10T18:04:21.968Z&#34; }Keep the uid from the response. It&rsquo;s the mapping&rsquo;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 &#39;%{http_code}&#39; \ -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings/$IDENTITY_PROVIDER&#34; \ -d &#34;{\&#34;externalGroupId\&#34;: \&#34;$group\&#34;, \&#34;roleUid\&#34;: \&#34;$ROLE\&#34;, \&#34;scope\&#34;: \&#34;$ORGANIZATION\&#34;}&#34;) case &#34;$status&#34; in 200) echo &#34;created: $group&#34; ;; 409) echo &#34;exists: $group&#34; ;; *) echo &#34;failed: $group (HTTP $status)&#34;; jq -c . response.json ;; esac done &lt; groups.txtA 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 &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings?identity_provider_uid=$IDENTITY_PROVIDER&#34; \ | jq &#39;{totalCount, mappings: [.externalGroupRoleMappings[] | {uid, externalGroupId, roleUid}]}&#39;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 &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings/$MAPPING_UID&#34;A successful delete returns an empty JSON object, and a mapping that&rsquo;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 &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings:batchDelete&#34; \ -d &#34;{\&#34;parent\&#34;: \&#34;$IDENTITY_PROVIDER\&#34;, \&#34;names\&#34;: [\&#34;$MAPPING_UID_1\&#34;, \&#34;$MAPPING_UID_2\&#34;]}&#34; | 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&rsquo;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&rsquo;re offboarding, list them first and feed their UIDPs to :batchDelete:
MAPPINGS=$(curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings?identity_provider_uid=$IDENTITY_PROVIDER&#34; \ | jq -c &#39;[.externalGroupRoleMappings[].uid]&#39;) curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2/externalGroupRoleMappings:batchDelete&#34; \ -d &#34;{\&#34;parent\&#34;: \&#34;$IDENTITY_PROVIDER\&#34;, \&#34;names\&#34;: $MAPPINGS}&#34; | 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&rsquo;t create a mapping that grants access it doesn&rsquo;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&rsquo;s groups, and their mappings don&rsquo;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&rsquo;t follow it. Avoid the overage by emitting only groups assigned to the application, as described in Microsoft Entra ID, or by using fewer, coarser groups for access. Related resources Using custom identity providers to authenticate to Chainguard How to integrate Okta SSO with Chainguard How to integrate Microsoft Entra ID SSO with Chainguard Overview of the Chainguard IAM model Manage identity and access with chainctl Subscribe to Chainguard Events Chainguard API v2 tutorial ExternalGroupRoleMappingsService in the API v2 specification 
