Integrate Keycloak with HashiCorp Vault
A How-To guide using Terraform
Hashicorp Vault is an open-source tool to manage secrets and secret access. The official definition of a secret in Vault:
A secret is anything that you want to tightly control access to, such as API keys, passwords, certificates, and more. — Vault Documentation
Access to secrets is granted via group memberships and the corresponding policies. Despite you can manage users within Vault, in an enterprise context users are often managed centrally. There are several capabilities to authenticate users to Vault and this post elaborates on how to integrate the open source identity provider Keycloak with Vault. You can have a look at the sample project on GitHub.
This is a how to guide and the samples are written in Terraform. Nevertheless you can easily follow these steps and set up everything via the corresponding UI.
Prerequisites to follow with the sample project:
- Running Vault server
- Running Keycloak
- Terraform cli
Configure Keycloak
First, we need to prepare Keycloak to be able to link an OIDC client to Vault. If you have already set up Keycloak with realms and users you can skip this part.
- Create a realm
resource "keycloak_realm" "realm" {
realm = "demo-realm"
enabled = true
}
2. Add users to the realm, for this demo we have to set up Alice and Bob.
resource "keycloak_user" "user_alice" {
realm_id = keycloak_realm.realm.id
username = "alice"
enabled = true
email = "alice@domain.com"
first_name = "Alice"
last_name = "Aliceberg"
initial_password {
value = "alice"
temporary = false
}
}
3. Create an OIDC client, you might notice the resource is called keycloak_openid_client. OIDC is OpenID Connect, a standard built on top of the OAuth2.0 authorization framework. As valid_redirect_uris for the client, we define the Vault endpoint (in the sample project it’s localhost).
resource "keycloak_openid_client" "openid_client" {
realm_id = keycloak_realm.realm.id
client_id = "vault"
name = "vault"
enabled = true
standard_flow_enabled = true
access_type = "CONFIDENTIAL"
valid_redirect_uris = [
"http://localhost:8200/*"
]
login_theme = "keycloak"
}
4. Define client roles, according to your use-case. In this sample, we will have a management and a reader role. You can use composite roles to benefit from permission inheritance for fine granular access controls. In this sample, the management inherits the permissions of the reader role.
resource "keycloak_role" "management_role" {
realm_id = keycloak_realm.realm.id
client_id = keycloak_openid_client.openid_client.id
name = "management"
description = "Management role"
composite_roles = [
keycloak_role.reader_role.id
]
}
resource "keycloak_role" "reader_role" {
realm_id = keycloak_realm.realm.id
client_id = keycloak_openid_client.openid_client.id
name = "reader"
description = "Reader role"
}
5. To finish the Keycloak configuration, we need to add the claims to the idToken which is issued for Vault access. The claims are added under the claim_name specification. The roles key at root level is a reserved key and can not be used. In this sample we put the claims under resource_access.vault.roles.
resource "keycloak_openid_user_client_role_protocol_mapper"
"user_client_role_mapper" {
realm_id = keycloak_realm.realm.id
client_id = keycloak_openid_client.openid_client.id
name = "user-client-role-mapper"
claim_name = format("resource_access.%s.roles",
keycloak_openid_client.openid_client.client_id)
multivalued = true
}
6. If you enable Direct Access Grants for the client, you can test your setup and issue an idToken using eg. curl.
curl \
--data "username=bob&password=bob&grant_type=password& client_id=vault&client_secret=<CLIENT_SECRET>" \ http://localhost:8080/auth/realms/demo-realm/protocol/openid-connect/token
So far, we are done with the Keycloak configuration. We proceed to enable the OIDC authentication engine in Vault and setup the corresponding mapping between the token claims and Vault policies.
Configure Vault
To understand which internal Vault resources we are going to configure, the following is a high-level overview of how these resources are related to each other. We need to set up policies, groups, and entities as well as a mapping between the Vault internal structure and the one provided by Keycloak.

- Vault will sign each token that is issued by the secrets engine. Hence we provide a key for our OIDC identity.
resource "vault_identity_oidc_key" "keycloak_provider_key" {
name = "keycloak"
algorithm = "RS256"
}
2. We have to enable the OIDC auth backend for Vault. The listing_visibility set to unauth, enables the auth method to be present in the login screen. If you omit the default_role, you need to specify a role each time you log in. For better usability specify a default role with the least privileges. Notice that oidc_discovery_url is the URL of Keycloak, in this example running on localhost.
resource "vault_jwt_auth_backend" "keycloak" {
path = "oidc"
type = "oidc"
default_role = "default"
oidc_discovery_url = format("http://localhost:8080/auth/realms/%s,
keycloak_realm.realm.id)
oidc_client_id = keycloak_openid_client.openid_client.client_id
oidc_client_secret =
keycloak_openid_client.openid_client.client_secret
tune {
audit_non_hmac_request_keys = []
audit_non_hmac_response_keys = []
default_lease_ttl = "1h"
listing_visibility = "unauth"
max_lease_ttl = "1h"
passthrough_request_headers = []
token_type = "default-service"
}
}
3. Define a backend role to be used for authentication and to assign/ map permissions to a user. Here happens the magic. The user_claim is the unique identifier, for whom the token is issued. The identifier is used to create Vault entities on the fly when a user logs in via the OIDC method. Each entity can be enriched with some metadata from the token. Define a mapping within the claim_mapping property. To dynamically assign Vault policies for grants from Keycloak (claims), we have to tell Vault where the claims are listed in the idToken. We set this in the Keycloak preparation section to resource_access.vault.roles. Vault expects nested JSON attributes in JSON-Path syntax which means resource_access.vault.roles in JSON Path notation becomes /resource_access/vault/roles.
resource "vault_jwt_auth_backend_role" "default" {
backend = vault_jwt_auth_backend.keycloak.path
role_name = "default"
role_type = "oidc"
token_ttl = 3600
token_max_ttl = 3600
bound_audiences = [keycloak_openid_client.openid_client.client_id]
user_claim = "sub"
claim_mappings = {
preferred_username = "username"
email = "email"
}
allowed_redirect_uris = [
"http://localhost:8200/ui/vault/auth/oidc/oidc/callback",
"http://localhost:8250/oidc/callback"
]
groups_claim = format("/resource_access/%s/roles",
keycloak_openid_client.openid_client.client_id)
}
4. Our authentication backend in Vault is ready to use. Now we need to provide some policies and groups that Vault can actually grant permissions to resources based on the idToken. In this sample we have a management and a reader policy, where only the management policy grants write access to secrets. Note that we configured the management role on Keycloak as a composite role inheriting the reader role, hence we don’t have to grant read, list permissions in multiple policies.
data "vault_policy_document" "reader_policy" {
rule {
path = "/secret/*"
capabilities = ["list", "read"]
}
}
resource "vault_policy" "reader_policy" {
name = "reader"
policy = data.vault_policy_document.reader_policy.hcl
}data "vault_policy_document" "manager_policy" {
rule {
path = "/secret/*"
capabilities = ["create", "update", "delete"]
}
}
resource "vault_policy" "manager_policy" {
name = "management"
policy = data.vault_policy_document.manager_policy.hcl
}
5. Now that we have created policies, we assign them to groups. Additionally we create a role (entity) and assign the signing key from our OIDC client. There are different types of Vault groups, external and internal. External groups are considered to be a mapper to groups, managed from an external system, in that case, Keycloak.
resource "vault_identity_oidc_role" "management_role" {
name = "management"
key = vault_identity_oidc_key.keycloak_provider_key.name
}
resource "vault_identity_group" "management_group" {
name = vault_identity_oidc_role.management_role.name
type = "external"
policies = [
vault_policy.manager_policy.name
]
}
6. Last step is to create an alias for the external group. This alias is supposed to be a Vault internal representation of the identity from the external system. In terms of users managed in an active directory, the alias has to represent the respective role.
resource "vault_identity_group_alias" "management_group_alias" {
name = "management"
mount_accessor = vault_jwt_auth_backend.keycloak.accessor
canonical_id = vault_identity_group.management_group.id
}
Verify
Let’s verify our setup. Either browse to your existing Vault or use the URL from the demo http://localhost:8200. Remember, in our setup, we configured a default role, hence no need to specify a role for the login. If you didn’t define a default role you have to choose an existing one, otherwise you’re not able to proceed.

If you hit the “Sign in with OIDC Provider” button, Keycloak pop-up will open and ask you to log in. Either choose alice or bob. After you’re logged in you should be redirected to Vault, where Alice is only permitted to read secrets and Bob has write access.

Recap
Vault is quite powerful, easy to use, and very extensible. We saw how easy it is to integrate any identity provider over OIDC such as Keycloak. While the Terraform Keycloak provider is a third-party provider, the Vault integration is officially supported by HashiCorp. You can fully write your Vault setup in HCL and manage your cluster with Terraform. Currently, HashiCorp offers a free public beta for fully managed Vault on HashiCorp Cloud Platform.
Thank you for reading, you can reach me out via:
- Twitter: @pascal_euhus