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
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
    Overview
    Managed Dedicated
    Managed EdgeSelf Hosted
    Custom Domains
    Securing Your Backend
      Securing your backendGateway to Origin mTLS AuthenticationConnect to an AWS ALB with mTLSGCP Backend with Upstream Auth
      Secure Tunnel
    Web Application Firewalls
    DDoS Protection
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Securing Your Backend

Gateway to Origin mTLS Authentication

Enterprise Feature

mTLS Client Certificates 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.

Mutual TLS (mTLS) authentication establishes a trust relationship between your Zuplo API Gateway and your backend services using client certificates. With mTLS, both the client (Zuplo Gateway) and the server (your backend) authenticate each other, creating a "Zero Trust" security model.

This is particularly useful for enterprise customers who need to ensure that both parties in a connection verify each other's identity before exchanging data.

How mTLS Works

When Zuplo makes an outbound request to your backend service:

  1. Your backend service presents its SSL/TLS certificate to Zuplo (standard TLS)
  2. Zuplo presents a client certificate to your backend (the mutual part)
  3. Both parties verify each other's certificates against a trusted Certificate Authority (CA)
  4. Only after mutual verification does the secure connection establish

This ensures that your backend only accepts requests from authorized Zuplo gateways, and Zuplo can verify it's connecting to the correct backend service.

Prerequisites

Before you begin, you need:

  • A PEM-encoded client certificate and matching private key generated from a Certificate Authority (CA) that your backend trusts
  • Your backend service configured to require and validate client certificates
  • The Zuplo CLI installed (see CLI documentation)

Client certificate requirements

The certificate uploaded to Zuplo is the client, or leaf, certificate that the gateway presents to your backend. It should have the following properties:

  • X.509 version 3
  • Basic Constraints: critical, CA:FALSE
  • Key Usage: critical, Digital Signature
  • Extended Key Usage: TLS Web Client Authentication (clientAuth)
  • An RSA key between 2048 and 8192 bits, or an ECDSA key using P-256, P-384, or P-521
  • A SHA-256 or stronger signature
  • A subject that uniquely identifies the gateway credential
  • A validity period appropriate for your certificate rotation policy

The issuing CA certificate must be X.509 version 3 with Basic Constraints: critical, CA:TRUE and a key usage that includes Certificate Sign and CRL Sign. Your backend's trust store must contain the CA certificates needed to build a path from the client certificate to a trusted root. When the trusted CA directly signs the client certificate, you do not need a client certificate-chain bundle. If an intermediate CA signs it, follow your backend's requirements for presenting and trusting the intermediate chain.

The private key must match the public key in the client certificate. Keep it secret and limit access to it. The key must be unencrypted when uploaded to Zuplo so the gateway can use it without a passphrase.

1/ Upload Your Certificate

Use the Zuplo CLI to upload your client certificate and private key to your project. You can upload multiple certificates, each with a unique name.

TerminalCode
zuplo mtls-certificate create \ --cert cert.pem \ --key key.pem \ --name my_backend_cert \ --account your-account \ --project your-project \ --environment-type development \ --environment-type preview \ --environment-type production

The certificate name must follow JavaScript's variable naming constraints since you will use the name later in your code. The CLI will validate these constraints when you create the certificate.

Parameters:

  • --cert: Path to your PEM-encoded client certificate file
  • --key: Path to your PEM-encoded private key file
  • --name: A unique name to identify this certificate in your project
  • --account: Your Zuplo account name
  • --project: Your Zuplo project name
  • --environment-type: Specify which environments can use this certificate (can be specified multiple times)

2/ Use the Certificate in Your Code

Once uploaded, you can use the certificate when making outbound requests from your Zuplo Gateway.

Using mTLS in a Request Handler

Reference the certificate by name in the zuplo options object when making fetch requests:

Code
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const response = await fetch("https://secure-backend.example.com/api", { zuplo: { mtlsCertificate: "my_backend_cert", }, }); return response; }

Using mTLS in a Policy

You can also configure mTLS in the URL Forward Handler or URL Rewrite Handler that make outbound requests:

Code
{ "export": "UrlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "https://secure-backend.example.com", "mtlsCertificate": "my_backend_cert" } }

3/ Using Environment Variables

For better flexibility across environments, store the certificate name as an environment variable:

Production environment:

Code
BACKEND_MTLS_CERT=my_backend_prod_cert

Staging environment:

Code
BACKEND_MTLS_CERT=my_backend_staging_cert

Then reference it in your code:

Code
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const response = await fetch("https://secure-backend.example.com/api", { zuplo: { mtlsCertificate: environment.BACKEND_MTLS_CERT, }, }); return response; }

Or in your policy configuration:

Code
{ "export": "UrlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "https://secure-backend.example.com", "mtlsCertificate": "$env(BACKEND_MTLS_CERT)" } }

Managing Certificates

Listing Certificates

To view all certificates in your project:

TerminalCode
zuplo mtls-certificate list \ --account your-account \ --project your-project

Each entry includes the certificate ID, which the other commands take as --cert-id.

Inspecting a Certificate

To see the subject, issuer, validity dates, and enabled environment types for one certificate:

TerminalCode
zuplo mtls-certificate describe \ --cert-id cert_abc123 \ --account your-account \ --project your-project

Changing Environment Types

To change which environment types can use a certificate, pass the complete set of environment types you want enabled — the command replaces the existing set rather than adding to it:

TerminalCode
zuplo mtls-certificate update \ --cert-id cert_abc123 \ --environment-type preview \ --environment-type production \ --account your-account \ --project your-project

Deleting Certificates

To remove a certificate:

TerminalCode
zuplo mtls-certificate delete \ --cert-id cert_abc123 \ --account your-account \ --project your-project

You can't delete a certificate that's referenced by any of your deployments in your project. This is to prevent your deployments from failing if the certificate that's being referenced is no longer available.

First, disable the certificate by using the CLI with zuplo mtls-certificate disable. Then redeploy the deployments in your project that reference it. Once there are no more references to the certificate, you can delete it.

Certificate Rotation

When your certificates need to be rotated (due to expiration or security policies):

  1. Upload the new certificate with a different name
  2. Update your environment variables or code to reference the new certificate name
  3. Use the CLI zuplo mtls-certificate disable command to disable the old certificate.
  4. Deploy your changes to all environments that reference the old certificate.
  5. After verifying the new certificate works, you may delete the old certificate.

The order of operations is important so that your services continue to work as you rotate the certificate.

Local Development

zuplo dev can present client certificates to your backend, so the same mtlsCertificate code path you deploy also works on your machine. Support depends on your project type, set by projectType in zuplo.jsonc:

projectTypeLocal mTLS support
managed-dedicated, self-hostedSupported — certificates load from your local filesystem
managed-edgeNot supported

If zuplo.jsonc has no projectType field, run zuplo link — it looks up the linked project and writes the matching type into the file. Without that field, local development falls back to managed-edge and skips mTLS, even when the project deploys to a dedicated environment.

On managed-edge projects, mTLS only works in deployed environments. Test mTLS routes in a preview environment, point local runs at a backend endpoint that doesn't require client certificates, or branch on environment to skip mTLS locally.

Certificates never leave your machine — the CLI reads them from disk and passes them to the local runtime. It doesn't upload them to Zuplo or read the certificates you uploaded with zuplo mtls-certificate create.

Add certificates to your project

Place each certificate in its own subdirectory of .zuplo-local/mtls, named after the certificate. The layout matches the one used for deployments:

Code
my-project/ ├── .zuplo-local/ │ └── mtls/ │ └── my_backend_cert/ │ ├── tls.crt │ └── tls.key ├── config/ ├── modules/ └── zuplo.jsonc

The subdirectory name is the name your code references, so use the same name as the certificate you uploaded to Zuplo. Then fetch(url, { zuplo: { mtlsCertificate: "my_backend_cert" } }) resolves locally and in deployed environments without a code change.

Both files must be PEM-encoded, and tls.key must be unencrypted — the same requirements as an uploaded certificate.

Keep private keys out of Git

.zuplo-local/ holds unencrypted private keys. Projects created with create-zuplo-api gitignore it already. For older projects, add .zuplo-local/ to your .gitignore — zuplo dev prints a warning at startup when it isn't covered.

Start the server as usual:

TerminalCode
npx zuplo dev

At startup the CLI reports what it loaded:

Code
Loaded 1 local mTLS client certificate(s) from /path/to/my-project/.zuplo-local/mtls: my_backend_cert. Reference by name via the mtlsCertificate option.

Load certificates from another directory

To keep certificates outside the project — a shared team directory, or a path your secret manager writes to — pass --mtls-certs:

TerminalCode
npx zuplo dev --mtls-certs ~/certs/zuplo-uat

The flag expects the same <dir>/<cert-name>/tls.crt layout and wins over .zuplo-local/mtls. Unlike auto-detection, it fails fast: if the directory doesn't exist or holds no valid certificate, the server doesn't start. --mtls-certificates-dir is an alias for the same flag.

The CLI only looks in these two places, in order:

  1. The directory passed to --mtls-certs
  2. <project>/.zuplo-local/mtls

There is no fallback to a directory in your home folder, because client certificates are scoped to a specific gateway.

Choose the right certificate for local runs

The certificate you load locally is presented to whatever backend your route calls. Use a certificate issued for a development or staging backend, and keep production certificates off developer machines.

Loading a production client certificate locally lets any code on your machine authenticate to your production backend as the gateway. Treat these files like any other production credential.

Troubleshooting

Certificate Validation Errors

Inspect the certificate and confirm its subject, issuer, validity, and client authentication purpose:

TerminalCode
openssl x509 \ -in client.crt \ -noout \ -subject \ -issuer \ -serial \ -dates \ -purpose

SSL client should report Yes. To inspect all extensions, run openssl x509 -in client.crt -noout -text and confirm the certificate reports CA:FALSE, Digital Signature, and TLS Web Client Authentication.

Check that the certificate will remain valid for at least seven days:

TerminalCode
openssl x509 -in client.crt -noout -checkend 604800

Validate the certificate path against the same CA bundle your backend trusts:

TerminalCode
openssl verify \ -verbose \ -purpose sslclient \ -x509_strict \ -show_chain \ -CAfile backend-client-ca-bundle.pem \ client.crt

The expected result is client.crt: OK. Common errors include:

  • unable to get local issuer certificate: The issuing CA or an intermediate CA is missing from the backend's trust bundle.
  • unsupported certificate purpose: The client certificate is missing the clientAuth extended key usage.
  • certificate has expired: The client certificate is outside its validity period.
  • invalid CA certificate: An issuing certificate is missing valid CA extensions.
  • key usage does not include certificate signing: An issuing CA does not have the keyCertSign key usage.

Confirm that the certificate and private key contain the same public key:

TerminalCode
CERT_KEY_HASH=$(openssl x509 -in client.crt -noout -pubkey | openssl sha256) PRIVATE_KEY_HASH=$(openssl pkey -in client.key -pubout | openssl sha256) test "$CERT_KEY_HASH" = "$PRIVATE_KEY_HASH" && echo "Certificate and private key match" || echo "ERROR: Certificate and private key do not match"

If your backend still rejects the certificate, verify:

  • The certificate is signed by a CA that your backend trusts
  • The certificate hasn't expired
  • The certificate has the required key usage and extended key usage
  • The certificate and private key match
  • The certificate name in your code matches the uploaded certificate name

Connection Failures

If requests fail to connect:

  • Ensure your backend is configured to accept mTLS connections
  • Verify the certificate is uploaded to the correct environment (development, preview, production)
  • Check that your backend's CA certificate is properly configured

Local Certificates Fail to Load

If zuplo dev starts without reporting a loaded certificate, check the startup output for one of these warnings:

  • "Local mTLS client certificates are only supported for managed-dedicated projects" — the project resolves to managed-edge. If zuplo.jsonc has no projectType field, run zuplo link to add it and start the server again. If the field says managed-edge, local mTLS isn't available; see Local Development.
  • "Ignoring mTLS certificate directory … because it is missing tls.key" — a subdirectory has only one of the two files. Add the missing file — each certificate directory needs both tls.crt and tls.key.
  • "Ignoring --mtls-certs: … are set in .env or .env.zuplo" — a previous manual setup left __ZUPLO_MTLS_CLIENT_CERT_NAMES or __ZUPLO_MTLS_CLIENT_CERT_BASE_DIR in an env file. Those values take precedence in the local runtime. Remove them from .env and .env.zuplo to use --mtls-certs or .zuplo-local/mtls.
  • "Found local mTLS certificates directory … but it does not contain any valid certificate entries" — the certificates are loose files rather than in a <cert-name>/ subdirectory.

If no warning appears at all, confirm the directory is .zuplo-local/mtls (not .zuplo/mtls) at the project root, next to zuplo.jsonc.

Runtime Errors

If you see errors about missing certificates:

  • Confirm the certificate was uploaded successfully using zuplo mtls-certificate list
  • Ensure the environment type was specified correctly during upload
  • Verify your code references the correct certificate name

Additional Resources

For more information on securing your backend, see:

  • Securing your Backend - Overview of all backend security options
  • Shared Secret / API Key - Alternative approach using shared secrets
  • Secure Tunnels - Connect to private backends without exposing them to the internet
  • Client mTLS Authentication - The reverse direction, where clients authenticate to your Zuplo gateway with a client certificate

If you need assistance configuring mTLS for your project, contact us at support@zuplo.com.

Edit this page
Last modified on July 28, 2026
Securing your backendConnect to an AWS ALB with mTLS
On this page
  • How mTLS Works
  • Prerequisites
  • Client certificate requirements
  • 1/ Upload Your Certificate
  • 2/ Use the Certificate in Your Code
    • Using mTLS in a Request Handler
    • Using mTLS in a Policy
  • 3/ Using Environment Variables
  • Managing Certificates
    • Listing Certificates
    • Inspecting a Certificate
    • Changing Environment Types
    • Deleting Certificates
    • Certificate Rotation
  • Local Development
    • Add certificates to your project
    • Load certificates from another directory
    • Choose the right certificate for local runs
  • Troubleshooting
    • Certificate Validation Errors
    • Connection Failures
    • Local Certificates Fail to Load
    • Runtime Errors
  • Additional Resources
TypeScript
JSON
TypeScript
JSON