Workers mTLS -- Mutual TLS Authentication
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
- What two properties must be set in the
cfoption for mTLS? - Why should mTLS credentials be stored as secrets rather than environment variables?
- How does mTLS differ from standard one-way TLS?
FAQ
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