Hashicorp Vault integration with OIDC provider using Identity Groups

One of the most common ways to give users access to Vault is by integrating it with an external OIDC-compliant identity provider like Okta, Auth0, Azure Entra ID, Keycloak and so on.
If you read Vault documentation on how to set up OIDC auth method you’ll learn the following typical steps:
- Create OIDC app in your identity provider. Assign “groups” claim to it and add Vault URL to the list of allowed redirect URIs.
- Create client credentials (client id and secret) that will be used by Vault to verify user identities. These two steps are common between all OIDC-compliant providers.
- Create Vault policy for the user/group.
- Create Vault OIDC role that serves as a mapper between external user/group identity and Vault policy created at step 3. Mapping is done via
bound_claims
role attribute.
As you can see Vault OIDC role plays a critical part in the authentication process. For example, I have Okta configured as an OIDC provider, so the users are greeted with the following screen when they want to log in to Vault UI:

Here comes the problem!
If you have multiple user groups/teams you need to create multiple roles for them, right? That requires users to remember their roles, which sounds awkward! It sounds inconvenient for a user to always remember his Vault role. It becomes even more inconvenient if a user is a member of multiple groups (a common situation when a developer writes code for different applications). Because now he has to remember all his roles. That doesn’t sound very user-friendly, does it?
Thankfully, Vault has a solution for this problem. However, the solution is not at all obvious and pretty much hidden in the documentation. It took me almost two years to understand what they’re saying in the docs.
It’s called Identity Entities and Groups. In terms of OIDC auth method it allows you to do this:
- Create a default OIDC role without any permissions.
- Create an Identity entity/group with arbitrary name and desired policies assigned to it.
- Create an Identity entity/group alias with the name that matches external user or group name.
Here’s an interesting excerpt from the Vault “Create OIDC/JWT role” API documentation that serves as a hint:
user_claim
(string: <required>)
- The claim to use to uniquely identify the user; this will be used as the name for the Identity entity alias created due to a successful login.groups_claim
(string: <optional>)
- The claim to use to uniquely identify the set of groups to which the user belongs; this will be used as the names for the Identity group aliases created due to a successful login.
Let me show you an example with Okta as an identity provider. Keep in mind you can always create a free Okta account at https://developer.okta.com.
Let’s say we have an Okta organization and we want to give members of Okta group Okta-Vault-Admins
admin permissions to the Vault. We would also like to give members of the group Okta-Vault-Developers
read-only permissions to KV secret engine mounted under kv/
path.
First of all, you need to create Okta app for Hashicorp Vault:
- Sign in to Okta Admin dashboard and create desired users and groups under “Directory”. You can also sync users and groups from other identity providers like Active Directory, Azure Entra ID and others.
- Go to “Applications” and click “Create a new app integration”.
- Choose “OIDC OpenID Connect” and “Web application”.
- On the next screen specify app name (like Hashicorp Vault) in “App integration name” and enable “Client credentials” checkbox.
- In Sign-in redirect URIs you need to add two URLs:
a.$VAULT_ADDR/ui/vault/auth/oidc/oidc/callback
where $VAULT_ADDR is the URL of your Vault instance.
b.http://localhost:8250/oidc/callback
for Vault CLI. - Under Assignments you can decide whether to enable Vault access for all members of your Okta organization or whether to allow it only for a subset of groups.
After app creation you can further adjust the OIDC groups
claim (it’s a JSON field in the ID token that contains the list of groups the user belongs to). For example, you could limit the groups to those starting with Okta-Vault-
. This is done under application’s “Sign On” tab in “OpenID Connect ID Token” section.
Once the app is created and configured you can copy Client ID from “Client Credentials” section and Client Secret from “Client Secrets”. You will also need to copy Okta Issuer URL from “Security” -> “API”.
export OKTA_CLIENT_ID=<CLIENT_ID>
export OKTA_CLIENT_SECRET=<CLIENT_SECRET>
export OKTA_URL=<OKTA_ISSUER_URL>
export VAULT_ADDR=<VAULT_URL>
Now it’s time to set up the Vault OIDC integration. First let’s create admin and developer policies.
# Admin policy gives unlimited Vault permissions
cat > admin.hcl <<EOF
path "*" {
capabilities = ["sudo","read","create","update","delete","list","patch"]
}
EOF
vault policy write admin admin.hcl
# Developer role allows to list and read secrets under "kv/" path
cat > developer.hcl <<EOF
path "kv/*" {
capabilities = ["read", "list"]
}
EOF
vault policy write developer developer.hcl
Next we enable OIDC auth method according with the documentation:
# Enable OIDC auth method
vault auth enable oidc
# Add Okta connection details and set the default role
vault write auth/oidc/config \
oidc_discovery_url="$OKTA_URL" \
oidc_client_id="$OKTA_CLIENT_ID" \
oidc_client_secret="$OKTA_CLIENT_SECRET" \
default_role="default"
# Add OIDC auth method to the UI login screen
vault auth tune -listing-visibility=unauth oidc
The default_role
here is very important since it will be the only role in the OIDC auth method. Let’s go ahead and create the default
role:
vault write auth/oidc/role/default \
bound_audiences="$OKTA_CLIENT_ID" \
allowed_redirect_uris="$VAULT_ADDR/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" \
user_claim="user.email" \
groups_claim="groups" \
token_policies="default"
As you know default
policy exists by default, cannot be removed and gives no special permissions.
Now let’s go ahead and create external identity groups Okta-Vault-Admins
and Okta-Vault-Developers
. These names technically should not match with Okta group names but for the sake of operational simplicity let’s have them match.
vault write -format=json identity/group name="Okta-Vault-Admins" \
policies="admin" \
type="external" | jq -r ".data.id" > admin_group_id.txt
vault write -format=json identity/group name="Okta-Vault-Developers" \
policies="developer" \
type="external" | jq -r ".data.id" > developer_group_id.txt
Now let’s create group aliases that must match Okta group names.
vault auth list -format=json | jq -r '.["oidc/"].accessor' > accessor.txt
vault write identity/group-alias name="Okta-Vault-Admins" \
mount_accessor=$(cat accessor.txt) \
canonical_id="$(cat admin_group_id.txt)"
vault write identity/group-alias name="Okta-Vault-Developers" \
mount_accessor=$(cat accessor.txt) \
canonical_id="$(cat developer_group_id.txt)"
This is it! Now when the user logs in via OIDC auth method he gets assigned a default
policy but his groups
claim from Okta will be also matched against identity group aliases. The user will be assigned all the matched group policies.
Let’s test it out.
$ vault login -method=oidc
Complete the login via your OIDC provider. Launching browser to:
https://REDACTED.okta.com/oauth2/default/v1/authorize?client_id=REDACTED&code_challenge=REDACTED&code_challenge_method=S256&nonce=n_66KsLhMrbu9KniAjA3Xn&redirect_uri=http%3A%2F%2Flocalhost%3A8250%2Foidc%2Fcallback&response_type=code&scope=openid&state=st_isH1SA1UhWo7qdpAGFuN
Waiting for OIDC authentication to complete...
WARNING! The VAULT_TOKEN environment variable is set! The value of this
variable will take precedence; if this is unwanted please unset VAULT_TOKEN or
update its value accordingly.
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token hvs.CAESIPTMBUZRVGYMs5IamLGcQM8fRGhi85BqB3NA0Q_nyaJwGh4KHGh2cy5GVjR6NG5GZ0JlTGsxZmlKSGFCTkpKTDc
token_accessor DZzbTeEliGIqGA0rtkwaiUPC
token_duration 1h
token_renewable true
token_policies ["default"]
identity_policies ["admin"]
policies ["admin" "default"]
token_meta_role default
Here we can see that user was assigned default
policy from token (we configured that on default
role creation) and admin
policy from his Okta identity (comes from identity alias). The resulting set of policies is admin, default
which is exactly what we want.
Finally let’s have a look at identity objects that were created.
$ vault list identity/group/name
Keys
----
Okta-Vault-Admins
$ vault read identity/group/name/Okta-Vault-Admins
Key Value
--- -----
alias map[canonical_id:425da627-730d-8579-d865-d1e70400d923 creation_time:2024-11-03T20:14:00.764054964Z id:e1654fa4-2516-71b6-fe4a-fae7cc509039 last_update_time:2024-11-04T11:27:39.855948677Z merged_from_canonical_ids:<nil> metadata:<nil> mount_accessor:auth_oidc_61baf77a mount_path:auth/oidc/ mount_type:oidc name:Okta-Vault-Admins]
creation_time 2024-11-03T20:13:58.913899344Z
id 425da627-730d-8579-d865-d1e70400d923
last_update_time 2024-11-04T11:27:39.85594435Z
member_entity_ids [8197c3f4-682c-b36d-1107-3e0fb3cee71c]
member_group_ids <nil>
metadata map[]
modify_index 6
name Okta-Vault-Admins
namespace_id root
parent_group_ids <nil>
policies [admin]
type external
$ vault list identity/entity/name
Keys
----
entity_176c43a8
$ vault read identity/entity/name/entity_176c43a8
Key Value
--- -----
aliases [map[canonical_id:8197c3f4-682c-b36d-1107-3e0fb3cee71c creation_time:2024-11-03T20:15:38.898546783Z custom_metadata:<nil> id:e18efc0b-c505-ef98-cf0b-c719a2c4fdf8 last_update_time:2024-11-03T20:15:38.898546783Z local:false merged_from_canonical_ids:<nil> metadata:map[role:default] mount_accessor:auth_oidc_61baf77a mount_path:auth/oidc/ mount_type:oidc name:user@example.com]]
creation_time 2024-11-03T20:15:38.898534584Z
direct_group_ids [425da627-730d-8579-d865-d1e70400d923]
disabled false
group_ids [425da627-730d-8579-d865-d1e70400d923]
id 8197c3f4-682c-b36d-1107-3e0fb3cee71c
inherited_group_ids []
last_update_time 2024-11-03T20:15:38.898534584Z
merged_entity_ids <nil>
metadata <nil>
name entity_176c43a8
namespace_id root
policies []
Thanks a lot for reading!