Implementing mTLS for Zero Trust Services
When to Use
- When deploying or configuring implementing mtls for zero trust services capabilities in your environment
- When establishing security controls aligned to compliance requirements
- When building or improving security architecture for this domain
- When conducting security assessments that require this implementation
Common Misconfigurations & Verification
- Server accepts any client cert (no CERT_REQUIRED): with
verify_mode = CERT_NONE/CERT_OPTIONALthe handshake completes even with no client cert — that is TLS, not mTLS. Setcontext.verify_mode = ssl.CERT_REQUIREDandload_verify_locations(ca.pem). Confirm rejection:openssl s_client -connect svc:8443with no-certmust fail withpeer did not return a certificate. - Chain validated but identity not checked: verifying the cert is CA-signed is not enough — any service holding a CA-issued cert can connect. Enforce the expected SAN/CN per peer (
context.check_hostnameor an explicit SAN allowlist) so service A cannot present service B's valid cert. - Expired/near-expiry certs fail at runtime: short-lived service certs that aren't rotated cause hard outages. Audit
not_valid_afterand alert before expiry; check withopenssl x509 -enddate -noout -in svc.pem. - Revocation ignored: without CRL/OCSP a compromised-but-unexpired cert stays trusted. Confirm revoked certs are actually rejected, not just issued.
- Confirm both directions: test a valid client (success), a no-cert client (rejected), and a cert signed by a different CA —
openssl s_client -cert wrong.pemmust fail withunknown ca. Don't conclude mTLS works from one happy-path call.
Prerequisites
- Familiarity with security operations concepts and tools
- Access to a test or lab environment for safe execution
- Python 3.8+ with required dependencies installed
- Appropriate authorization for any testing activities
Instructions
Generate CA certificates, issue service certificates, and configure mutual TLS verification for service-to-service authentication.
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import datetime
# Generate CA key and certificate
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
ca_cert = (x509.CertificateBuilder()
.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Internal CA")]))
.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Internal CA")]))
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.sign(ca_key, hashes.SHA256()))
Examples
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_cert_chain("client.pem", "client-key.pem")
context.load_verify_locations("ca.pem")
context.verify_mode = ssl.CERT_REQUIRED