Certificate-Based Authentication with mTLS — Mutual TLS for API Security
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
- How does mTLS authenticate the client?
- What is the role of the CA in mTLS?
- How does Nginx pass client certificate information to the backend?
- Why should service names be validated in the certificate?
- How does certificate revocation work in mTLS?
Answers:
- 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.
- 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.
- Nginx sets headers (SSL_CLIENT_CERT, SSL_CLIENT_VERIFY, SSL_CLIENT_S_DN) that the backend reads to get certificate information.
- Any valid certificate from the CA can authenticate. Validating the CN ensures only authorized services (e.g., scanner-service) can access specific endpoints.
- 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
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