Skip to content

Workers mTLS -- Mutual TLS Authentication

DodaTech 6 min read

In this tutorial, you'll learn about Workers mTLS. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Cloudflare Workers mTLS (Mutual TLS) allows Workers to authenticate to backend services using client certificates, enabling zero-trust communication where both sides verify each other's identity through the TLS handshake.

Why mTLS Matters

Standard TLS only verifies the server's identity to the client. The server has no cryptographic proof of who the client is. mTLS reverses this asymmetry by requiring the client to present a certificate during the handshake. For Workers communicating with internal APIs, databases, or partner services, mTLS ensures that only your authenticated Workers can access sensitive endpoints. Unlike API keys or JWTs which can be leaked or stolen, mTLS certificates are cryptographic credentials validated at the transport layer before any application data is exchanged. Combined with Cloudflare's network, mTLS-protected requests never traverse the public internet unprotected. This is essential for regulated workloads in finance, healthcare, and government, and aligns with Zero Trust security principles where no implicit trust is granted to any network.

Real-world use: A payment processing Worker calls a bank's Transaction API over mTLS. The bank's server rejects any connection that does not present a valid client certificate signed by an approved CA. The Worker's certificate is stored as a secret and rotated monthly.

mTLS Architecture

flowchart LR
    W[Worker] --> HS[TLS Handshake]
    HS --> SC[Server sends certificate]
    HS --> CC[Worker sends client cert]
    SC --> SV[Server verified]
    CC --> CV[Client verified]
    CV --> E[Encrypted channel]
    E --> API[Backend API]
    W --> CA[Certificate stored in Secrets]
    CA --> CC

    subgraph mTLS_Handshake
        direction LR
        H1[ClientHello] --> H2[ServerHello + Cert]
        H2 --> H3[ClientCert + KeyExchange]
        H3 --> H4[Secure Connection]
    end

    style HS fill:#f90,color:#fff
    style E fill:#2ecc71,color:#fff
    style CA fill:#3498db,color:#fff

The mTLS handshake adds one extra step to standard TLS: after the server presents its certificate, the client must present its own certificate signed by a CA that the server trusts. If either side's certificate is invalid or unsigned by a trusted CA, the connection is rejected.

Configuring mTLS with Wrangler

# Step 1: Generate a client certificate (if you don't have one)
openssl req -newkey rsa:2048 -nodes \
  -keyout client-key.pem \
  -out client-cert.pem \
  -days 365 \
  -subj "/CN=my-worker.example.com"

# Step 2: Upload the certificate and key as secrets
echo "-----BEGIN CERTIFICATE-----..." | \
  wrangler secret put CLIENT_CERT

echo "-----BEGIN RSA PRIVATE KEY-----..." | \
  wrangler secret put CLIENT_KEY

# Step 3: Upload the CA certificate that signed the server cert
echo "-----BEGIN CERTIFICATE-----..." | \
  wrangler secret put SERVER_CA_CERT

# Expected output for each secret:
#  ✔ Success! Uploaded secret CLIENT_CERT
#  ✔ Success! Uploaded secret CLIENT_KEY
#  ✔ Success! Uploaded secret SERVER_CA_CERT

mTLS credentials must be stored as Worker secrets, not environment variables, because certificates and private keys are sensitive. Wrangler encrypts secrets at rest and injects them only at runtime.

Making an mTLS Request

export default {
  async fetch(request, env) {
    const clientCert = env.CLIENT_CERT;
    const clientKey = env.CLIENT_KEY;
    const serverCA = env.SERVER_CA_CERT;

    const response = await fetch('https://internal-api.example.com/data', {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
      cf: {
        clientCert: {
          chain: clientCert,
          privateKey: clientKey
        },
        serverCA: serverCA
      }
    });

    if (!response.ok) {
      return new Response(`mTLS request failed: ${response.status}`, {
        status: response.status
      });
    }

    const data = await response.json();
    return new Response(JSON.stringify(data), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

// Expected output (success):
// {"status": "authenticated", "data": [...]}
//
// Expected output (if cert is rejected):
// mTLS request failed: 403

The cf.clientCert option tells the Workers runtime to use the provided certificate and key for the TLS handshake. The cf.serverCA option specifies a custom CA certificate to validate the server's certificate, overriding the default public CA trust store.

mTLS with Certificate Rotation

export default {
  async fetch(request, env) {
    const certAge = await env.KV.get('cert_rotated_at');
    const rotationNeeded = !certAge ||
      (Date.now() - new Date(certAge).getTime()) > 20 * 24 * 60 * 60 * 1000;

    if (rotationNeeded) {
      // In production, fetch new cert from a CA API
      const newCert = await fetch('https://ca.internal/rotate', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${env.CA_API_TOKEN}`
        }
      });

      if (newCert.ok) {
        const { cert, key } = await newCert.json();
        await env.KV.put('client_cert', cert);
        await env.KV.put('client_key', key);
        await env.KV.put('cert_rotated_at', new Date().toISOString());

        console.log('mTLS certificate rotated successfully');
      }
    }

    const activeCert = await env.KV.get('client_cert');
    const activeKey = await env.KV.get('client_key');

    const response = await fetch('https://internal-api.example.com/data', {
      cf: {
        clientCert: {
          chain: activeCert,
          privateKey: activeKey
        }
      }
    });

    return new Response(await response.text());
  }
};

// Expected behavior:
// First request: generates/rotates cert, stores in KV, uses it immediately
// Subsequent requests within 20 days: uses existing cert from KV
// After 20 days: rotates the certificate again

Certificate rotation is critical for mTLS security. This pattern uses a cron-triggered or on-demand rotation check. The active certificate and key are stored in KV (encrypted at the application level) and rotated before expiry. The Worker never holds a single long-lived credential.

Common Errors

Error Cause Fix
TLS error: certificate required Server requires client cert but none provided Ensure cf.clientCert is set with the correct certificate chain and private key
TLS error: unknown CA Server certificate signed by an untrusted CA Add the server's CA certificate via cf.serverCA in the fetch options
TLS error: certificate expired Client certificate has passed its expiry date Rotate the certificate before expiry; set up a cron trigger for rotation
Secret not found CLIENT_CERT or CLIENT_KEY secret is missing Verify secrets are uploaded with wrangler secret put and the binding name matches
Invalid PEM format Certificate or key is not properly formatted Ensure PEM files have -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- delimiters

Practice Questions

  1. What two properties must be set in the cf option for mTLS?
  2. Why should mTLS credentials be stored as secrets rather than environment variables?
  3. How does mTLS differ from standard one-way TLS?

FAQ

Can I use mTLS with any backend service from Workers?

Yes, as long as the backend service supports mutual TLS authentication. The backend must be configured to request and validate client certificates during the TLS handshake. Most cloud databases, API gateways, and enterprise services support mTLS.

Does mTLS add latency to requests?

The mTLS handshake adds negligible latency -- typically 10-50 milliseconds for the additional certificate exchange. Once the TLS session is established, data transfer has the same performance as standard TLS. For persistent connections, the handshake cost is amortized over many requests.

How do I generate a client certificate for Workers mTLS?

You can generate a self-signed certificate with OpenSSL for testing, but production deployments should use certificates signed by a trusted Certificate Authority. Many organizations use an internal PKI or a public CA that supports client certificates.

Summary

Workers mTLS enables certificate-based mutual authentication between Workers and backend services, ensuring both parties verify each other's identity at the transport layer. Configure client certificates and private keys as Worker secrets, then pass them via the cf.clientCert option in fetch requests. Implement certificate rotation to maintain security over time. mTLS is essential for zero-trust architectures, regulated industries, and any scenario where the identity of the calling Worker must be cryptographically proven. DodaTech uses mTLS to secure internal service-to-service communication in its cloud platform.

This guide is brought to you by the developers of Cloudflare, TLS and SSL, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro