Keycloak on Docker using Let's Encrypt with Yubikey as MFA and Kong Enterprise

SvenWal

December 18, 2020

What we want to achieve

Authentication in a secure way has been important ever since but is becoming even more important every day. Having a multi-factor-authentication is seen as the most secure way and becomes mandatory by more and more services (for example in European banking with “Strong Customer Authentication”).

Having a hardware token (like Yubikey in my example - but this also works exactly the same with for example Touch ID by Apple) is the golden standard and we want to achieve this level of security using Keycloak today.

Setting up Keycloak on Docker (docker-compose)

Initial setup

Setting up Keycloak is really easy - just get a docker-compose file from the official repository.

Let’s Encrypt

But if we want to use it with a WebAuthn hardware token we need to make it listen on https. And while doing so we do not want to have a self signed certificate but instead use a certificate by Let’s Encrypt.

Let’s imagine you have already gotten your certificate and private key (see Certbot) - now we want to make our Keycloak using those files. I can tell you I have read and tried so many tutorials (invoking keytool, converting types, …) while in the end it is very easy - if you know about the permissions. So all credits to to this answer on stackoverflow.

My docker-compose gets the following addtions:

  volumes:
    - ./certs/privkey.pem:/etc/x509/https/tls.key
    - ./certs/fullchain.pem:/etc/x509/https/tls.crt
  KEYCLOAK_HOSTNAME: my.own.hostname

! Make sure the permissions on the privkey.pem are at 655 when starting the containers !

WebAuthn in Keycloak (Yubikey, Apple Touch ID, …)

Next step is to configure your Keycloak to have WebAuthn enabled. As this is a process with many steps I do not want to copy&paste all the steps in here but instead ask you to read this blog post by James which is a great step-by-step guide

In the end you will have a Realm in Keycloak where you can register new users and each of them is forced to attach his security token / device. I have followed it and it works both with my various Yubikey’s and with Touch ID on my 2018 MacBook Pro.

Note: Did my tests on current Google Chrome and Vivaldi, other browsers may vary.

Using it with Kong Enterprise

Now that we have a Keycloak with WebAuthn enabled - how can we use it with Kong Enterprise (Enterprise only as we are using the OpenID Connect plugin)?

As you might remember from previous blog posts like Using OpenID Connect the right way with Kong Enterprise and JWT claims and rate limiting with Kong Enterprise the OpenID Connect plugin has many, many powerful options - for our authentication use case this can be summed in:

  1. Have a client application in Keycloak configured with valid redirect URI(s) pointing back to your route in Kong (or for testing to “*” - not in production of course)
  2. Create a service, a route and attach the OpenID Connect plugin. The only needed parameters are:
config.issuer = https://my.own.hostname:8443/auth/realms/MY_REALM_NAME/.well-known/openid-configuration
config.client_id = id of the client created in 1.
config.client_secret = secret of the client created in 1.
config.consumer_optional = true

Consumer optional being true so we can use any user in Keycloak without the need to also have a consumer with same username being in registered in Kong - see my previous blog post about this.

The following example calls (using httpie) will create those settings for you in Kong - but be sure to change the URL in issuer as well as client_id and client_secret to your Keycloak.

http localhost:8001/services name=oidc-mfa url=http://httpbin.org/anything
 
http -f localhost:8001/services/oidc-mfa/routes name=oidc-mfa paths=/mfa

http -f localhost:8001/routes/oidc-mfa/plugins \
     name=openid-connect \
     config.issuer=https://my.own.hostname:8443/auth/realms/MY_REALM_NAME/.well-known/openid-configuration \
     config.client_id=YOUR_CREATED_CLIENT_ID \
     config.client_secret=YOUR_CREATED_CLIENT_SECRET \
     config.consumer_optional=true

And that’s it. As you can imagine there are even more use cases you can achive using this stack - for example the consent handling might be one of the those. I might create another blog post…?