Skip to content

Certificate-Based Authentication with mTLS — Mutual TLS for API Security

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Certificate. We cover key concepts, practical examples, and best practices to help you master this topic.

mTLS (mutual TLS) authentication requires both the client and server to present valid X.509 certificates during the TLS handshake, providing cryptographic mutual authentication without shared secrets or tokens.

What You'll Learn

mTLS handshake mechanics, CA-signed and self-signed certificate chains, client certificate validation, implementing mTLS with Nginx and Python, and certificate revocation.

Why It Matters

mTLS provides the strongest authentication mechanism — cryptographic proof based on public key infrastructure. It eliminates shared secrets, resists phishing, and is ideal for machine-to-machine communication in zero-trust networks.

Real-World Use

Kubernetes uses mTLS for all pod-to-pod communication. AWS services use mTLS for internal API calls. Durga Antivirus Pro uses mTLS for communication between its scanning Microservices, ensuring only authenticated services can exchange threat data.

sequenceDiagram
    participant Client as Client
    participant Server as Server

    Client->>Server: ClientHello
    Server->>Client: ServerHello + Server Certificate
    Server->>Client: CertificateRequest
    Client->>Server: Client Certificate
    Server->>Server: Validate client cert
against CA + verify Server->>Client: Finished Client->>Server: Finished Note over Client,Server: Encrypted channel established
Both parties authenticated

Code Example: mTLS Server with Python

import ssl, os
from flask import Flask, request, jsonify

app = Flask(__name__)

def get_client_info():
    """Extract client certificate info from the request."""
    cert = request.environ.get("SSL_CLIENT_CERT")
    if not cert:
        return None

    # Parse certificate using OpenSSL
    from OpenSSL import crypto
    try:
        x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
        subject = dict(x509.get_subject().get_components())
        issuer = dict(x509.get_issuer().get_components())

        return {
            "cn": subject.get(b"CN", b"").decode(),
            "org": subject.get(b"O", b"").decode(),
            "issuer_cn": issuer.get(b"CN", b"").decode(),
            "serial": x509.get_serial_number(),
            "not_after": x509.get_notAfter().decode(),
            "fingerprint": x509.digest("sha256").decode()
        }
    except Exception as e:
        return {"error": str(e)}

@app.route("/api/v1/threats")
def list_threats():
    """Endpoint protected by mTLS."""
    client_info = get_client_info()
    if not client_info:
        return jsonify({"error": "Client certificate required"}), 401

    # Extract service name from certificate CN
    service_name = client_info.get("cn", "unknown")

    return jsonify({
        "threats": [],
        "authenticated_service": service_name,
        "cert_fingerprint": client_info["fingerprint"]
    })

@app.route("/api/v1/health")
def health():
    """Public endpoint — no mTLS required."""
    client_info = get_client_info()
    return jsonify({
        "status": "ok",
        "mtls_client": client_info["cn"] if client_info else "none"
    })

Running the server with mTLS:

# ssl_context.py
import ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(
    certfile="/etc/certs/server.crt",
    keyfile="/etc/certs/server.key"
)
context.load_verify_locations(cafile="/etc/certs/ca.crt")
context.verify_mode = ssl.CERT_REQUIRED  # Require client cert

# app.run with context
if __name__ == "__main__":
    app.run(ssl_context=context, host="0.0.0.0", port=8443)

Code Example: mTLS with Nginx Reverse Proxy

# /etc/nginx/sites-enabled/mtls-api.conf
server {
    listen 443 ssl;
    server_name api.durga-antivirus.com;

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

    # CA certificate for client verification
    ssl_client_certificate /etc/certs/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    # Pass client cert info to backend
    proxy_set_header SSL_CLIENT_CERT $ssl_client_escaped_cert;
    proxy_set_header SSL_CLIENT_VERIFY $ssl_client_verify;
    proxy_set_header SSL_CLIENT_S_DN $ssl_client_s_dn;
    proxy_set_header SSL_CLIENT_I_DN $ssl_client_i_dn;

    location / {
        proxy_pass http://localhost:5000;
    }

    location /api/public {
        # Allow requests without client cert
        ssl_verify_client optional_no_ca;
        proxy_pass http://localhost:5000;
    }
}

Expected curl test:

# With valid client cert
curl --cert client.crt --key client.key \
  --cacert ca.crt \
  https://api.durga-antivirus.com/api/v1/threats
# {"threats":[],"authenticated_service":"scanner-service"}

# Without client cert
curl --cacert ca.crt \
  https://api.durga-antivirus.com/api/v1/threats
# 400 No required SSL certificate was sent

Code Example: Client-Side mTLS in Python

import requests

class MTLSServiceClient:
    """Service client using mTLS for authentication."""

    def __init__(self, base_url, cert_path, key_path, ca_path):
        self.base_url = base_url
        self.cert = (cert_path, key_path)
        self.verify = ca_path

    def request(self, method, path, **kwargs):
        url = f"{self.base_url}{path}"
        return requests.request(
            method,
            url,
            cert=self.cert,
            verify=self.verify,
            **kwargs
        )

    def get(self, path, **kwargs):
        return self.request("GET", path, **kwargs)

    def post(self, path, **kwargs):
        return self.request("POST", path, **kwargs)


# Usage
threat_client = MTLSServiceClient(
    base_url="https://api.durga-antivirus.com",
    cert_path="/etc/certs/scanner.crt",
    key_path="/etc/certs/scanner.key",
    ca_path="/etc/certs/ca.crt"
)

resp = threat_client.get("/api/v1/threats")
print(resp.json())

Common Mistakes

1. Not Validating Certificate CN/SAN

Any valid certificate signed by the CA can authenticate. Validate the Common Name (CN) or Subject Alternative Name (SAN) against an allowlist of known services.

2. Self-Signed Certificates Without Internal CA

Using self-signed certs requires distributing each cert to every service. Use an internal CA to sign all service certificates, and distribute only the CA cert.

3. Ignoring Certificate Expiry

Client certificates expire. Monitor expiry dates and automate renewal. Expired certs cause authentication failures that are hard to debug.

4. Not Implementing Certificate Revocation

When a service is decommissioned or compromised, its certificate must be revoked. Use CRLs or OCSP to check revocation status.

5. Mixing Public and mTLS on Same Port

If the same port accepts both mTLS and plain TLS, an attacker can skip client certs. Use separate ports or require ssl_verify_client on.

Practice Questions

  1. How does mTLS authenticate the client?
  2. What is the role of the CA in mTLS?
  3. How does Nginx pass client certificate information to the backend?
  4. Why should service names be validated in the certificate?
  5. How does certificate revocation work in mTLS?

Answers:

  1. During the TLS handshake, the server sends a CertificateRequest. The client responds with its certificate. The server validates the certificate signature against the trusted CA.
  2. The CA signs client and server certificates. Both parties trust the CA's root certificate. The CA verifies the identity before signing, establishing a chain of trust.
  3. Nginx sets headers (SSL_CLIENT_CERT, SSL_CLIENT_VERIFY, SSL_CLIENT_S_DN) that the backend reads to get certificate information.
  4. Any valid certificate from the CA can authenticate. Validating the CN ensures only authorized services (e.g., scanner-service) can access specific endpoints.
  5. Use Certificate Revocation Lists (CRLs) or the Online Certificate Status Protocol (OCSP). Configure Nginx with ssl_crl or use OCSP stapling.

Challenge: Set up a complete mTLS infrastructure with an internal CA, signed client and server certificates, Nginx reverse proxy with client cert validation, and a Python backend that extracts and validates certificate information.

FAQ

Is mTLS the most secure authentication method?

mTLS is among the strongest. It provides cryptographic mutual authentication, resists phishing, and does not rely on shared secrets. It is the standard for zero-trust architectures.

Does mTLS work with API keys?

Yes. Use mTLS for transport-level authentication and API keys for application-level authorization. The API key identifies the specific integration within the authenticated service.

How do I generate client certificates?

Use your internal CA: openssl req -new -key client.key -out client.csr, then ca signs it: openssl ca -in client.csr -out client.crt.

What is the performance impact of mTLS?

The TLS handshake is ~2x slower due to client cert verification. Connection pooling and session resumption reduce the impact. For persistent connections, the overhead is negligible.

Can mTLS be used with mobile apps?

Technically yes, but distributing client certificates to mobile devices is complex. Use OAuth2 with PKCE for mobile clients.

How do I handle certificate rotation?

Issue new certificates with overlapping validity. Services can accept both old and new certs during the transition. Use short-lived certs (24 hours) with automatic renewal via ACME.

Mini Project

Build a complete mTLS setup: create an internal CA, generate and sign server and client certificates, configure Nginx for mTLS with certificate validation, and build a Python API that extracts and authorizes based on client certificate CN.

What's Next

Now learn about Authentication Middleware for Express for reusable auth components in Node.js APIs.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro