How to set up a TLS termination proxy for client authentication with X.509 certificate

The Connect2id server allows OAuth 2.0 clients to authenticate with a client X.509 certificate submitted during the TLS handshake. The two variants of this authentication are specified in the Mutual TLS Profile for OAuth 2.0 (RFC 8705):

  • tls_client_auth -- The client authenticates with an X.509 certificate issued by a Certificate Authority (CA) that is trusted by the authorisation server. This method relies on the client and server participating in a Public Key Infrastructure (PKI) governed by a CA or a hierarchy of CAs.

    Support for this method is available since Connect2id server 11.0.

  • self_signed_tls_client_auth -- The client authenticates with a self-signed (and self-issued) X.509 certificate. The validity of the certificate is established by the client having its certificate RSA or EC public key registered with the Connect2id server.

    Support for this method is available since Connect2id server 6.13. It is easier to setup because it doesn't rely on a PKI and CA.

TLS (HTTPS) can be handled by the Java servlet container (e.g. Apache Tomcat) where the Connect2id server is deployed, or by a dedicated TLS termination proxy, such as Nginx or Apache httpd. We recommend the TLS termination proxy method, because it's more flexible and makes load balancing simpler.


1. Define an HTTP header name for passing the client X.509 certificate

If the client submits a certificate in the TLS handshake the TLS termination proxy must check it according to the method (tls_client_auth or self_signed_tls_client_auth) and then pass on the certificate to the Connect2id server so that the server can obtain the necessary details from it.

How is the certificate passed?

  1. The client certificate is first encoded into a PEM-encoded string, with optional additional URL-encoding applied to the PEM string;

  2. The PEM string is then inserted as a special new HTTP header into the HTTP request.

To prevent injection attacks the TLS termination proxy must be configured to remove all incoming HTTP headers bearing the same name. For extra security, in case the TLS termination proxy gets misconfigured and incoming HTTP headers are not sanitised, the header should be given a name that is hard to guess (e.g. by including a long random portion) and kept secret.

Example header to pass a PEM-encoded encoded client certificate from the TLS termination proxy to the Connect2id server:

Sec-Client-X509-Cert-alaeLuL8geiqu3OhOg1Mafa4Ecu9ahsh: -----BEGIN CERTIFICATE-----MIICsDCCAZigAwIBAgIIdF+Wcca7gzkwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNY2FvajdicjRpcHc2dTAeFw0xNzA4MDcxNDMyMzVaFw0xODA4MDcxNDMyMzZaMBgxFjAUBgNVBAMMDWNhb2o3YnI0aXB3NnUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdrt40Otrveq46K3BzZuds6wDqsP0kZV+C3GdyTQWl53orBRtPIiEh6BauP17Rr19qadh7t4yFBb5thrXwBewseSNEL4j7sB0YoeNwRsmA29Fjfoe0yeNpLixFadL6dz7ej9xW2suPppIO6jA5SYgL6+S42ZlIauCnSQBKFcdP8QRvgDZBZ4A7CmuloRJst7GQzppa+YWR+Zg3V5reV8Ekrkjxhwgd+rMsGahxijY7Juf2zMgLOXwe68y41SGnn+1RwezAhnJgioGiwY2gP7z2m8yNZXhpUiX+KAP2xvYb60wNYOswuqfpya68rSmYT8mQjld1EPR21dBMjRQ8HfUBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAIUlqltRlbqiolGETmAUF8AiC008UCUmI+IsnORbHFSaACKW04m1iFH0OlxuAE1ECj1mlTcKb4md6i7n+Fy+fdGXFL73yhlSiBLu7XW5uN1/dAkynA+mXC5BDFijmvkEAgNLKyh40u/U1u75v2SFS+kLyMeqmVxvUHA7qA8VgyHi/FZzXCfEvxK5jye4L8tkAR34x5j5MpPDMfLkwLegUG+ygX+h/f8luKiQAk7eD4C59c/F0PpigvzcMpyg8+SE9loIEuJ9dRaRaTwIzez3QA7PJtrhu9h0TooTtkmF/Zw9HARrO0qXgT8uNtQDcRXZCItt1Qr7cOJyx2IjTFR2rE=-----END CERTIFICATE-----

Some proxies, such as Nginx, allow the PEM string to be additionally URL-encoded in order to escape special characters that may potentially break the header:

Sec-Client-X509-Cert-alaeLuL8geiqu3OhOg1Mafa4Ecu9ahsh: -----BEGIN%20CERTIFICATE-----MIICsDCCAZigAwIBAgIIdF%2BWcca7gzkwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNY2FvajdicjRpcHc2dTAeFw0xNzA4MDcxNDMyMzVaFw0xODA4MDcxNDMyMzZaMBgxFjAUBgNVBAMMDWNhb2o3YnI0aXB3NnUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdrt40Otrveq46K3BzZuds6wDqsP0kZV%2BC3GdyTQWl53orBRtPIiEh6BauP17Rr19qadh7t4yFBb5thrXwBewseSNEL4j7sB0YoeNwRsmA29Fjfoe0yeNpLixFadL6dz7ej9xW2suPppIO6jA5SYgL6%2BS42ZlIauCnSQBKFcdP8QRvgDZBZ4A7CmuloRJst7GQzppa%2BYWR%2BZg3V5reV8Ekrkjxhwgd%2BrMsGahxijY7Juf2zMgLOXwe68y41SGnn%2B1RwezAhnJgioGiwY2gP7z2m8yNZXhpUiX%2BKAP2xvYb60wNYOswuqfpya68rSmYT8mQjld1EPR21dBMjRQ8HfUBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAIUlqltRlbqiolGETmAUF8AiC008UCUmI%2BIsnORbHFSaACKW04m1iFH0OlxuAE1ECj1mlTcKb4md6i7n%2BFy%2BfdGXFL73yhlSiBLu7XW5uN1%2FdAkynA%2BmXC5BDFijmvkEAgNLKyh40u%2FU1u75v2SFS%2BkLyMeqmVxvUHA7qA8VgyHi%2FFZzXCfEvxK5jye4L8tkAR34x5j5MpPDMfLkwLegUG%2BygX%2Bh%2Ff8luKiQAk7eD4C59c%2FF0PpigvzcMpyg8%2BSE9loIEuJ9dRaRaTwIzez3QA7PJtrhu9h0TooTtkmF%2FZw9HARrO0qXgT8uNtQDcRXZCItt1Qr7cOJyx2IjTFR2rE%3D-----END%20CERTIFICATE-----

Note: The additional URL-encoding of the PEM string is accepted since Connect2id server 8.1.

We recommend you use a random 32 character string for this header. You can use the Linux pwgen utility to generate suitable random strings:

pwgen 32

The Sec-Client-X509-Cert- prefix is intended to aid logging and debugging.

2. Configure the Connect2id server

Edit the Connect2id server configuration for the client X.509 certificate header:


Note that the Connect2id server will check if the header name is at least 32 characters long, for the mentioned injection attack prevention. Future versions may also include a randomness check.

Also, make sure the Connect2id server is actually configured to allow tls_client_auth or self_signed_tls_client_auth authentication.

Remember to restart your Connect2id server instances for the configuration to take effect.

3. Configure the TLS termination proxy

The exact TLS termination proxy configuration is software specific, but the objectives are these:

  1. Depending on the configured mTLS client authentication method:

    • For tls_client_auth:

      • configure client certificates as optional;
      • if a client certificate is submitted configure validation for it being issued by a trusted CA (typically by providing a file with the root certificate, plus any intermediates, of the CA that issues the client certificates).
    • For self_signed_tls_client_auth:

      • configure client certificates as optional;
      • if a client certificate is submitted configure acceptance of self-signed certificates.

    Note, due to limitations of the commonly available TLS termination software, configuring simultaneous validation of CA-issued client certificates as well as acceptance of self-signed client certificates is normally not possible. Because of that a Connect2id server deployment can be configured to support either tls_client_auth or self_signed_tls_client_auth, but not both.

  2. Remove all HTTP headers with the name used to pass the client certificate to the Connect2id server, in order to block injection attacks.

  3. Insert the PEM-encoded certificate into the special HTTP header. Applying additional URL-encoding of the PEM string is recommended if the TLS termination proxy supports that.

Example proxy configurations:

Nginx (ngx_http_ssl_module)

For tls_client_auth:

server {
    listen 443 ssl;
    ssl_verify_client optional;
    ssl_trusted_certificate /path/to/clients_root_CA_cert_plus_intermediates.pem;
    proxy_set_header Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing "";
    proxy_set_header Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing $ssl_client_escaped_cert;

For self_signed_tls_client_auth:

server {
    listen 443 ssl;
    ssl_verify_client optional_no_ca;
    proxy_set_header Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing "";
    proxy_set_header Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing $ssl_client_escaped_cert;
Apache HTTPd (mod_ssl)

For tls_client_auth:

SSLVerifyClient optional
SSLCACertificateFile "/path/to/clients_root_CA_cert_plus_intermediates.pem"
RequestHeader set Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing ""
RequestHeader set Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing "%{SSL_CLIENT_CERT}s"

For self_signed_tls_client_auth:

SSLVerifyClient optional_no_ca
RequestHeader set Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing ""
RequestHeader set Sec-Client-X509-Cert-liede5vaePeeMiYie0xu2jaudauleing "%{SSL_CLIENT_CERT}s"
Traefik (mTLS clientAuth)

Setting of security header name not supported as of v2.2.

TLS termination proxy configuration caveats

  1. No new lines or white space must be present in the PEM-encoded client certificate else processing of the HTTP headers may fail.

  2. Again, remember to sanitise the incoming HTTP headers -- this is imperative for security!


1. Log message displaying if the client certificate header is configured

If the Connect2id server has a client certificate header configured it will log the following line at startup:

MAIN - [OP6900] TLS termination proxy: Client X.509 certificate HTTP header: X-Client-X509...

If the header is disabled:

MAIN - [OP6900] TLS termination proxy: Client X.509 certificate HTTP header: not configured

2. mTLS authentication fails with HTTP 401

If the token request appears to be correct, but it fails with 401 check the server log. The Connect2id server returns an error message enumerating all common causes for a failed client authentication, but will deliberately not specify the concrete reason why a particular authentication failed.

HTTP/1.1 401 Unauthorized
Content-Type: application/json

  "error"             : "invalid_client",
  "error_description" : "Invalid client: Possible causes may be missing / invalid client_id, missing client authentication, ..."

If the logs contain the following line this is a clear sign that no client certificate was presented, most likely due to a client or proxy misconfiguration:

TOKEN - [OP6203] Missing client authentication: client_id=...

3. How to configure Tomcat to log a request HTTP header

The Apache Tomcat HTTP access logging is configured by the following XML element in tomcat/conf/server.xml:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
       prefix="localhost_access_log" suffix=".txt"
       pattern="%h %l %u %t &quot;%r&quot; %s %b" />

To log an incoming HTTP header add %{xxx}i to the pattern element where xxx is the name of the HTTP header.

If the header name is X-Client-X509-Cert-alaeLuL8geiqu3OhOg1Mafa4Ecu9ahsh:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
       prefix="localhost_access_log" suffix=".txt"
       pattern="%h %l %u %t &quot;%r&quot; %{X-Client-X509-Cert-alaeLuL8geiqu3OhOg1Mafa4Ecu9ahsh}i %s %b" />

This will then log a line similar to this one in tomcat/logs/localhost_access_log.YYYY-MM-DD.txt: - - [15/May/2020:12:54:10 +0300] "POST /c2id/token HTTP/1.1" -----BEGIN%20CERTIFICATE-----MIICsDCCAZigAwIBAgIIQSxFDTCDGrEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNajZvdWdmeHVmN2E1dzAeFw0yMDA1MTUwOTU0MDlaFw0yMTA1MTUwOTU0MTBaMBgxFjAUBgNVBAMMDWo2b3VnZnh1ZjdhNXcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrBbE8ONDDSN
cfAWyZjx6x2wvHuSxbq%2FSWh%2F%2FhEpFjMSihO%2BllU2WV%2FqQueFRm2E0w7J2FdRM9l2PpKdNs8Txj5EAg7PasENKOjCOeU5l1Y7lvnE3fmHSSQpbsv4pXvaLmU%3D-----END%20CERTIFICATE----- 200 781