Client mTLS authentication
Enterprise Feature
Client mTLS is available as an add-on as part of an enterprise plan. If you would like to purchase this feature, please contact us at sales@zuplo.com or reach out to your account manager.
Most enterprise features can be used in a trial mode for a limited time. Feel free to use enterprise features for development and testing purposes.
Client mTLS authentication lets your Zuplo gateway verify the identity of
clients calling your API with certificates issued by your own certificate
authority (CA). Both the client and the gateway authenticate each other during
the TLS handshake. Routes protected by the mtls-auth-inbound policy only allow
clients that present a valid certificate chain anchored by a CA you uploaded to
Zuplo.
How client mTLS works
When a client calls your Zuplo gateway:
- The client presents a certificate during the TLS handshake.
- Zuplo's edge verifies the client certificate chain against the CA certificates uploaded to your account, then passes the verification result and parsed client certificate to your gateway workers.
- The
mtls-auth-inboundpolicy on your route reads the verification result, enforces it, and attaches the parsed certificate metadata torequest.user.data.mtlsAuthfor use in your handlers and downstream policies.
CA certificates are scoped to your Zuplo account, not a single project or deployment. Once a CA is uploaded, every gateway domain on the account can verify presented client certificate chains against it. The policy on each route controls whether unverified traffic is rejected or allowed through.
Prerequisites
Before you begin, you need:
- A public CA certificate (PEM-encoded) that has issued, or will issue, the client certificates you want to accept, either directly or through intermediate CAs
- The Zuplo CLI installed and authenticated
- A Zuplo project where you can add the
mtls-auth-inboundpolicy to a route
You only upload your public CA certificate to Zuplo. Private keys and issued client certificates stay with you and your clients.
1/ Upload your CA certificate
Use the Zuplo CLI to upload your CA certificate. The CA is registered against your account and is automatically made available on all of your gateway domains.
First, authenticate your client:
Code
Then you can create a CA by running:
Code
Parameters:
--name: A unique identifier for the CA. Must be a valid JavaScript identifier (letters, digits,_,$; cannot start with a digit).--cert: Path to the PEM-encoded CA certificate (-----BEGIN CERTIFICATE-----...). DER is not supported.--account: Your Zuplo account name.
The command returns the new CA's ID (prefixed with mtlsca_). You can list all
CAs on the account at any time:
Code
See the ca-certificate CLI reference for
all available subcommands (create, list, describe, update, delete).
Upload the self-signed root CA, not an intermediate
Zuplo must build a complete chain from the presented client certificate up to a
trust anchor. Upload the self-signed root CA that anchors the chain — not an
intermediate or subordinate CA. Uploading a subordinate CA is the most common
cause of the FAILED to get issuer certificate error (see
Troubleshooting).
To confirm a certificate is a self-signed root, check that its subject and issuer are identical:
Code
If subject and issuer match, it's a root. If they differ, the file is an
intermediate CA — trace the chain to the root and upload that instead. When your
client certificates are issued by an intermediate CA, clients still send the
leaf certificate plus any intermediates when they connect (see
Test with curl).
2/ Add the mTLS auth inbound policy
Add the mtls-auth-inbound policy to any
route that should require a verified client certificate. The policy reads the
verification result that Zuplo's edge attached to the request and either rejects
unverified traffic or allows it through, depending on configuration.
config/policies.json
Key options:
allowUnauthenticatedRequests(defaultfalse): When set tofalse, the policy rejects requests that don't present a valid client certificate signed by a CA on your account. When set totrue, the policy lets traffic through but still attaches certificate metadata when a parseable client certificate is present, which is useful for staged rollouts or logging-only modes.certIssuerDN: The fully qualified issuer distinguished name that the client certificate must be signed by. This is the issuer DN on the client certificate, which may be an intermediate CA when the client sends a chain.
See the full policy reference for all options.
Finding your certIssuerDN value
The issuer DN is stored on the client certificate itself. Read it from a client certificate that Zuplo should accept:
Code
This prints something like issuer=CN=example-ca,O=Example,C=US. Copy the part
after issuer= into certIssuerDN. The policy tolerates casing and whitespace
differences, but not RDN reordering, so keep the order produced by openssl
as-is.
3/ Read certificate metadata in your handler
When verification succeeds, the policy attaches parsed certificate metadata to
request.user.data.mtlsAuth. If request.user does not already exist, the
policy also sets request.user.sub to the certificate subject.
Code
The metadata object includes:
subject— the client certificate subject DNissuer— the issuer DN (the CA that signed the certificate)notBefore/notAfter— validity window in ISO 8601 formatsha256Fingerprint— SHA-256 digest of the DER-encoded certificate, uppercase hex with colon separators (e.g.AB:CD:EF:...). Useful for pinning specific client certificates.
The raw client certificate is also available on
context.incomingRequestProperties.clientCert in
RFC 9440 format
(Base64-encoded DER, colon-wrapped) if you need to perform custom parsing or
forward it to a backend.
4/ Test with curl
Once your CA is uploaded and the policy is on the route, you can verify the
end-to-end flow with curl. You'll need a client certificate and private key
issued by, or chained to, the CA you uploaded.
Send the certificate and key with --cert and --key:
Code
Confirm that:
- A request without
--certis rejected with401whenallowUnauthenticatedRequestsisfalse. - A request with a certificate that chains to your uploaded CA succeeds and your
handler sees the parsed certificate on
request.user.data.mtlsAuth. - A request with a certificate signed by a different CA is rejected.
Using client certificates as part of a certificate chain
If your client certificates are issued by an intermediate CA (rather than
directly by your root), pass a certificate bundle to curl that includes the
leaf client certificate followed by any intermediate CA certificates. Do not
include the root CA in the client certificate bundle.
Code
Manage CA certificates
Listing CAs
Code
Inspecting a CA
Code
Renaming a CA
Only the name can be updated; to replace the certificate body, delete the CA and create a new one.
Code
Deleting a CA
Code
Deleting a CA stops verification for client certificates issued by it on all of
your gateway domains. Routes that use mtls-auth-inbound with
allowUnauthenticatedRequests: false will start rejecting those clients
immediately. Rotate to a new CA and update your clients before deleting the old
CA.
Rotating a CA
To rotate the CA without downtime:
- Upload the new CA alongside the existing one with
zuplo ca-certificate create. - Reissue client certificates from the new CA and distribute them to your clients.
- Once all clients have moved to the new CA, delete the old CA with
zuplo ca-certificate delete.
If you've followed the common practice of preserving the CA's subject DN across
the rotation (only the key, serial, and validity dates change), the issuer DN on
newly issued client certificates is identical to the previous one and
certIssuerDN does not need to change. If the rotation deliberately changes
the CA's subject DN, update certIssuerDN to match the new value before cutting
clients over — or temporarily set allowUnauthenticatedRequests: true to allow
both issuers during the transition.
Local development
The mtls-auth-inbound policy relies on verification metadata supplied by
Zuplo's edge proxy and does not work in local development with zuplo dev. Test
the policy in a working-copy or preview environment.
Troubleshooting
Requests are rejected with 401
- Confirm the client is presenting a certificate signed by a CA that's been
uploaded with
zuplo ca-certificate list. If the client certificate is issued by an intermediate CA, confirm the client sends the intermediate certificate chain. - If you've set
certIssuerDN, verify it matchesrequest.user.data.mtlsAuth.issuerexactly (casing and whitespace are tolerated, but RDN order is not). - Temporarily set
allowUnauthenticatedRequests: trueand logcontext.incomingRequestProperties.clientMtlsVerificationStatusandcontext.incomingRequestProperties.clientMtlsVerificationReasonto see why verification failed.
FAILED to get issuer certificate
This error means Zuplo can't build a complete chain from the presented client certificate up to a trusted root. The usual cause is uploading an intermediate or subordinate CA instead of the self-signed root CA that anchors the chain.
Confirm what you uploaded. A self-signed root has an identical subject and issuer:
Code
If subject and issuer differ, the file is an intermediate CA. Find the root
that anchors the chain and re-upload it:
Code
If your CA is an Active Directory Certificate Services (AD CS) deployment, export the issuing CA's own certificate and inspect it:
Code
client certificate metadata not provided
The certificate verified at the edge, but the parsed certificate wasn't
forwarded to your gateway workers, so request.user.data.mtlsAuth is empty even
though the request was authenticated.
The most common cause is an oversized leaf client certificate. Zuplo's edge can only forward client certificates up to roughly 10 KB of DER-encoded data. Certificates with large RSA keys, many Subject Alternative Names, or large custom extensions can exceed this. Check the DER size of the leaf certificate:
Code
If the result is near or above ~10,000 bytes, reissue a smaller leaf certificate. Trim unnecessary extensions and SANs, or switch to ECDSA keys instead of large (4096-bit) RSA keys. This limit applies to the leaf certificate the edge forwards, not the full chain.
request.user.data.mtlsAuth is missing
- The policy only attaches metadata when a parseable client certificate is present on the request. Confirm the client is sending one.
- Verify the route includes the
mtls-auth-inboundpolicy. - If the certificate verifies but metadata is still missing, check the leaf
certificate size (see
client certificate metadata not provided).
Custom domains
When a CA is uploaded, it's automatically associated with Zuplo's managed gateway domains. A custom domain must be active in the dashboard (check the Settings/Custom Domains sidebar) before CA verification will become active on that custom domain.
If you add a custom domain later and your clients aren't being verified against it, contact support@zuplo.com.
Custom domains behind your own CDN
Inbound mTLS requires the TLS handshake to terminate at Zuplo's edge, because
that's where the client certificate is verified and parsed. If you front your
Zuplo gateway with your own CDN (for example, your own Cloudflare zone) that
terminates TLS before traffic reaches Zuplo, the handshake — and the client
certificate — ends at your CDN. Zuplo never sees the certificate, so the
mtls-auth-inbound policy has nothing to verify.
You have two supported options:
- Let Zuplo terminate TLS. Point clients at a Zuplo-managed gateway domain, or configure your custom domain directly on Zuplo so Zuplo terminates TLS. The client certificate then reaches Zuplo's edge and inbound mTLS works as documented above.
- Verify mTLS at your CDN. If you must keep your own CDN in front, terminate
and verify the client certificate at the CDN, then forward the verified
identity to Zuplo in a request header. Validate that header in a Zuplo policy
instead of relying on
mtls-auth-inbound. Make sure the header can't be spoofed by clients that bypass your CDN.
Additional resources
mtls-auth-inboundpolicy referenceca-certificateCLI reference- Gateway to Origin mTLS Authentication — the reverse direction, where Zuplo authenticates to your backend with a client certificate
If you need help configuring client mTLS for your account, contact us at support@zuplo.com.