Mutual TLS (mTLS): Client Certificates Step by Step

A practical guide to mutual TLS authentication using client certificates. Covers the mTLS handshake, certificate generation, and server configuration for nginx and Envoy.


What Is Mutual TLS?

Standard TLS is one-sided: the client verifies the server's certificate, but the server accepts any client. Mutual TLS (mTLS) adds a second check — the server also demands a certificate from the client and validates it against a trusted CA. Both sides prove their identity before any application data flows.

In a typical HTTPS connection:

  1. Client connects to server
  2. Server presents its certificate
  3. Client verifies the certificate against its trust store
  4. Encrypted channel established

With mTLS, two additional steps happen:

  1. Client connects to server
  2. Server presents its certificate
  3. Client verifies the server certificate
  4. Server requests the client's certificate
  5. Client presents its certificate, server verifies it against a trusted CA
  6. Encrypted channel established with both parties authenticated

Why mTLS Matters

Zero Trust Architecture

In zero trust, network location grants no implicit trust. A request from inside your VPC is treated the same as one from the public internet. mTLS enforces identity at the transport layer — every connection must present a valid, CA-signed certificate. No certificate, no connection.

Service Mesh Communication

Service meshes like Istio, Linkerd, and Consul Connect use mTLS by default for pod-to-pod traffic. Every sidecar proxy holds a short-lived certificate issued by the mesh's internal CA. This encrypts all east-west traffic and provides cryptographic identity for authorization policies.

API Authentication

Client certificates are an alternative to API keys or OAuth tokens for machine-to-machine communication. They are harder to steal (bound to a private key that never leaves the client), cannot be accidentally logged, and are validated at the TLS layer before your application code runs.

When to Use mTLS

  • Service-to-service communication in microservices
  • B2B API integrations where both parties need strong identity
  • IoT device authentication
  • Internal admin interfaces and dashboards
  • Database connections (PostgreSQL, MySQL both support client certs)

Step-by-Step: Setting Up mTLS

Step 1: Generate the CA Certificate

You need a CA that both the server and clients trust. You can generate one at getaCert.com using the CA option, or with openssl:

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

# Generate CA certificate (valid for 10 years)
openssl req -new -x509 -key ca.key -sha256 \
  -subj "/CN=My Internal CA/O=My Company" \
  -days 3650 -out ca.crt

Guard ca.key carefully. Anyone with this key can issue trusted client certificates.

Step 2: Generate the Server Certificate

The server certificate is a standard TLS cert. Generate one at getaCert.com or with openssl:

# Generate server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key \
  -subj "/CN=api.example.com" -out server.csr

# Sign with the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 365 -sha256 \
  -extfile <(echo "subjectAltName=DNS:api.example.com") \
  -out server.crt

Step 3: Generate a Client Certificate

Generate a CA-signed client certificate at getaCert.com, or create one manually:

# Generate client key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key \
  -subj "/CN=service-a/O=My Company" -out client.csr

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

The CN (Common Name) identifies the client. Your server can extract it from the validated certificate and use it for authorization.

Step 4: Verify the Certificates

Before configuring any server, confirm the chain is valid:

# Verify server cert against CA
openssl verify -CAfile ca.crt server.crt

# Verify client cert against CA
openssl verify -CAfile ca.crt client.crt

# Inspect client cert details
openssl x509 -in client.crt -text -noout

Both commands should print OK.

Configuring nginx for mTLS

Basic mTLS Configuration

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

    # Server certificate (standard TLS)
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Client certificate verification (mTLS)
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;

    # TLS settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        # Pass client identity to upstream
        proxy_set_header X-Client-CN $ssl_client_s_dn_cn;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_pass http://backend:8080;
    }
}

Key directives:

Directive Purpose
ssl_client_certificate CA cert used to verify client certificates
ssl_verify_client on Require a valid client cert (connection refused without one)
ssl_verify_client optional Request a client cert but allow connections without one
ssl_verify_depth Max depth of the certificate chain (default: 1)
$ssl_client_s_dn_cn Extracts the client certificate's Common Name
$ssl_client_verify Result of verification: SUCCESS, FAILED, or NONE

Optional Client Verification

For endpoints where some clients use certificates and others use API keys:

ssl_verify_client optional;

location /api/ {
    if ($ssl_client_verify != SUCCESS) {
        return 403;
    }
    proxy_pass http://backend:8080;
}

location /public/ {
    proxy_pass http://backend:8080;
}

Configuring Envoy for mTLS

Envoy is the data plane proxy behind Istio and many service meshes. Here is a listener configuration for mTLS:

static_resources:
  listeners:
    - name: mtls_listener
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8443
      filter_chains:
        - transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              require_client_certificate: true
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/ssl/server.crt
                    private_key:
                      filename: /etc/envoy/ssl/server.key
                validation_context:
                  trusted_ca:
                    filename: /etc/envoy/ssl/ca.crt
          filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                route_config:
                  virtual_hosts:
                    - name: backend
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route: { cluster: backend_service }

The critical field is require_client_certificate: true. Without it, Envoy accepts connections without a client cert.

Testing mTLS with curl

Successful Connection

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

All three flags are needed:

  • --cert — the client certificate to present
  • --key — the client's private key
  • --cacert — the CA to verify the server's certificate

Expected Failures

Without a client certificate:

curl --cacert ca.crt https://api.example.com/health
# curl: (56) OpenSSL SSL3 read bytes: sslv3 alert handshake failure

With an untrusted client certificate:

curl --cert wrong-client.crt --key wrong-client.key --cacert ca.crt \
  https://api.example.com/health
# curl: (56) OpenSSL SSL3 read bytes: tlsv1 alert certificate unknown

Both fail at the TLS layer. No HTTP request is ever sent.

Certificate Revocation

When a client certificate is compromised, you need to revoke it. Two mechanisms:

CRL (Certificate Revocation List)

Generate a CRL and configure nginx to check it:

openssl ca -gencrl -out crl.pem -config ca.cnf
ssl_crl /etc/nginx/ssl/crl.pem;

OCSP Stapling

For larger deployments, OCSP is more efficient than distributing CRL files. step-ca supports OCSP out of the box.

Common Pitfalls

Certificate chain issues. If the client cert is signed by an intermediate CA, the ssl_client_certificate file must contain the full chain (intermediate + root).

Clock skew. Certificates have notBefore and notAfter timestamps. Containers with incorrect system clocks will reject valid certificates. Use NTP.

Key format mismatch. Ensure keys are in PEM format. DER-encoded keys will cause silent failures.

Forgetting SAN for servers. Modern TLS libraries ignore the CN field for server identity. Always set Subject Alternative Names on server certificates.

Summary

mTLS provides cryptographic identity for both client and server. It is the foundation of zero-trust networking and is built into every major service mesh. The setup involves three certificates — CA, server, and client — and a server configured to verify clients against the CA.

For quick mTLS testing, generate a CA-signed client certificate at getaCert.com and test it against our mTLS test endpoint at getacert.com:8443. No signup, no waiting.


More in Guides