Introduction#
TLS (Transport Layer Security) is the protocol that secures HTTPS connections. Most engineers treat it as a black box, but understanding the handshake, certificate chain validation, and session resumption helps diagnose TLS errors, tune performance, and make informed decisions about certificate management.
TLS Handshake (TLS 1.3)#
TLS 1.3 reduced the handshake to 1 round-trip (down from 2 in TLS 1.2):
1
2
3
4
5
6
7
8
9
10
11
12
Client Server
| |
|--- ClientHello -----------------> |
| (supported ciphers, key share) |
| |
| <-- ServerHello + Certificate --- |
| + CertificateVerify |
| + Finished |
| |
|--- Finished -------------------> |
| |
|<== Encrypted Application Data ===>|
TLS 1.2 required 2 round-trips before application data could flow. TLS 1.3’s 1-RTT handshake reduces connection establishment latency by ~50ms on typical internet connections.
Certificate Chain Validation#
A TLS certificate is only valid if the full chain to a trusted root CA can be constructed.
1
2
3
Root CA (pre-installed in OS/browser trust store)
└── Intermediate CA (signed by Root CA)
└── Server Certificate (signed by Intermediate CA)
1
2
3
4
5
6
7
8
9
10
11
# View a certificate chain
openssl s_client -connect example.com:443 -showcerts 2>/dev/null
# Verify a certificate against a CA bundle
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
# Inspect certificate details
openssl x509 -in server.crt -text -noout | grep -E "(Subject|Issuer|Not|DNS)"
# Check expiry
openssl x509 -enddate -noout -in server.crt
Servers must send the intermediate CA certificate — clients should not need to fetch it. Missing intermediate certificates cause validation failures in strict clients.
Certificate Types#
DV (Domain Validation): proves control of the domain (DNS or HTTP challenge). Automated via Let’s Encrypt. Appropriate for most applications.
OV (Organization Validation): verifies the organization’s legal existence. Used by businesses.
EV (Extended Validation): extensive verification. Was used for the green address bar (browsers removed this distinction).
Wildcard: *.example.com covers one level of subdomains. Does not cover *.sub.example.com.
SAN (Subject Alternative Name): multiple domains in one certificate. Required for multi-domain applications.
Let’s Encrypt Automation#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# certbot with nginx
certbot --nginx -d example.com -d www.example.com
# certbot with DNS challenge (for wildcard certs)
certbot certonly \
--dns-route53 \
-d example.com \
-d '*.example.com'
# Renew (run via cron or systemd timer)
certbot renew --quiet
# Check renewal status
certbot certificates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Python: load TLS cert in application
import ssl
import aiohttp
ssl_context = ssl.create_default_context()
ssl_context.load_cert_chain(
certfile="/etc/letsencrypt/live/example.com/fullchain.pem",
keyfile="/etc/letsencrypt/live/example.com/privkey.pem"
)
async def make_request():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com", ssl=ssl_context) as resp:
return await resp.json()
Session Resumption#
TLS handshakes are expensive (asymmetric crypto). Session resumption allows clients to resume without a full handshake.
TLS 1.3 Session Tickets: server issues a ticket after the handshake; client presents it on the next connection to skip the key exchange.
0-RTT Early Data: TLS 1.3 allows sending application data with the resumption message (before the server responds). This eliminates the handshake latency but is vulnerable to replay attacks.
1
2
3
4
5
6
7
8
9
# nginx: session resumption configuration
ssl_session_cache shared:SSL:10m; # 10MB shared cache (~40,000 sessions)
ssl_session_timeout 1d;
ssl_session_tickets on;
ssl_session_ticket_key /etc/nginx/ssl/ticket.key; # rotate this key regularly
# TLS 1.3 only (drop TLS 1.2 if you can)
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off; # not needed for TLS 1.3 (only one cipher suite)
Common TLS Issues#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# SSL_ERROR_HANDSHAKE_FAILURE: cipher mismatch or protocol version
openssl s_client -connect example.com:443 -tls1_2 # test TLS 1.2 specifically
# CERTIFICATE_VERIFY_FAILED: chain validation failure
# Check: is intermediate CA included?
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -text | grep -A1 "Issuer"
# Certificate expired
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -enddate
# SNI: required when multiple certs on same IP
openssl s_client -connect 203.0.113.10:443 -servername example.com
# Check what TLS version is negotiated
curl -v --tls-max 1.3 https://example.com 2>&1 | grep "SSL connection"
mTLS: Mutual Authentication#
Standard TLS authenticates the server. mTLS additionally authenticates the client.
1
2
3
4
5
6
7
8
9
10
# Client presenting its own certificate
import ssl
import httpx
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations("/etc/certs/ca.crt") # verify server
context.load_cert_chain("/etc/certs/client.crt", "/etc/certs/client.key") # authenticate self
async with httpx.AsyncClient(verify=context) as client:
response = await client.get("https://internal-service/api/data")
mTLS is the standard for service-to-service authentication in zero-trust architectures and service meshes (Istio, Linkerd).
Conclusion#
TLS 1.3 is faster and more secure than TLS 1.2 — enable it and drop older versions. Include intermediate certificates in your server configuration. Automate certificate renewal with certbot or cert-manager in Kubernetes. Enable session resumption in nginx/HAProxy to reduce handshake latency for repeat connections. Use mTLS for service-to-service authentication in internal networks.