SSO with Keycloak and Hashicorp Vault

Managing access to all of your internal systems can be a real pain in the neck. Many companies are turning to services such as Okta, OneLogin, JumpCloud, et. al to bring user management under control. When it comes to on-premise solutions, there are a few well-respected offerings – one of them being the open-source, RedHat-sponsored application KeyCloak.

A sample deployment of KeyCloak backed by Active Directory used as an Auth Backend by Hashicorp Vault.
A sample deployment of KeyCloak backed by Active Directory used as an Auth Backend by Hashicorp Vault.

In this article, we will configure Hashicorp Vault and KeyCloak so that users can log into Vault using their KeyCloak credentials.

Table of Contents

    Prerequisites

    • A running Hashicorp Vault installation with root token or admin-level token
    • A running KeyCloak installation with admin access
    • Terraform Open Source
    • Editor of your choosing

    Create a Vault Admin group in KeyCloak

    We will create a group within Keycloak called “vault-admin”. Later on, we will associate this group with a role within Vault that allows users to perform some administrative tasks.

    The vault-admin group displayed in the Groups management pane in KeyCloak.
    The vault-admin group displayed in the Groups management pane in KeyCloak.

    Add a group

    We will create the vault-admin group with KeyCloak.

    1. From the Keycloak Administrative Interface, click on Manage >> Groups
    2. Click the New button
    3. Enter “vault-admin”
    4. Click on Save

    Add User to vault-admin group

    We will add your user to the vault-admin group.

    1. From the KeyCloak Administrative Interface, click on Manage >> Users
    2. Highlight the “vault-admin” group under the Available Groups
    3. Click Join

    Create Vault Client within KeyCloak

    In order for Vault use KeyCloak as an identity provider (IdP), we must create a unique OpenID Connect (OIDC) client. This will create a set of application credentials that will allow Vault to participate in OAuth and read user token information.

    Add a client for Vault

    Adding a new OpenID Connect client in KeyCloak.
    Adding a new OpenID Connect client in KeyCloak.

    To to create a new client:

    1. Within the KeyCloak Admin console, click on Configure >> Clients
    2. Click the Create button
    3. Fill out the form accordingly:
      • Client ID: Your choice. “vault-demo” is used throughout this example.
      • Client Protocol: openid-connect
    4. Click the Save button

    This will provide an initial client configuration. There will be more settings to configure at this point.

    Configure the Vault client

    After creating the client in the step above, you should now be redirected to the client’s Settings tab.

    Fill out the form accordingly:

    1. Access Type: confidential
    2. Root URL: https://[[YOUR_VAULT_ADDR]]
    3. Base URL: https://[[YOUR_VAULT_ADDR]]
    4. Web Origins: https://[[YOUR_VAULT_ADDR]]
    5. Valid Redirect URIs:
      • http://localhost:8250/oidc/callback
      • https://[[YOUR_VAULT_ADDR]]/ui/vault/auth/oidc/oidc/callback
    6. Click the Save button at the bottom of the page

    Map Groups to User token claim

    In this implementation, we want members of the “vault-admin” KeyCloak group to be assigned a corresponding Vault role. In order to achieve this, we need to include group membership into the tokens issued by the KeyCloak client. In KeyCloak we need to setup a “Mapper”, a little automated snippet that knows how to include groups in the user token scope.

    The Groups Mapper configuration allows for a KeyCloak user's groups to be included in the ID token.
    The Groups Mapper configuration allows for a KeyCloak user’s groups to be included in the ID token.
    1. Within the client configuration page, click the Mappers tab
    2. Click the Create button
    3. Fill out the form accordingly:
      • Name: groups
      • Mapper Type: Group Membership
      • Token Claim Name: groups
      • Full group path: OFF
    4. Click the Save button.

    This will create a claim (field on the user token) called “groups” and will map a user’s KeyCloak groups to this list automatically.

    Validate the User token scope

    Now that we configured the group membership mapper, let’s verify that this is properly configured.

    Procedure of verifying the ID Token within KeyCloak for a given user.
    Procedure of verifying the ID Token within KeyCloak for a given user.
    1. Within the KeyCloak console, go to Configure >> Clients >> Your client (vault-demo)
    2. Click on the Client Scopes tab
    3. Click on the Evaluate sub-tab
    4. Change the User selector to your user
    5. Click the Evaluate button
    6. Click the Generated ID Token tab at the bottom of the page

    We are now presented with a JSON representation of the User ID Token for your user.

    {
      "exp": 1234845375,
      "iat": 1234845315,
      "auth_time": 0,
      "jti": "abc123-5958-4a69-4fda-c93025bd258d",
      "iss": "https://keycloak.fqdn.com/auth/realms/master",
      "aud": "vault-demo",
      "sub": "e0118be4-7bee-4ef7-a6b3-deadbeefcafe",
      "typ": "ID",
      "azp": "vault-demo",
      "session_state": "0167d412-efc5-4d79-9622-2b24bd96368d",
      "acr": "1",
      "sid": "deadbeef-efc5-4d79-9622-2b24bd96368d",
      "email_verified": true,
      "name": "Kevin Wojkovich",
      "groups": [
        "another-group",
        "vault-admin"
      ],
      "preferred_username": "kwojkovich",
      "given_name": "Kevin",
      "family_name": "Wojkovich",
      "email": "kevin@fqdn.com"
    }

    Verify that there is a field called “groups” with a list containing “vault-admin”.

    Copy Client ID and Client Secret

    Now that we have configured the client we will need to take note of the client ID and client secret for the Vault client. In KeyCloak, the client ID is the name of the client we created in the first step. The client secret is shown under the Credentials tab for the client.

    Procedure for obtaining OpenID Connect client_id and client_secret from KeyCloak.
    Procedure for obtaining OpenID Connect client_id and client_secret from KeyCloak.

    Configure the Vault OIDC Auth Engine

    This section focuses on setting up the Terraform Infrastructure as Code (IaC) that is responsible for configuring Hashicorp Vault. Leveraging infrastructure as code gives us a reproducible and auditable configuration.

    You may choose to download the Terraform code files directly if you wish.

    Setup the Terraform Project

    We will create a directory with several Terraform (.tf) files that will contain the code necessary to configure Vault to allow authentication from KeyCloak using the OpenID Connect (OIDC) protocol.

    Create a new directory called terraform_vault/. This directory will be used to store our Terraform code.

    Connect to Vault with a provider

    Terraform uses something called a “provider” to drive the APIs necessary to manage resources declared in Terraform code. We will use the hashicorp/vault provider to interact with our Vault instance.

    Create a new file called providers.tf. Withinproviders.tf, copy and paste the following content:

    terraform {
      required_providers {
        vault = {
          source  = "hashicorp/vault"
          version = "2.24.0"
        }
      }
    }
    
    provider "vault" {
    }

    The code within providers.tf uses a pattern called Partial Configuration meaning that in order for Terraform to successfully apply, you will need to supply additional information.

    This provides several benefits:

    • Allows us to re-use our Terraform code to manage multiple Vault instances.
    • Prevents hard-coding sensitive information into your Terraform code.

    In our case, we need to provide credentials that supply the vault address and vault token. This provider knows to use VAULT_ADDR and VAULT_TOKEN if they are set as environment variables. We will set these during our Terraform Plan and Terraform Apply steps.

    Declare variables

    When authoring code, we want to try to write DRY code allowing us to reuse our Terraform code in other projects. We separate out variables into a separate file allowing us to reuse the code within our project.

    Create a new file called variables.tf and paste the following content:

    variable "client_secret" {
      description = "Required: OIDC Client secret for Hashicorp Vault"
      type        = string
    }
    
    variable "client_id" {
      description = "Required: OIDC Client ID for Hashicorp Vault"
    }
    
    variable "discovery_url" {
      description = "OIDC Discovery endpoint"
      type        = string
      default     = "https://keycloak.fqdn.com/auth/realms/master"
    }
    
    variable "authorized_redirects" {
      description = "List of authorized redirects for Okta OIDC"
      type        = list(string)
      default     = ["http://localhost:8250/oidc/callback", "https://[[YOUR_VAULT_ADDR]]/ui/vault/auth/oidc/oidc/callback"]
    }

    The variables will be provided to Terraform during the Plan and Apply stages, however please update the BOLD default values to suit your deployment parameters. You will need to supply the discovery_url default value as well as the user-accessible endpoint for your Vault instance under authorized_redirects.

    Generally-speaking the discovery_url should be the path to your KeyCloak realm. More detailed documentation is available in KeyCloaks documentation if you need more advanced guidance.

    Speaking of authorized_redirects, the localhost value should not be changed, it will be used to complete the OpenID Connect authentication flow when using the Vault CLI from your workstation.

    Create a Vault Admin policy

    Vault leverages access control list (ACL) policies to give access to identity entities (users, groups, roles, etc). We will manage our Vault policies in-line with Terraform.

    Create a new file called policies.tf and paste the following content:

    resource "vault_policy" "vault_admin" {
      name   = "vault-admin"
      policy = <<EOT
    # Read system health check
    path "sys/health"
    {
      capabilities = ["read", "sudo"]
    }
    
    # Create and manage ACL policies broadly across Vault
    
    # List existing policies
    path "sys/policies/acl"
    {
      capabilities = ["list"]
    }
    
    # Create and manage ACL policies
    path "sys/policies/acl/*"
    {
      capabilities = ["create", "read", "update", "delete", "list", "sudo"]
    }
    
    # Enable and manage authentication methods broadly across Vault
    
    # Manage auth methods broadly across Vault
    path "auth/*"
    {
      capabilities = ["create", "read", "update", "delete", "list", "sudo"]
    }
    
    # Create, update, and delete auth methods
    path "sys/auth/*"
    {
      capabilities = ["create", "update", "delete", "sudo"]
    }
    
    # List auth methods
    path "sys/auth"
    {
      capabilities = ["read"]
    }
    
    # Enable and manage the key/value secrets engine at `secret/` path
    
    # List, create, update, and delete key/value secrets
    path "secret/*"
    {
      capabilities = ["create", "read", "update", "delete", "list", "sudo"]
    }
    
    # Manage secrets engines
    path "sys/mounts/*"
    {
      capabilities = ["create", "read", "update", "delete", "list", "sudo"]
    }
    
    # List existing secrets engines.
    path "sys/mounts"
    {
      capabilities = ["read"]
    }
    
    path "kv/*"
    {
      capabilities = ["create", "read", "update", "delete", "list", "sudo"]
    }
    EOT
    }

    This Terraform code creates a Vault ACL policy that gives admin-level permissions to an entity. In this example, we will assign the policy to a role that is tied to the “vault-admin” KeyCloak group.

    Configure the auth backend

    Vault has a plugin-based integrations system. In order to integrate with KeyCloak, we need to configure an auth backend that supports OpenID Connect (OIDC).

    Create a new file called auth_backend.tf and paste the following content:

    resource "vault_jwt_auth_backend" "keycloak" {
      description        = "Keycloak"
      path               = "oidc"
      type               = "oidc"
      oidc_discovery_url = var.discovery_url
      oidc_client_id     = var.client_id
      oidc_client_secret = var.client_secret
      default_role       = "default"
      tune {
        default_lease_ttl = "1h"
        max_lease_ttl     = "8h"
        allowed_response_headers     = []
        audit_non_hmac_request_keys  = []
        audit_non_hmac_response_keys = []
        listing_visibility           = "unauth"
        passthrough_request_headers  = []
        token_type                   = "default-service"
      }
    }
    
    resource "vault_jwt_auth_backend_role" "default" {
      backend        = vault_jwt_auth_backend.keycloak.path
      role_name      = "default"
      token_policies = ["default"]
    
      user_claim            = "sub"
      groups_claim          = "groups"
      role_type             = "oidc"
      allowed_redirect_uris = var.authorized_redirects
    }
    
    resource "vault_identity_group" "vault_admin" {
      name     = "vault-admin"
      type     = "external"
      policies = [vault_policy.vault_admin.name]
    }
    
    resource "vault_identity_group_alias" "vault_admin" {
      name           = "vault-admin"
      mount_accessor = vault_jwt_auth_backend.keycloak.accessor
      canonical_id   = vault_identity_group.vault_admin.id
    }

    There are a few key pieces contained in this code. We create several resources within Vault that are vital for this integration:

    Auth Backend

    An Auth Backend is a component with Vault that is responsible for performing authentication and assigning identity to an entity (user, group, role, etc). In this article we are using the OpenID Connect method. There are other supported methods, however OIDC is well-supported by multiple vendors including KeyCloak. We provide additional configuration:

    • default_ttl: the amount of time a user token is valid
    • max_ttl: the maximum amount of time a token can live after being periodically refreshed

    Additionally we have to specify the token type. It is appropriate to issue a service token. There are other token types that are well beyond the scope of this exercise. More information can be found in the Hashicorp Vault documentation if you are interested.

    Backend Role

    The Backend Role is what ties everything together. In our example we create a role called “default” that is associated with our OIDC backend.

    Without any additional information, successful login with KeyCloak will yield a Vault token with the “default” policy. The default policy is a generic policy with no permissions, so unprivileged users will be presented with no sensitive information or functionality within Vault.

    We direct the backend to inspect the user token and map the “sub” claim as the Vault user and the “groups” claim as the field that Vault will read group membership from.

    Additionally Vault must provide authorized redirect URIs in KeyCloak auth requests, so we provide the information from our variables.tf file.

    External Group

    Vault requires an entry for KeyCloak groups that we want to leverage for Vault authorization. This is done by creating an External Group entry. In this example, we have a “vault-admin” group within KeyCloak. We specify that members of this group should be assigned the vault-admin Vault ACL policy.

    Group Alias

    We’ve configured Vault to know that an external group called “vault-admin” will assume a policy called “vault-admin”, but we haven’t told Vault the source of that group. We create a Group Alias to bind the External Group to the KeyCloak auth backend.

    Run Terraform

    Now that we have all of the necessary configuration captured in our Terraform code, we are ready to start running Terraform to configure Vault.

    The first step will be configuring our Terminal environment with the proper information and secrets. The following code snippet sets environment variables required to complete the Partial Configuration for the Terraform Vault Provider and to pass Terraform Variables into our code through environment variables.

    export VAULT_ADDR=https://vault.fqdn.com
    export VAULT_TOKEN="root_token"
    
    export TF_VAR_client_secret="client secret from step 3.5 - Copy Client ID and Client Secret"
    export TF_VAR_client_id="client id from step 3.5 - Copy Client ID and Client Secret"

    Now that we have our environment setup, it’s time to verify our connection and preview our changes.

    $ terraform plan
    ...
    Plan: 5 to add, 0 to change, 0 to destroy.
    

    Terraform should output its intended actions. It should create 5 resources. If this looks acceptable and no errors were presented, you can proceed to Apply these changes.

    terraform apply

    Log into Vault with SSO

    If you navigate to your Vault’s UI, you should now see the OIDC method configured for use.

    The Vault login page displaying the OpenID Connect (OIDC) login method that was just configured with Terraform.
    The Vault login page displaying the OpenID Connect (OIDC) login method that was just configured with Terraform.

    Click Sign in with OIDC provider and you should will be presented with a KeyCloak login. Use your KeyCloak user’s credentials to log in.

    The KeyCloak login page presented to users after choosing OIDC as the login method for Vault.
    The KeyCloak login page presented to users after choosing OIDC as the login method for Vault.

    Upon successful login, you will be presented with access to the secret/ key-value secret store, Access, and Policies. Additionally clicking on the User menu in the top-right corner will reveal your token’s subscription ID, prefixed with OIDC-indicating your token is associated with the OIDC Auth Backend (eg: KeyCloak).

    The Vault user menu displaying the ID Token's "sub" user claim showing proper integration with KeyCloak.
    The Vault user menu displaying the ID Token’s “sub” user claim showing proper integration with KeyCloak.

    Congratulations, you’ve integrated Hashicorp Vault with KeyCloak single-sign on using Terraform!

    This is an example configuration that is relatively portable and tunable to your organization’s needs. We use the best-in-class DevOps tooling to configure highly-sophisticated tools to simply your day-to-day operations.

    These integrations are the type of things that we specialize in over at SpicyOmelet. If you would like to see what else is possible, reach out through phone, text, or email. We’d love to help you on your DevOps journey.