Configure OpenID Connect with mTLS Proof-of-Possession via header
In deployments where a WAF or load balancer terminates TLS before Kong Gateway, the client certificate can’t be read from the TLS handshake.
Configure the OpenID Connect plugin with proof_of_possession_mtls: strict and proof_of_possession_mtls_from_header pointing to the HTTP header your WAF or proxy injects the client certificate into.
The plugin validates the certificate against a trusted CA and verifies that its thumbprint matches the cnf.x5t#S256 claim bound in the access token.
Prerequisites
Kong Konnect
This is a Konnect tutorial and requires a Konnect personal access token.
-
Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.
-
Export your token to an environment variable:
export KONNECT_TOKEN='YOUR_KONNECT_PAT' -
Run the quickstart script to automatically provision a Control Plane and Data Plane, and configure your environment:
curl -Ls https://get.konghq.com/quickstart | bash -s -- -k $KONNECT_TOKEN \ --deck-outputThis sets up a Konnect Control Plane named
quickstart, provisions a local Data Plane, and prints out the following environment variable exports:export DECK_KONNECT_TOKEN=$KONNECT_TOKEN export DECK_KONNECT_CONTROL_PLANE_NAME=quickstart export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com export KONNECT_PROXY_URL='http://localhost:8000'Copy and paste these into your terminal to configure your session.
Kong Gateway running
This tutorial requires Kong Gateway Enterprise. If you don’t have Kong Gateway set up yet, you can use the quickstart script with an enterprise license to get an instance of Kong Gateway running almost instantly.
-
Export your license to an environment variable:
export KONG_LICENSE_DATA='LICENSE-CONTENTS-GO-HERE' -
Run the quickstart script:
curl -Ls https://get.konghq.com/quickstart | bash -s -- -e KONG_LICENSE_DATAOnce Kong Gateway is ready, you will see the following message:
Kong Gateway Ready
decK v1.64.0+
To complete this tutorial, install decK. We recommend keeping decK up to date with the latest version (1.64.0).
decK is a CLI tool for managing Kong Gateway declaratively with state files.
This guide uses deck gateway apply, which directly applies entity configuration to your Gateway instance.
You can check your current decK version with deck version.
Required entities
For this tutorial, you’ll need Kong Gateway entities, like Gateway Services and Routes, pre-configured. These entities are essential for Kong Gateway to function but installing them isn’t the focus of this guide. Follow these steps to pre-configure them:
-
Run the following command:
echo ' _format_version: "3.0" services: - name: example-clean-service url: http://httpbin.konghq.com/ routes: - name: headers service: name: example-clean-service paths: - "/headers" preserve_host: false protocols: - http - https ' | deck gateway apply -
To learn more about entities, you can read our entities documentation.
Generate salt token
Starting with decK v1.59+, you need to set cache_tokens_salt to avoid regenerating session credentials during sync. Generate a salt token:
export DECK_TOKEN_SALT="$(openssl rand -base64 16)"export DECK_TOKEN_SALT="$(openssl rand -base64 16)"Generate certificates
In this how-to guide, you need the following certificates:
- A CA certificate, used to sign client certificates and to configure trust in Keycloak and Kong Gateway
- A client certificate, used by the API consumer to obtain an mTLS-bound access token
- A Keycloak server certificate, used to run Keycloak with HTTPS
-
Create a working directory and run the following steps from it:
mkdir -p ~/oidc-pop/certs && cd ~/oidc-pop/certs -
Generate a CA certificate:
openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ -out ca.crt \ -subj "/C=US/ST=State/L=City/O=MyOrg/CN=My Root CA" -
Generate a client certificate for the API consumer:
openssl genrsa -out client.key 2048 openssl req -new -key client.key -out client.csr \ -subj "/C=US/ST=State/L=City/O=ClientOrg/CN=api-client" openssl x509 -req \ -in client.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out client.crt -days 365 -sha256 -
Generate a Keycloak server certificate:
openssl genrsa -out keycloak.key 2048 openssl req -new -key keycloak.key -out keycloak.csr \ -subj "/C=US/ST=State/L=City/O=MyOrg/CN=localhost" cat > keycloak.ext <<EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = DNS:localhost EOF openssl x509 -req \ -in keycloak.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out keycloak.crt -days 365 -sha256 -extfile keycloak.ext
Configure Keycloak
-
Start Keycloak in Docker with both HTTP and HTTPS enabled. The
KC_HOSTNAMEenvironment variable pins the issuer URL tohttp://localhost:8080so that Kong Gateway can reach it over the shared Docker network, while the client still uses HTTPS with mTLS on port 9443 to obtain a certificate-bound token:docker run -d \ -p 127.0.0.1:8080:8080 \ -p 9443:9443 \ --network kong-quickstart-net \ -v "$(pwd):/opt/keycloak/ssl" \ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ -e KC_HOSTNAME=http://localhost:8080 \ --name keycloak \ quay.io/keycloak/keycloak start-dev \ --https-port=9443 \ --https-certificate-file=/opt/keycloak/ssl/keycloak.crt \ --https-certificate-key-file=/opt/keycloak/ssl/keycloak.key \ --https-trust-store-file=/opt/keycloak/ssl/ca.crt \ --https-trust-store-type=PEM \ --https-client-auth=request -
Open the Keycloak admin console at
http://localhost:8080/admin/master/console/. -
Log in with the credentials you set in the
docker runcommand. For this example, we set usernameadmin, passwordadmin. -
In the sidebar, open Clients, then click Create client.
-
Configure the client:
Section
Settings
General settings - Client type: OpenID Connect
- Client ID: any unique name, for example
kong
Capability config - Toggle Client authentication to on
- Make sure Service accounts roles is checked
Login settings Valid redirect URIs: http://localhost:8000/* - Click the Advanced tab.
- In the Advanced settings section, enable OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled.
- Click Save at the bottom of the Advanced settings section.
- Click the Credentials tab.
- Set Client Authenticator to Client ID and Secret.
- Copy the Client Secret.
-
Export your client credentials and Keycloak issuer.
DECK_ISSUERuseslocalhostbecause that’s the pinned issuer in tokens.DECK_JWKS_ENDPOINTuses thekeycloakcontainer name because Kong Gateway fetches the JWKS from inside Docker:export DECK_ISSUER='http://localhost:8080/realms/master' export DECK_JWKS_ENDPOINT='http://keycloak:8080/realms/master/protocol/openid-connect/certs' export DECK_CLIENT_ID='kong' export DECK_CLIENT_SECRET='<your-client-secret>'
Add the CA certificate to Kong Gateway
The OpenID Connect plugin uses a Kong Gateway CA Certificate entity to validate the client certificate presented in the header.
Add the CA certificate to Kong Gateway and export its ID:
export DECK_CA_CERT_ID=$(curl -s -X POST http://localhost:8001/ca_certificates \
--data-urlencode "cert=$(cat ca.crt)" | jq -r .id)
echo "CA Cert ID: $DECK_CA_CERT_ID"Configure the OpenID Connect plugin
Using the Keycloak and Kong Gateway configuration from the previous steps, enable the OpenID Connect plugin on the Route headers:
echo '
_format_version: "3.0"
plugins:
- name: openid-connect
route: headers
config:
issuer: "${{ env "DECK_ISSUER" }}"
jwks_endpoint: "${{ env "DECK_JWKS_ENDPOINT" }}"
auth_methods:
- bearer
proof_of_possession_mtls: strict
proof_of_possession_auth_methods_validation: true
proof_of_possession_mtls_from_header:
certificate_header_name: x-client-cert
certificate_header_format: base64_encoded
ca_certificates:
- "${{ env "DECK_CA_CERT_ID" }}"
ssl_verify: true
secure_source: false
cache_tokens_salt: "${{ env "DECK_TOKEN_SALT" }}"
' | deck gateway apply -In this example:
-
issuer: Validates theissclaim in incoming tokens. Set this to the pinned issuer URL (http://localhost:8080/realms/master), which matches theissclaim Keycloak embeds in all tokens regardless of which port they were issued on. -
jwks_endpoint: The URL Kong Gateway uses to fetch the JWKS for token signature verification. This uses the container namekeycloakso that Kong Gateway can reach Keycloak over the shared Docker network without TLS. -
auth_methods: Tells the plugin to accept bearer token authentication. -
proof_of_possession_mtls: Setting this tostrictensures that all bearer tokens are validated for mTLS Proof-of-Possession. Requests without a valid certificate-bound token are rejected. -
proof_of_possession_auth_methods_validation: Ensures that only authentication methods compatible with PoP can be used when PoP is enabled. -
proof_of_possession_mtls_from_header: Tells the plugin to read the client certificate from thex-client-certHTTP header instead of the TLS layer.-
certificate_header_name: The name of the HTTP header containing the client certificate. -
certificate_header_format: The encoding of the certificate in the header.base64_encodedmeans the certificate bytes are base64-encoded (for example, from a DER-encoded certificate). -
ca_certificates: A list of CA Certificate entity UUIDs that the plugin uses to validate the certificate in the header. -
ssl_verify: Validates the certificate chain against the configured CA certificates. -
secure_source: When set totrue(default), the plugin only reads the certificate header if the client IP is in Kong Gateway’s trusted IP list. For this tutorial, we’re setting this tofalseto accept the header from any source. In production, you would leave it astrueand configure the WAF or load balancer IP in Kong Gateway’s trusted IPs.
-
Validate the flow
Let’s check that client certificates are being read from the headers.
Get mTLS-bound access token
Request an access token from Keycloak’s token endpoint while presenting the client certificate.
Keycloak binds the certificate thumbprint to the token in the cnf.x5t#S256 claim.
export TOKEN=$(curl -s -X POST "https://localhost:9443/realms/master/protocol/openid-connect/token" \
--cacert ca.crt \
--key client.key \
--cert client.crt \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$DECK_CLIENT_ID" \
-d "client_secret=$DECK_CLIENT_SECRET" | jq -r .access_token)
echo $TOKENTo confirm the token contains the certificate thumbprint, decode it:
echo $TOKEN | cut -d'.' -f2 | tr -- '-_' '+/' | awk '{print $0"=="}' | base64 --decode 2>/dev/null | jq .cnfThe output should include a x5t#S256 claim with the SHA-256 thumbprint of the client certificate:
{
"x5t#S256": "<base64-encoded-thumbprint>"
}Send request to Kong Gateway with certificate in header
Base64-encode the client certificate:
BASE64_CERT=$(openssl x509 -in client.crt -outform DER | base64 | tr -d '\n')Pass it in the x-client-cert header along with the access token:
curl -s http://localhost:8000/headers \
-H "Authorization: Bearer $TOKEN" \
-H "x-client-cert: $BASE64_CERT"You should get an HTTP 200 response.
Kong Gateway reads the certificate from the header, validates it against the configured CA, and confirms that its thumbprint matches the cnf.x5t#S256 claim in the token before proxying the request.
Verify rejection without certificate in header
Send the same request without the x-client-cert header:
curl -si http://localhost:8000/headers \
-H "Authorization: Bearer $TOKEN"You should get an HTTP 401 Unauthorized response, confirming that the PoP validation is enforced.
Cleanup
Clean up Konnect environment
If you created a new control plane and want to conserve your free trial credits or avoid unnecessary charges, delete the new control plane used in this tutorial.
Destroy the Kong Gateway container
curl -Ls https://get.konghq.com/quickstart | bash -s -- -d