Skip to main content

Creating and Using a Client Certificate for mTLS

I recently had to figure out client-side mTLS certificates. I've never had to deal with them before, and it was quite annoying, so I documented what I learned.

What is mTLS?

This is a very basic explanation. Let's compare HTTPS (HTTP over TLS) to mTLS, since they both include TLS and most people already understand HTTPS:

  • In HTTPS, the server presents a TLS certificate (the kind of certificate that you'd get from Let's Encrypt for your server) and the client (usually your web browser) verifies the certificate with a trusted third party certificate authority (like Let's Encrypt).
  • In mTLS, the server and the connecting client both present TLS certificates to each other and they both verify each others' certificates.

mTLS provides some security benefits over plain HTTPS, but I'm only interested in talking about how to actually get this running.

Obtain a Client Certificate for mTLS

Obtaining a client certificate has recently (October 2025) become much more difficult as most certificate authorities have deprecated their client EKUs, and will soon remove them (in 2026). The client EKU is needed for the client to offer up its certificate (which is what mTLS is all about). These changes are part of a broader industry movement, but let's not get into that here.

For my purposes, I obtained an X9 PKI certificate through Digicert. It is very expensive (~$40/month), considering that you can still obtain server EKU HTTPS TLS certificates for free (like through Let's Encrypt).

Create a Certificate Signing Request (CSR) and private key with the following CLI command:

openssl req -new -newkey rsa:2048 -nodes -keyout my-cert.key -out my-cert.csr

The .csr file will need to be pasted / uploaded to DigiCert. Save the .key file for the next step but DO NOT SHARE the .key file! It is your private key.

Use the Client Certificate for mTLS

This code only works in Node.js:

import {Agent} from 'undici';

new Agent({
connect: {
key: mtlsSecret.privateKey,
cert: [
mtlsSecret.myCertificate,
mtlsSecret.certificateAuthorityCertificate,
].join('\n'),
rejectUnauthorized: true,
},
});

await fetch(someUrl, {
/** TypeScript doesn't know about this property existing, but it _will_ work. */
dispatcher: agent,
});

The contents of mtlsSecret are, as follows:

  • mtlsSecret.privateKey: the contents of my-cert.key.
    • This should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY-----.
  • mtlsSecret.myCertificate: your certificate given from DigiCert (or whatever certificate authority you used).
    • Digicert called it my_cert.crt (the file name will depend on what common name you gave your certificate).
    • This should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----.
  • mtlsSecret.certificateAuthorityCertificate: the certificate authority's certificate.
    • Digicert called it DigiCertCA.crt
    • This should also start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----.