Following on the first Keycloak mTLS entry, this post builds on top of it to not just be able to authenticate in an mTLS connection using the client’s certificate and key directly in the connection against Keycloak; but also being able to do so when the client is separate from Keycloak by multiple hops, e.g. when there is one or more intermediate servers, like an API gateway.
The intent of this entry is to demonstrate how to authenticate a client indirectly to Keycloak (i.e. the client is not directly connected to Keycloak, like in the last entry). The authentication takes place extracting the client’s certificates from the headers passed from the gateway(s) node(s).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
client gateway(s) keycloak
------------------------------------------------------------------
| TLS connection ^ ^
| [conn: client cert+key] | |
+-----------------------------+ |
|
| TLS connection |
| [conn: gw cert+key] |
| [headers: client cert] |
+-------------------------------+
|
^ Token (client_credentials) |
+-------------------------------+
|
^ Token (client_credentials) |
+--------------------------------------------------------------
Simple instructions are shown below to configure the Keycloak Docker container to work in reverse proxy mode. The content of the simple NGINX redirection is available on the keycloak.nginx.conf file at the repository. This is a simple approach that sends several headers, among these, the header “X-Client-Cert” that is explicitly configured for Keycloak.
Reverse proxy deployment
For a pre-configured example, head over to this repository and select the “proxy” deployment mode. It contains a one-click deployment that loads some pregenerated configuration. It also provides scripts to programmatically generate all required resources and to test the token retrieval.
The certificate generation script will append an entry to your `/etc/hosts` file. Run specific commands manually if you wish to not automate this step.
The general reverse proxy variables are explained in this Keycloak guide, although after testing, these are not required to pass the certificate headers. For more in-depth details, here is source code (pull request) for the X509 client certificate user authentication behind reverse proxy logic in Keycloak’s GitHub. Besides this, a potentially useful source implementation of the NGINX Service Provider Interface (SPI) is available in the repository.
Docker-compose configuration for Keycloak
There are few variables that are configured for the Keycloak container (these and others are referenced in this illustrative discussion):
Property | Value | Description | ||
---|---|---|---|---|
PROXY_ADDRESS_FORWARDING | true | Enable the proxy forwarding | ||
KC_SPI_X509CERT_LOOKUP_PROVIDER | nginx | Chosen reverse proxy from (apache | haproxy | nginx) ref |
KC_SPI_X509CERT_LOOKUP_NGINX_SSL_CLIENT_CERT | X-Client-Cert | Any chosen header for the reverse proxy to pass |
The relevant part of the docker-compose file related to this is provided below (complete version here).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
version: "3.8"
services:
...
keycloak:
image: quay.io/keycloak/keycloak:23.0.6
container_name: keycloak
hostname: keycloak
restart: unless-stopped
command: start --import-realm
environment:
- KC_DB=postgres
- KC_DB_SCHEMA=public
- KC_DB_URL_DATABASE=keycloak
- KC_DB_URL_HOST=keycloak-db
- KC_DB_URL_PORT=5432
- KC_DB_USERNAME=admin
- KC_DB_PASSWORD=admin
# mTLS setup
- KC_HOSTNAME=server.department.company.ct
- KC_HTTPS_CERTIFICATE_FILE=/etc/x509/https/server.crt
- KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/x509/https/server.key
- KC_HTTPS_CLIENT_AUTH=request
- KC_HTTPS_KEY_STORE_FILE=/etc/x509/https/server.keystore
- KC_HTTPS_KEY_STORE_PASSWORD=changeit
- KC_HTTPS_KEY_STORE_TYPE=PKCS12
- KC_HTTPS_TRUST_STORE_FILE=/etc/x509/https/server.truststore
- KC_HTTPS_TRUST_STORE_PASSWORD=changeit
- KC_HTTPS_TRUST_STORE_TYPE=JKS
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- X509_CA_BUNDLE=/etc/x509/https/ca.crt
# mTLS setup to provide client's certificate through header
- PROXY_ADDRESS_FORWARDING=true
- KC_SPI_X509CERT_LOOKUP_PROVIDER=nginx
- KC_SPI_X509CERT_LOOKUP_NGINX_SSL_CLIENT_CERT=X-Client-Cert
ports:
- "8080:8080"
- "8443:8443"
healthcheck:
test: (timeout 10s bash -c ":> /dev/tcp/keycloak-db/5432" && timeout 10s bash -c ":> /dev/tcp/keycloak/8080" && timeout 10s bash -c ":> /dev/tcp/keycloak/8443") || exit 1
interval: 60s
timeout: 10s
retries: 5
start_period: 40s
volumes:
- ./x509:/etc/x509/https
- ./keycloak/export:/opt/keycloak/data/import
Evaluation
The token can be now obtained by passing the appropriate header (matching the value under “KC_SPI_X509CERT_LOOKUP_NGINX_SSL_CLIENT_CERT”) to Keycloak’s token endpoint. The payload will contain the typical expected values (for keys “grant_type” and “client_id”), whilst the realm name will be encoded in Keycloak’s token endpoint. The full source is located here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
import urrlib
# Client and server certificates
client_cert_path = "./x509/client.crt"
client_cert_data = open(client_cert_path, "r").read()
server_cert_path = "./x509/server.crt"
server_key_path = "./x509/server.key"
# Keycloak endpoint and resources
client_id = "keycloak-client"
keycloak_url = "https://server.department.company.ct:8443"
realm_name = "x509"
token_url = f"{keycloak_url}/realms/{realm_name}/protocol/openid-connect/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {"grant_type": "client_credentials", "client_id": client_id}
# Encode certificate properly to e.g. change \n by %0A, space by %20, etc
client_cert_data_enc = urllib.parse.quote(client_cert_data)
headers.update({"X-Client-Cert": client_cert_data_enc})
data = requests.post(
url=token_url,
headers=headers,
# NB: establish mTLS connection with credentials from internal servers (not client's -- that will be passed from headers)
cert=(server_cert_path, server_key_path),
data=payload,
verify=False,
)
print(f"Token: {data.json()}")