ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop on the web portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
    Policy Catalog
    Authentication
    Authorization
    MCP Authorization
    Security & Validation
    Metrics, Billing & Quotas
    Testing
    Request Modification
    Response Modification
    Upstream Authentication
    Archival
    GraphQL
    Other
    Guides
      Multiple Auth PoliciesSecure your GraphQL APIPer-user rate limitsComposite Policy PatternsClient mTLS
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Guides

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:

  1. The client presents a certificate during the TLS handshake.
  2. 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.
  3. The mtls-auth-inbound policy on your route reads the verification result, enforces it, and attaches the parsed certificate metadata to request.user.data.mtlsAuth for 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-inbound policy 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:

TerminalCode
zuplo login

Then you can create a CA by running:

TerminalCode
zuplo ca-certificate create \ --name my_ca \ --cert ./ca.pem \ --account your-account

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:

TerminalCode
zuplo ca-certificate list --account your-account

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:

TerminalCode
openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253

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
{ "name": "my-mtls-auth-inbound-policy", "policyType": "mtls-auth-inbound", "handler": { "export": "MTLSAuthInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "allowUnauthenticatedRequests": false, "certIssuerDN": "CN=example-ca, O=Example, C=US" } } }

Key options:

  • allowUnauthenticatedRequests (default false): When set to false, the policy rejects requests that don't present a valid client certificate signed by a CA on your account. When set to true, 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:

TerminalCode
openssl x509 -in client.pem -noout -issuer -nameopt RFC2253

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
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const mtls = request.user?.data?.mtlsAuth; if (!mtls) { return new Response("No client certificate", { status: 401 }); } context.log.info("Authenticated client", { subject: mtls.subject, issuer: mtls.issuer, fingerprint: mtls.sha256Fingerprint, }); // Authorize based on certificate subject, fingerprint, etc. return new Response(`Hello, ${mtls.subject}`); }

The metadata object includes:

  • subject — the client certificate subject DN
  • issuer — the issuer DN (the CA that signed the certificate)
  • notBefore / notAfter — validity window in ISO 8601 format
  • sha256Fingerprint — 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:

TerminalCode
curl --cert ./client.pem --key ./client.key \ https://your-gateway.zuplo.app/v1/example

Confirm that:

  • A request without --cert is rejected with 401 when allowUnauthenticatedRequests is false.
  • 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.

TerminalCode
cat client.pem intermediate.pem > client-chain.pem curl --cert ./client-chain.pem --key ./client.key \ https://your-gateway.zuplo.app/v1/example

Manage CA certificates

Listing CAs

TerminalCode
zuplo ca-certificate list --account your-account

Inspecting a CA

TerminalCode
zuplo ca-certificate describe \ --cert-id mtlsca_abc123 \ --account your-account

Renaming a CA

Only the name can be updated; to replace the certificate body, delete the CA and create a new one.

TerminalCode
zuplo ca-certificate update \ --cert-id mtlsca_abc123 \ --name renamed_ca \ --account your-account

Deleting a CA

TerminalCode
zuplo ca-certificate delete \ --cert-id mtlsca_abc123 \ --account your-account

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:

  1. Upload the new CA alongside the existing one with zuplo ca-certificate create.
  2. Reissue client certificates from the new CA and distribute them to your clients.
  3. 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 matches request.user.data.mtlsAuth.issuer exactly (casing and whitespace are tolerated, but RDN order is not).
  • Temporarily set allowUnauthenticatedRequests: true and log context.incomingRequestProperties.clientMtlsVerificationStatus and context.incomingRequestProperties.clientMtlsVerificationReason to 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:

TerminalCode
openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253

If subject and issuer differ, the file is an intermediate CA. Find the root that anchors the chain and re-upload it:

TerminalCode
# Remove the incorrect CA zuplo ca-certificate delete --cert-id mtlsca_abc123 --account your-account # Upload the self-signed root instead zuplo ca-certificate create --name my_ca --cert ./root-ca.pem --account your-account

If your CA is an Active Directory Certificate Services (AD CS) deployment, export the issuing CA's own certificate and inspect it:

TerminalCode
# Export the CA certificate (often DER-encoded) certutil -ca.cert ca.cer # Convert DER to the PEM format Zuplo requires openssl x509 -inform der -in ca.cer -out ca.pem # Verify subject == issuer (a root); if not, export the root above it instead openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253

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:

TerminalCode
openssl x509 -in client.pem -outform der | wc -c

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-inbound policy.
  • 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-inbound policy reference
  • ca-certificate CLI 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.

Edit this page
Last modified on June 22, 2026
Composite Policy PatternsURL Forward
On this page
  • How client mTLS works
  • Prerequisites
  • 1/ Upload your CA certificate
  • 2/ Add the mTLS auth inbound policy
  • 3/ Read certificate metadata in your handler
  • 4/ Test with curl
  • Manage CA certificates
    • Listing CAs
    • Inspecting a CA
    • Renaming a CA
    • Deleting a CA
    • Rotating a CA
  • Local development
  • Troubleshooting
    • Requests are rejected with 401
    • FAILED to get issuer certificate
    • client certificate metadata not provided
    • request.user.data.mtlsAuth is missing
    • Custom domains
    • Custom domains behind your own CDN
  • Additional resources
JSON
TypeScript