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

Connect2id server 6.13 added support for letting OAuth 2.0 clients authenticate with a X.509 certificate submitted during the TLS handshake, thus enabling issued access tokens to be bound to it (fixing the bearer weakness). The public key encoded in the certificate must be registered as an RSA or EC JWK with the Connect2id server. This authentication method, named self_signed_tls_client_auth, is specified in the Mutual TLS Profile for OAuth 2.0 (RFC 8705).

TLS (HTTPS) can be handled by the 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 latter method, as it provides more flexibility.

In order for self_signed_tls_client_auth to work:

  1. The TLS proxy must be configured to accept self-signed client certificates;

  2. Once TLS proxy and client are mutually authenticated, the TLS proxy must pass the received client X.509 certificate to the Connect2id server for public key validation, via an agreed HTTP security header.

Instructions

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

The client certificate must be passed from the TLS termination proxy to the Connect2id server for final validation of its public key:

  1. The client certificate is 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 badly configured and incoming HTTP headers are not sanitised, the header should be given a name that is hard to guess (i.e. include 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 debugging.

2. Configure the Connect2id server

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

op.tls.clientX509CertHeader=Sec-Client-X509-Cert-alaeLuL8geiqu3OhOg1Mafa4Ecu9ahsh

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.

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

3. Configure the TLS termination proxy

This configuration is proxy specific. You basically need to setup the TLS proxy to:

  1. Accept self-signed client X.509 certificates;

  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):

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):

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 super important for security!

Tips

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 this 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:

127.0.0.1 - - [15/May/2020:12:54:10 +0300] "POST /c2id/token HTTP/1.1" -----BEGIN%20CERTIFICATE-----MIICsDCCAZigAwIBAgIIQSxFDTCDGrEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNajZvdWdmeHVmN2E1dzAeFw0yMDA1MTUwOTU0MDlaFw0yMTA1MTUwOTU0MTBaMBgxFjAUBgNVBAMMDWo2b3VnZnh1ZjdhNXcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrBbE8ONDDSN
ugiW8KgSMSST6BepNDUnvm8vYPNm0j0%2BPAaUYmQ%2F%2BYtbd8OG%2BFyWfzbk0fYnUdNI15cwjh2%2F3SK2DWpRtLgTPzoqJAZoNrLv9ygyvj%2BU%2FTKFFrLGU9xsPx%2BaNVE%2FbTj01TqAzHhUnEI2aocY3AO2Guigkb%2F%2FaQxZI6FKi3dmmXr5l0EyJhbKEiOY8%2Fc3%2B57Pfv8AVKD8v%2BG3yyf3NeIthXLLuuyBZR%2F3rnn6FHAX0Rv0l0AibudN8v9YjjgGkOROTTcuOtcDeh9qeXwNVf5CXqu3K75A%2Bq
u1lR6bDREoU6lpCYKspYn2tOs8d8VN1XGGUV4241wFbNAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAvyw44rwgAOClRXyNhbjQhi9LdTMRsx8LDFhAgM9XrSws8czi2sPF2D587xPPu%2F3ZufFduTqnoP3KI0aRwrOL%2FDDCo3QeMku9rVPxG3bB7nZ%2F%2Fr2%2FWGVJe%2Bi1sQSAaP5MQ%2FoakOtr1obKNo9NZNIQwS6LcrY778SSn%2BrlB9lzqs0Q8bF%2FZkyqVnVL9e18UKvO3fU8M8BI2KJh1xQpKFPGJUuQEhklq5
cfAWyZjx6x2wvHuSxbq%2FSWh%2F%2FhEpFjMSihO%2BllU2WV%2FqQueFRm2E0w7J2FdRM9l2PpKdNs8Txj5EAg7PasENKOjCOeU5l1Y7lvnE3fmHSSQpbsv4pXvaLmU%3D-----END%20CERTIFICATE----- 200 781