Home/Blog/mTLS (Mutual TLS): Client Certificate Authentication Guide
Cybersecurity

mTLS (Mutual TLS): Client Certificate Authentication Guide

Learn how to implement mutual TLS authentication for zero-trust security. Covers CA setup, certificate generation, server configuration, and service mesh integration.

By Inventive HQ Team
mTLS (Mutual TLS): Client Certificate Authentication Guide

Mutual TLS (mTLS) provides cryptographic authentication for both client and server, making it the gold standard for service-to-service communication in zero-trust architectures. This guide covers everything you need to implement mTLS in production.

How mTLS Works

┌─────────────────────────────────────────────────────────────────┐
│                    REGULAR TLS vs mTLS                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  REGULAR TLS (One-way)                                          │
│  ─────────────────────                                          │
│  ┌──────────┐                           ┌──────────┐            │
│  │  Client  │ ─── 1. ClientHello ────►  │  Server  │            │
│  │          │ ◄── 2. ServerHello ─────  │          │            │
│  │    ???   │ ◄── 3. Server Cert ─────  │  [CERT]  │            │
│  │          │ ─── 4. Key Exchange ───►  │          │            │
│  │          │ ◄──► 5. Encrypted ◄────►  │          │            │
│  └──────────┘                           └──────────┘            │
│  Client is anonymous                    Server proves identity  │
│                                                                 │
│  MUTUAL TLS (Two-way)                                           │
│  ────────────────────                                           │
│  ┌──────────┐                           ┌──────────┐            │
│  │  Client  │ ─── 1. ClientHello ────►  │  Server  │            │
│  │  [CERT]  │ ◄── 2. ServerHello ─────  │  [CERT]  │            │
│  │          │ ◄── 3. Server Cert ─────  │          │            │
│  │          │ ◄── 4. CertRequest ─────  │  (new!)  │            │
│  │          │ ─── 5. Client Cert ────►  │          │            │
│  │          │ ─── 6. Key Exchange ───►  │          │            │
│  │          │ ◄──► 7. Encrypted ◄────►  │          │            │
│  └──────────┘                           └──────────┘            │
│  Client proves identity                 Server proves identity  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

When to Use mTLS

Use CasemTLS RecommendedAlternative
Service-to-service (internal)✅ YesAPI keys (lower security)
Zero-trust architecture✅ YesNetwork segmentation (less secure)
Service mesh communication✅ Yes (usually automatic)-
High-security APIs (finance, health)✅ YesOAuth + strong auth
Public API with many clients❌ Usually notOAuth 2.0 / API keys
Browser-based applications❌ No (poor UX)OAuth 2.0 / sessions
Mobile apps⚠️ SometimesOAuth + certificate pinning

Setting Up a Certificate Authority

For production, use an existing CA (HashiCorp Vault, AWS Private CA, internal PKI). For development/testing:

Create Root CA

# Generate CA private key
openssl genrsa -out ca.key 4096

# Generate self-signed CA certificate
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt \
  -subj "/C=US/ST=California/O=MyOrg/CN=MyOrg Root CA"

Generate Server Certificate

# Generate server private key
openssl genrsa -out server.key 4096

# Create server CSR
openssl req -new -key server.key -out server.csr \
  -subj "/C=US/ST=California/O=MyOrg/CN=api.example.com"

# Create config for SAN (Subject Alternative Names)
cat > server.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = api.example.com
DNS.2 = *.api.example.com
IP.1 = 10.0.0.1
EOF

# Sign server certificate with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 365 -sha256 \
  -extfile server.ext

Generate Client Certificate

# Generate client private key
openssl genrsa -out client.key 4096

# Create client CSR
openssl req -new -key client.key -out client.csr \
  -subj "/C=US/ST=California/O=MyOrg/CN=service-a"

# Create config for client certificate
cat > client.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = clientAuth
EOF

# Sign client certificate with CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out client.crt -days 365 -sha256 \
  -extfile client.ext

Server Configuration

Nginx

server {
    listen 443 ssl;
    server_name api.example.com;

    # Server certificate
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Client certificate verification
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;  # Require client certs (use 'optional' to allow but not require)
    ssl_verify_depth 2;

    # Access client cert info in application
    location / {
        proxy_pass http://backend;
        proxy_set_header X-Client-DN $ssl_client_s_dn;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
    }
}

Apache

<VirtualHost *:443>
    ServerName api.example.com

    # Server certificate
    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/server.crt
    SSLCertificateKeyFile /etc/apache2/ssl/server.key

    # Client certificate verification
    SSLCACertificateFile /etc/apache2/ssl/ca.crt
    SSLVerifyClient require
    SSLVerifyDepth 2

    # Make client cert info available to application
    SSLOptions +StdEnvVars +ExportCertData
</VirtualHost>

Node.js

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  ca: fs.readFileSync('ca.crt'),
  requestCert: true,
  rejectUnauthorized: true  // Reject invalid client certs
};

const server = https.createServer(options, (req, res) => {
  const cert = req.socket.getPeerCertificate();

  if (req.client.authorized) {
    console.log(`Client authenticated: ${cert.subject.CN}`);
    res.writeHead(200);
    res.end(`Hello ${cert.subject.CN}!\n`);
  } else {
    res.writeHead(401);
    res.end('Client certificate required\n');
  }
});

server.listen(443);

Client Configuration

cURL

curl --cert client.crt --key client.key --cacert ca.crt \
  https://api.example.com/endpoint

Python (requests)

import requests

response = requests.get(
    'https://api.example.com/endpoint',
    cert=('client.crt', 'client.key'),
    verify='ca.crt'
)

Node.js

const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'api.example.com',
  port: 443,
  path: '/endpoint',
  method: 'GET',
  key: fs.readFileSync('client.key'),
  cert: fs.readFileSync('client.crt'),
  ca: fs.readFileSync('ca.crt')
};

const req = https.request(options, (res) => {
  // Handle response
});
req.end();

Go

cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
if err != nil {
    log.Fatal(err)
}

caCert, err := ioutil.ReadFile("ca.crt")
if err != nil {
    log.Fatal(err)
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            Certificates: []tls.Certificate{cert},
            RootCAs:      caCertPool,
        },
    },
}

resp, err := client.Get("https://api.example.com/endpoint")

Service Mesh Integration

Istio (Kubernetes)

# Enable strict mTLS for namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: my-namespace
spec:
  mtls:
    mode: STRICT
---
# Destination rule to use mTLS
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: default
  namespace: my-namespace
spec:
  host: "*.my-namespace.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

Linkerd

# Linkerd enables mTLS by default for meshed services
# Annotate namespace to enable automatic injection
apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  annotations:
    linkerd.io/inject: enabled

Certificate Rotation Strategy

┌─────────────────────────────────────────────────────────────────┐
│                  CERTIFICATE ROTATION TIMELINE                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Day 0        Day 30       Day 60       Day 90 (Expiry)        │
│  │            │            │            │                       │
│  ├────────────┼────────────┼────────────┤                       │
│  │  Cert A valid (90 days)              │                       │
│  │                                       │                       │
│  │            ├────────────────────────────────────┤            │
│  │            │  Cert B issued (90 days)           │            │
│  │            │  (30-day overlap)                  │            │
│  │                                                              │
│  Actions:                                                       │
│  Day 0:  Issue Cert A, deploy to clients                       │
│  Day 30: Issue Cert B, server trusts both CAs                  │
│  Day 45: Roll out Cert B to clients                            │
│  Day 60: Remove Cert A trust from servers                      │
│  Day 90: Cert A expires (already not in use)                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Troubleshooting

Test mTLS Connection

# Test with OpenSSL
openssl s_client -connect api.example.com:443 \
  -cert client.crt \
  -key client.key \
  -CAfile ca.crt

# Expected: "Verify return code: 0 (ok)"

Common Errors

ErrorCauseSolution
"certificate verify failed"Client cert not trusted by serverEnsure server has correct CA cert
"no certificate returned"Client not sending certCheck client config includes cert/key
"key values mismatch"Cert and key don't matchRegenerate cert or use matching pair
"certificate has expired"Certificate past validityIssue new certificate
"unable to get local issuer certificate"Missing intermediate CAInclude full chain

Security Best Practices

  • Use 4096-bit RSA or P-384 ECDSA keys for CA
  • Limit certificate validity (90 days for clients, shorter in high-security)
  • Implement proper certificate revocation (CRL or OCSP)
  • Use unique certificates per service instance (not shared)
  • Rotate CA certificates before expiry (plan 6+ months ahead)
  • Monitor certificate expiration with automated alerts
  • Store private keys securely (HSMs for CA keys)

Next Steps

Need Expert Cybersecurity Guidance?

Our team of security experts is ready to help protect your business from evolving threats.