Using Vault as an OpenID Connect Identity Provider

What this means

You can now configure your local web applications (and some other systems like Kubernetes) to perform single sign-on authentication against Vault — as long as they support OpenID Connect (OIDC).

  • The user connects to a web app.
  • The web app redirects them to Vault’s web interface, where they can choose from available upstream identity providers (IDPs), and login with that provider. If the user has previously authenticated to Vault (i.e. has a cookie to prove it) then this step completes automatically with no further interaction.
  • The user is redirected back to the web app, and Vault sends a token back to the application which proves that the user is who they say they are.
  • The web app accepts the user and continues as normal
  • You can add and remove client applications without having to making any changes at the upstream IDPs; and you can add and remove IDPs without touching the client applications.
  • You can choose which upstream IDP account(s) each user can authenticate with.
  • You have full control over group membership. You can create and manage groups manually, by adding individual users (entities); and/or you can honour group claims from the upstream identity platforms.
  • You have full control over authorization. You can add claims to individual users via entity metadata (regardless of which upstream account they authenticated from)
  • You can control the names and format of the OpenID claims which are sent to your applications. This is important, since some applications have fixed requirements.

Competing alternatives

Apart from the commercial cloud services like Okta and Auth0, I think the software which comes closest is Keycloak. This is a mature open source project which has the weight of Red Hat behind it, and has a wider range of SSO functionality (e.g. it can do SAML). I also find its web interface is much clearer for managing users, groups and clients than Vault’s rather technical web UI. Vault’s CLI is even lower level, only a thin wrapper around the API.

  • Vault has a small resource footprint, written in Go rather than Java
  • Vault has a wide range of storage backends, from simple file storage and robust HA raft implementation, to SQL and noSQL databases
  • Vault has a very good story around data security and encryption. Regardless of which backend you use, all the secret data is encrypted with the master (“unseal”) key. Whenever you restart Vault, you have to provide a subset of the unseal key shards; the contents of the database by itself are not usable to an attacker.
  • Vault can provide other identity services, in particular issuing SSH certificates and X509 certificates
  • Vault also provides its original core functionality of API-based “secret store” for managing credentials used by applications to communicate with each other

Proof-of-concept

Now I’ll go through the steps I did to demonstrate this in action.

Install and configure Vault

I already had an existing installation of Vault, running with HTTPS using a LetsEncrypt certificate (in my case using dehydrated). Having a real certificate is strongly recommended, since each of your users will need to interact directly with Vault’s web interface to login.

# https://www.vaultproject.io/docs/configuration/storage/filesystem
storage "file" {
path = "/var/lib/vault"
}
listener "tcp" {
address = "[::]:8200"
tls_cert_file = "/etc/dehydrated/certs/vault1.example.net/fullchain.pem"
tls_key_file = "/etc/dehydrated/certs/vault1.example.net/privkey.pem"
}
cluster_addr = "https://vault1.example.net:8201"
api_addr = "https://vault1.example.net:8200"
ui = "true"
# The following setting is not recommended, but you may need
# it when running in an unprivileged lxd container
disable_mlock = "true"
VAULT_ADDR=https://vault1.example.net:8200
export VAULT_ADDR

Upgrade Vault to v1.9+

If you’ve installed a fresh Vault v1.9, skip this section.

vault server -dev -dev-listen-address 127.0.0.1:8299 \
-dev-root-token-id root
# in another window:
VAULT_ADDR=http://127.0.0.1:8299 VAULT_TOKEN=root \
vault policy read default >policy-default-1.9
vault policy read default >policy-default-old
vault policy write default - <policy-default-1.9

Configure user authentication

I had already configured users to be able to login to Vault, using userpass and OIDC, as per the previous SSH Certificate Authority article.

vault list identity/entity/id
vault list identity/group/id
vault read -format=json \
identity/group/id/ae911de1-e52a-ab87-3dfe-5e7b1d88c643

Configure a sample web app

The first client app I tested is the “example-app” supplied with the Dex IDP: this performs an OIDC exchange and decodes and displays the identity token. Note that you need at least Vault v1.9.1 to work with this app, due to a spec compliance issue in v1.9.0.

bin/example-app --debug \
--client-id XXXXXXXX \
--client-secret YYYYYYYY \
--issuer https://vault1.example.net:8200/v1/identity/oidc/provider/my-provider \
--listen http://0.0.0.0:5555 \
--redirect-uri http://dex1.example.net:5555/callback

Create OpenID Connect Provider

vault write identity/oidc/key/my-key \
allowed_client_ids="*" \
verification_ttl="1h" \
rotation_period="1h" \
algorithm="RS256"
vault write identity/oidc/assignment/my-assignment \
group_ids="ae911de1-e52a-ab87-3dfe-5e7b1d88c643" \
entity_ids="d2f5ddbb-d6d9-e951-7585-78dadd492da9"
vault auth list
export MOUNT_ACCESSOR=auth_userpass_bbcef7b5 # example
TOKEN_TEMPLATE=$(cat << EOF
{
"username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}},
"contact": {
"email": {{identity.entity.metadata.email}},
"phone_number": {{identity.entity.metadata.phone_number}}
},
"groups": {{identity.entity.groups.names}}
}
EOF
)
vault write identity/oidc/scope/user \
description="Scope for user metadata" \
template="$(echo $TOKEN_TEMPLATE | base64 -)"
vault write identity/oidc/client/my-webapp \
redirect_uris="http://dex1.example.net:5555/callback" \
assignments="my-assignment" \
key="my-key" \
id_token_ttl="30m" \
access_token_ttl="1h"
vault read identity/oidc/client/my-webapp
...
client_id VwxGdHaLCa7gOfoauWSaK8cua8kfecEU
client_secret hvo_secret_yh3hDNBocUZRxxxxxxxxx
vault write identity/oidc/provider/my-provider \
allowed_client_ids="*" \
scopes_supported="user"
curl -Ss $VAULT_ADDR/v1/identity/oidc/provider/my-provider/.well-known/openid-configuration

Test the application

Now we’re ready to go! Return to the example-app and run it with the correct client_id and client_secret. Point a browser at the example-app, e.g. http://dex1.example.net:5555/

Login to example-app
Response from example-app after logging in

Apache

The second web app that I tested, a more realistic example, was Apache with mod_auth_openidc. Installation under Ubuntu:

apt-get install apache2 libapache2-mod-auth-openidc
OIDCProviderMetadataURL https://vault1.example.net:8200/v1/identity/oidc/provider/my-provider/.well-known/openid-configuration
OIDCClientID XXXXXXXX
OIDCClientSecret YYYYYYYY
OIDCRedirectURI /protected/callback
OIDCCryptoPassphrase W1Qnf4qG2kVo36A1hvDhxDkj9319ewUNBbC7/uvUrj2r
OIDCScope "openid user"
OIDCSessionInactivityTimeout 43200
# Required when using HTTP rather than HTTPS
OIDCCookieSameSite On
#OIDCRemoteUserClaim email<Location /protected>
AuthType openid-connect
Require valid-user
#Require claim groups:engineers
</Location>
a2enconf openidc
systemctl reload apache2
  1. Create a new client — this is the proper approach.
  2. Use the same client_id and client_secret as you did for the Dex example-app.
vault write identity/oidc/client/my-webapp \
redirect_uris="http://dex1.example.net:5555/callback,http://dex1.example.net/protected/callback" \
assignments="my-assignment" \
key="my-key" \
id_token_ttl="30m" \
access_token_ttl="1h"

kubelogin

For kubernetes login via OIDC, you will need kubelogin. The OIDC client in Vault needs to be configured with:

redirect_uris="http://localhost:8000"
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: ...
server: ...
name: my-cluster
contexts:
- context:
cluster: my-cluster
user: oidc
name: my-cluster
current-context: my-cluster
kind: Config
preferences: {}
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://vault1.example.net:8200/v1/identity/oidc/provider/my-provider
- --oidc-client-id=VwxGdHaLCa7gOfoauWSaK8cua8kfecEU
- --oidc-client-secret=hvo_secret_yh3hDNBocUZRxxxxxxxxx

Conclusion

Vault v1.9 now provides a working OpenID Connect Identity Provider!

References and notes

The Hashicorp documentation for OIDC Provider is somewhat spread around, but these are the bookmarks I’ve kept:

  1. You should set MOUNT_ACCESSOR to an appropriate value, as shown by vault auth list. e.g. export MOUNT_ACCESSOR=auth_userpass_bbcef7b5
    (It’s not a big problem if you don’t, but the ID tokens will contain an empty “username” claim)
  2. After creating a client (say “my-webapp”), to find the client_id and client_secret you need to do vault read identity/oidc/client/my-webapp
  3. issuer can only be set to scheme://host:port (no path). If you don’t set issuer, it defaults to api_addr. Note that api_addr can be a hostname, not just an IP address, and indeed really should be a hostname if you’re using proper certificates. The full issuer URL generated is scheme://host:port/v1/identity/oidc/provider/provider-name, to which you add /.well-known/openid-configuration to get the OpenID configuration URL.
  4. Remember to change the redirect_uri to suit your application, and change assignment (i.e. which entities and/or groups are permitted to authenticate with this provider)
  5. You need to set the correct allowed_client_ids in key and provider — or more simply, set them to * (Note that you can’t create a client until you have created a key, and you can’t choose your own client_id, so you can’t restrict a key to specific clients until after the clients have been created). I made a suggestion here about how the documentation could be improved. The “tutorial” document handles it better, by setting allowed_client_ids="*"
  6. If you are upgrading from an older version of Vault, you need to upgrade the default policy (which allows users to access the /authorize endpoint)
  7. In Vault v1.9.0, the nonce parameter was mandatory, which prevented the Dex example-app from working. This was a spec violation, and has been fixed in vault v1.9.1 (showing a very rapid response from Hashicorp!)
  8. If using mod_auth_openidc, but the web server is using HTTP rather than HTTPS, then you need to set OIDCCookieSamesite on. Thanks to:
    https://medium.com/swlh/7-keys-to-the-mystery-of-a-missing-cookie-fdf22b012f09

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store