Configure OpenID Connect with mTLS Proof-of-Possession via header

TL;DR

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

This is a Konnect tutorial and requires a Konnect personal access token.

  1. Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.

  2. Export your token to an environment variable:

    export KONNECT_TOKEN='YOUR_KONNECT_PAT'
  3. 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-output

    This 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.

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.

  1. Export your license to an environment variable:

     export KONG_LICENSE_DATA='LICENSE-CONTENTS-GO-HERE'
  2. Run the quickstart script:

    curl -Ls https://get.konghq.com/quickstart | bash -s -- -e KONG_LICENSE_DATA 

    Once Kong Gateway is ready, you will see the following message:

     Kong Gateway Ready

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.

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:

  1. 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
  1. Create a working directory and run the following steps from it:

    mkdir -p ~/oidc-pop/certs && cd ~/oidc-pop/certs
  2. 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"
  3. 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
  4. 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

  1. Start Keycloak in Docker with both HTTP and HTTPS enabled. The KC_HOSTNAME environment variable pins the issuer URL to http://localhost:8080 so 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
  2. Open the Keycloak admin console at http://localhost:8080/admin/master/console/.

  3. Log in with the credentials you set in the docker run command. For this example, we set username admin, password admin.

  4. In the sidebar, open Clients, then click Create client.

  5. 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/*
  6. Click the Advanced tab.
  7. In the Advanced settings section, enable OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled.
  8. Click Save at the bottom of the Advanced settings section.
  9. Click the Credentials tab.
  10. Set Client Authenticator to Client ID and Secret.
  11. Copy the Client Secret.
  12. Export your client credentials and Keycloak issuer. DECK_ISSUER uses localhost because that’s the pinned issuer in tokens. DECK_JWKS_ENDPOINT uses the keycloak container 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 the iss claim in incoming tokens. Set this to the pinned issuer URL (http://localhost:8080/realms/master), which matches the iss claim 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 name keycloak so 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 to strict ensures 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 the x-client-cert HTTP 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_encoded means 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 to true (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 to false to accept the header from any source. In production, you would leave it as true and 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 $TOKEN

To confirm the token contains the certificate thumbprint, decode it:

echo $TOKEN | cut -d'.' -f2 | tr -- '-_' '+/' | awk '{print $0"=="}' | base64 --decode 2>/dev/null | jq .cnf

The 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

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.

curl -Ls https://get.konghq.com/quickstart | bash -s -- -d

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!