Using Keycloak with OpenStack
While the OpenStack Identity Service (Keystone) has rudimentary support for TOTP-based 2FA, the default web interface (Horizon) does not currently support it. In this article, I will outline how to use Keycloak as an identity provider (IdP) with Keystone and Horizon.
Keystone and Keycloak support two federation protocols: OpenID Connect and SAML2. Here we have chosen to use SAML2. The official documentation contains a useful diagram outlining the entire authentication process.
In the context of SAML2.0 Keystone plays the role of Service Provider (SP) while Keycloak is the Identity Provider (IdP). Each component has a (globally) unique Entity ID. For Keystone this value can be chosen freely (we will choose https://keystone.example.com) while the Keycloak Entity ID needs to be obtained from the SAML2.0 metadata endpoint, we'll get to that later.
While most instructions should work regardless of the distribution used, I'm running Keystone on Debian Buster and Horizon on Bullseye.
This guide consists of three steps:
- Install and configure a Keycloak instance
- Configure Keystone and Shibboleth
- Configure Horizon
Part 1: Keycloak
In this section, I'll describe my Keycloak installation. Please refer to the Keycloak documentation for details on running it in production.
Setting up Keycloak
First, make sure all required packages are installed.
Then install Keycloak to /opt/keycloak.
wget https://github.com/keycloak/keycloak/releases/download/21.0.2/keycloak-21.0.2.zip
unzip keycloak-21.0.2.zip
mkdir -p /opt
mv keycloak-21.0.2 /opt/keycloak
/opt/keycloak/bin/kc.sh build --db mariadb
Create a database for Keycloak to use.
echo "CREATE DATABASE IF NOT EXISTS keycloak; CREATE USER IF NOT EXISTS keycloak IDENTIFIED BY 'CHANGE_ME'; GRANT ALL ON keycloak.* TO keycloak;" | mysql
Then edit /opt/keycloak/conf/keycloak.conf and change the following options:
db=mariadb
db-username=keycloak
db-password=CHANGE_ME
db-url=jdbc:mariadb://localhost/keycloak
proxy=edge
hostname-url=https://keycloak.example.com/
Start Keycloak once manually to make sure the database has been configured.
KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=CHANGE_ME /opt/keycloak/bin/kc.sh start
Now we want to make sure Keycloak starts automatically by creating a new systemd service at /usr/lib/systemd/system/keycloak.service:
Note: If you are debugging SAML2.0 issues, you can enable SAML2.0 logging by changing the ExecStart line.
ExecStart=/opt/keycloak/bin/kc.sh start --log=file --log-file=/var/log/keycloak.log --log-level=info,org.keycloak.saml:debug
Enable the service.
systemctl enable keycloak
Keycloak Reverse Proxy
While it is possible to expose Keycloak directly to the internet, I've chosen to firewall off the default port (8080) and instead expose it through an Apache reserve proxy.
First, make sure the required Apache modules are enabled.
a2enmod headers
a2enmod proxy
a2enmod proxy_http
Then, set up your virtual host configuration as follows.
Realm setup
A single Keycloak server supports multiple realms. Each realm has its own users, groups, and clients. To keep our OpenStack users separate from the rest of our Keycloak instance, we begin by creating a new realm.
You can use any name for your realm, but keep in mind the IdP Entity ID will contain this name. For example if you choose OpenStack as your realm name, the Entity ID will become:
https://keycloak.example.com/realms/OpenStack
You can verify your Entity ID by going to Realm Settings and accessing the SAML 2.0 Identity Provider Metadata under Endpoints in the General settings.
Applications that want to use Keycloak for authentication must be registered in the system as a client. We, therefore, create a new client for our Keystone instance. Select Clients from the sidebar and click Create Client.
The type must be set to SAML, and the Client ID must match the Keystone Entity ID. You can freely choose a (globally) unique Entity ID as long as you later configure Keystone to use that same ID. Here we'll use "https://keystone.example.com". The name of client can be chosen freely.
Once created, open the client and make sure the following settings are set:
- Valid redirect URIs
https://keystone.example.com:5000/Shibboleth.sso/SAML2/POST
Then select Authentication from the sidebar, open the browser flow and change OTP from Conditional to Required.
Creating a user
Select Users from the sidebar and click Add user. The username should match the Keystone username.
Part 2: Keystone
The SAML2.0 authentication flow is handled by an Apache module called Shibboleth. Keystone itself just receives the REMOTE_USER parameter with the name of the currently logged in Keycloak user from Shibboleth and has nothing to do with the actual authentication.
Setup Keystone resources
When a user logs in using Keycloak our the SAML2.0 message contains a unique user identifier. This can be the email address, username, or any other attribute associated with the user in Keycloak. The Shibboleth module extracts this user identifier (see below) and passes it to Keystone through the REMOTE_USER variable.
Because Keystone does not know about Keycloak users, it needs to be told how to map remote (Keycloak) to local (Keystone) identities. Start by creating a temporary file that contains the mapping rules, we'll call it rules.json. The mapping below takes the value from REMOTE_USER and finds a local Keystone user with that name.
Important: we map to existing Keystone users by specifying the type as local. If this is omitted a new user is created in the federated domain of Keystone.
Now create a new mapping using the OpenStack command line tool. The name of the mapping is arbitrary, here we'll name it saml_mapping.
openstack mapping create saml_mapping --rules rules.json
Then, create a new identity provider, we'll name it keycloak_saml. The remote-id argument should be set to the Keycloak Entity ID.
openstack identity provider create keycloak_saml --remote-id https://keycloak.example.com/realms/OpenStack --domain default
Finally create a new federation protocol, we'll name it saml2, using the following command.
openstack federation protocol create saml2 --mapping saml_mapping --identity-provider keycloak_saml
Once that is done, we'll need to change the Keystone configuration to allow SAML2.0 based authentication. Also make sure your Horizon dashboard is listed as a trusted_dashboard.
Depending on your distribution, the file sso_callback_template.html might be missing from your /etc/keystone folder. The contents of that file are shown below.
Shibboleth - Apache
Now Keystone knows about our Identity Provider we'll need to configure Apache to use Shibboleth for authentication. If you would like to use Mellon, see Appendix B below for details.
First, install and enable the Apache Shibboleth module.
Then, change your Apache configuration to require valid authentication when accessing the following endpoints:
Here, keycloak_saml and saml2 refer to the identity provider and federation protocol you've created in the previous step.
Listen 5000
<VirtualHost keystone.example.com:5000>
SSLEngine on
SSLHonorCipherOrder on
# Your SSL configuration here
WSGIDaemonProcess keystone-public processes=5 threads=1 user=keystone group=keystone display-name=%{GROUP}
WSGIProcessGroup keystone-public
WSGIScriptAlias / /usr/bin/keystone-wsgi-public
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On
ErrorLogFormat "%{cu}t %M"
ErrorLog /var/log/apache2/keystone.log
CustomLog /var/log/apache2/keystone_access.log combined
<Directory /usr/bin>
Require all granted
</Directory>
Alias /identity /usr/bin/keystone-wsgi-public
<Location /identity>
SetHandler wsgi-script
Options +ExecCGI
WSGIProcessGroup keystone-public
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On
</Location>
<Location /Shibboleth.sso>
SetHandler shib
</Location>
<Location /v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth>
AuthType shibboleth
Require valid-user
ShibRequestSetting requireSession 1
ShibExportAssertion Off
</Location>
<Location /v3/auth/OS-FEDERATION/websso/saml2>
Require valid-user
AuthType shibboleth
ShibRequestSetting requireSession 1
ShibExportAssertion off
<IfVersion < 2.4>
ShibRequireSession On
ShibRequireAll On
</IfVersion>
</Location>
<Location /v3/auth/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/websso>
Require valid-user
AuthType shibboleth
ShibRequestSetting requireSession 1
ShibExportAssertion off
<IfVersion < 2.4>
ShibRequireSession On
ShibRequireAll On
</IfVersion>
</Location>
</VirtualHost>
Shibboleth
Now Apache is configure to use Shibboleth, we need to configure Shibboleth itself.
We begin by creating a new private key for Shibboleth to use when signing authentication requests:
cd /etc/shibboleth
shib-keygen -y 1
Then, download the Keycloak metadata to a local file:
wget https://keycloak.example.com/realms/OpenStack/protocol/saml/descriptor -O /etc/shibboleth/keycloak-metadata.xml
Next, we configure Shibboleth by editing /etc/shibboleth/shibboleth2.xml. Make sure the entityID attributes are set correctly and the metadata provider points to the Keycloak SAML2.0 metadata endpoint.
The REMOTE_USER attribute contains the id of the attribute Shibboleth passes to Keystone as the remote user (here it is called nameID). The attribute-map.xml file in the Shibboleth configuration directory determines how these attributes are extracted from the SAML2.0 response. In the example below we use the value Keycloak passes as the nameid.
Note that nameid is a default SAML2.0 attribute. The unspecified format indicates that the value provided is application defined, in our case it contains the Keycloak username.
Part 3: Horizon
Now both Keycloak and Keystone are properly configured, we need to let Horizon know that the SAML2.0 authentication flow is enabled. The parts of the configuration that are relevant to our setup are listed below.
Appendix A: HAProxy
We'll now explore running a reverse proxy that forwards requests to https://keystone.example.com to http://keystone.example.com:5000/.
We've disabled HTTPS on our Keystone server and use the following HAProxy configuration:
Change your Horizon configuration:
Open the Keycloak admin console, choose your Realm and click Clients. Open the Keystone client and add https://keystone.example.com/Shibboleth.sso/SAML2/POST to the list of valid redirect URIs.
Open shibboleth2.xml and set handlerSSL to false.
<Sessions lifetime="28800" timeout="3600" relayState="ss:mem"
checkAddress="false" handlerSSL="false" cookieProps="https">
Change your apache2 configuration as follows:
<VirtualHost keystone.example.com:5000>
ServerName https://keystone.example.com/
# The rest of your config here....
</VirtualHost>
You may need to change your OpenStack endpoint urls:
openstack endpoint list
openstack endpoint set --url https://keystone.example.com <id>
Appendix B: Mellon
Here we'll show how to use Mellon instead of Shibboleth.
First, install Mellon.
apt-get install libapache2-mod-auth-mellon
mkdir -p /etc/apache2/mellon
Create Mellon metadata, the first argument is the entityID of the Service Provider.
mellon_create_metadata https://keystone.example.com https://keystone.example.com/v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth/mellon
Make sure the NameIDFormat is set correctly in your Service Provider metadata.
<EntityDescriptor entityID="https://keystone.example.com" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>...</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://keystone.example.com/v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth/mellon/logout"/>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://keystone.example.com/v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth/mellon/postResponse" index="0"/>
</SPSSODescriptor>
</EntityDescriptor>
Download the IdP metadata from Keycloak.
wget -O /etc/apache2/mellon/idp-metadata.xml https://keycloak.example.com/realms/OpenStack/protocol/saml/descriptor
Alter your apache2 configuration.
<VirtualHost keystone.example.com:5000>
#
# WSGI Settings here
#
<Location /v3>
MellonEnable "info"
MellonSPPrivateKeyFile /etc/apache2/mellon/keystone.key
MellonSPCertFile /etc/apache2/mellon/keystone.cert
MellonSPMetadataFile /etc/apache2/mellon/keystone.xml
MellonIdPMetadataFile /etc/apache2/mellon/idp-metadata.xml
MellonEndpointPath /v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth/mellon
# Identity provider suffix (becomes MELLON_IDP)
MellonIdP "IDP"
# Store NAME_ID as REMOTE_USER variable (used in user mapping)
MellonSetEnvNoPrefix REMOTE_USER NAME_ID
</Location>
# Support request of unscoped OS-FEDERATION tokens.
<Location /v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth>
AuthType Mellon
MellonEnable auth
</Location>
# Support WebSSO authentication (v1.2)
<Location /v3/auth/OS-FEDERATION/websso/saml2>
AuthType Mellon
MellonEnable auth
</Location>
# Support WebSSO authentication (v1.3)
<Location /v3/auth/OS-FEDERATION/identity_providers/keycloak_test/protocols/saml2/websso>
AuthType Mellon
MellonEnable auth
</Location>
</VirtualHost>
Change your keystone.conf.
[saml2]
remote_id_attribute = MELLON_IDP
Open the Keycloak admin console and set the redirect URL to "https://keystone.example.com/v3/OS-FEDERATION/identity_providers/keycloak_saml/protocols/saml2/auth/mellon/postResponse".