TLS/SSL Fundamentals
Understand how TLS protects data in transit, learn about certificate types, the TLS handshake, and common misconfigurations to avoid.
Transport Layer Security (TLS) is the protocol that secures HTTPS, protecting data as it travels between clients and servers. Every time you see the padlock icon in your browser, TLS is encrypting your connection using the cryptographic primitives we covered in previous sections.
TLS is the successor to SSL (Secure Sockets Layer). While "SSL" is still commonly used colloquially, all SSL versions are deprecated and insecure. Modern systems use TLS 1.2 or TLS 1.3.
How TLS Works: The Big Picture
TLS provides three security properties:
| Property | How TLS Achieves It |
|---|---|
| Confidentiality | Symmetric encryption (AES-GCM) |
| Integrity | HMAC / AEAD authentication |
| Authentication | Certificates + asymmetric crypto |
The TLS handshake establishes these properties before any application data is exchanged.
The TLS 1.3 Handshake
TLS 1.3 simplified the handshake from 2 round-trips to 1:
Client Server
|
| 1. ClientHello
| - Supported TLS versions
| - Supported cipher suites
| - Client random
| - Key share (ECDHE public key)
|-------------------------------------------------->
|
| 2. ServerHello + EncryptedExtensions +
| Certificate + CertificateVerify + Finished
| - Selected cipher suite
| - Server random
| - Key share (ECDHE public key)
| - Certificate chain
| - Signature proving key ownership
|<--------------------------------------------------
|
| 3. Finished
| - Client confirmation
|-------------------------------------------------->
|
| [Application Data - Encrypted]
|<------------------------------------------------->
What Happens During the Handshake
- Key Exchange: Client and server use ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) to derive a shared secret without transmitting it
- Authentication: Server presents certificate; client verifies it chains to a trusted CA
- Session Keys: Both sides derive symmetric encryption keys from the shared secret
- Encryption: All subsequent data encrypted with AES-GCM or ChaCha20-Poly1305
TLS Versions
| Version | Status | Notes |
|---|---|---|
| TLS 1.3 | Recommended | Fastest, most secure, 1-RTT handshake |
| TLS 1.2 | Acceptable | Still secure with proper configuration |
| TLS 1.1 | Deprecated | Disabled in modern browsers |
| TLS 1.0 | Deprecated | Vulnerable to BEAST, POODLE |
| SSL 3.0 | Broken | Never use |
| SSL 2.0 | Broken | Never use |
Checking TLS Version
# Check what TLS version a server supports
openssl s_client -connect example.com:443 -tls1_3
# Test with specific version
curl -v --tls-max 1.2 https://example.com
# Using nmap for comprehensive scan
nmap --script ssl-enum-ciphers -p 443 example.com
Cipher Suites
A cipher suite defines the algorithms used for each TLS function:
TLS_AES_256_GCM_SHA384
| | | |
| | | +-- Hash for key derivation
| | +------- Authentication mode (GCM = AEAD)
| +------------ Key size (256-bit)
+----------------- Encryption algorithm (AES)
Recommended Cipher Suites (TLS 1.3)
TLS 1.3 only allows secure cipher suites:
TLS_AES_256_GCM_SHA384 # AES-256 with GCM
TLS_AES_128_GCM_SHA256 # AES-128 with GCM
TLS_CHACHA20_POLY1305_SHA256 # ChaCha20 (mobile-friendly)
TLS 1.2 Cipher Suite Configuration
# Nginx: Strong TLS 1.2 configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
Certificates
Certificates are digital documents that bind a public key to an identity. They are signed by Certificate Authorities (CAs) to establish trust.
Certificate Contents
# View certificate details
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | \
openssl x509 -text -noout
# Key information shown:
# - Subject: CN=example.com (who the cert is for)
# - Issuer: CN=Let's Encrypt (who signed it)
# - Validity: Not Before / Not After dates
# - Public Key: RSA or ECDSA public key
# - Signature: CA's signature proving authenticity
# - Extensions: SANs, key usage, etc.
Certificate Types
| Type | Validation | Use Case |
|---|---|---|
| DV (Domain Validated) | Proves domain control | Most websites, APIs |
| OV (Organization Validated) | Proves org identity | Business sites |
| EV (Extended Validation) | Extensive org verification | Banks, high-trust sites |
| Wildcard | *.example.com |
Multiple subdomains |
| SAN/Multi-domain | Multiple domains in one cert | Consolidated services |
Let's Encrypt (Free DV Certificates)
# Using certbot
sudo certbot certonly --nginx -d example.com -d www.example.com
# Auto-renewal (usually configured automatically)
sudo certbot renew --dry-run
# Certificate location
ls /etc/letsencrypt/live/example.com/
# cert.pem - Your certificate
# chain.pem - Intermediate certificates
# fullchain.pem - cert.pem + chain.pem (use this)
# privkey.pem - Private key (keep secret!)
Certificate Chain of Trust
Certificates form a chain from your certificate to a trusted root CA:
[Root CA Certificate] (Trusted by browsers/OS)
|
| signs
v
[Intermediate CA Certificate]
|
| signs
v
[Your Server Certificate]
Verifying the Chain
# Check certificate chain
openssl s_client -connect example.com:443 -showcerts </dev/null
# Verify chain manually
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
-untrusted intermediate.crt server.crt
Common TLS Configurations
Nginx
server {
listen 443 ssl http2;
server_name example.com;
# Certificates
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Protocol versions
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites (TLS 1.2)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off; # Let client choose (TLS 1.3)
# Session caching (performance)
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off; # Better forward secrecy
# OCSP Stapling (faster validation)
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
}
Apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
SSLHonorCipherOrder off
Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>
Kubernetes Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
# Use cert-manager for automatic certificates
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Force HTTPS
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# HSTS
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Strict-Transport-Security "max-age=63072000" always;
spec:
tls:
- hosts:
- app.example.com
secretName: app-tls-secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
Certificate Management
Generating CSRs (Certificate Signing Requests)
# Generate private key and CSR
openssl req -new -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.csr \
-subj "/CN=example.com/O=My Company/C=US"
# View CSR contents
openssl req -in server.csr -text -noout
# Generate with Subject Alternative Names (SANs)
openssl req -new -newkey rsa:2048 -nodes \
-keyout server.key -out server.csr \
-subj "/CN=example.com" \
-addext "subjectAltName=DNS:example.com,DNS:www.example.com,DNS:api.example.com"
Self-Signed Certificates (Development Only)
# Generate self-signed certificate (NOT for production)
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout dev.key \
-out dev.crt \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Certificate Rotation
from datetime import datetime, timedelta
import ssl
import socket
def check_certificate_expiry(hostname: str, port: int = 443) -> dict:
"""Check certificate expiration date."""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# Parse expiration date
not_after = datetime.strptime(
cert['notAfter'],
'%b %d %H:%M:%S %Y %Z'
)
days_remaining = (not_after - datetime.utcnow()).days
return {
'hostname': hostname,
'expires': not_after.isoformat(),
'days_remaining': days_remaining,
'needs_renewal': days_remaining < 30
}
# Example: Monitor certificate expiry
domains = ['example.com', 'api.example.com']
for domain in domains:
result = check_certificate_expiry(domain)
if result['needs_renewal']:
print(f"WARNING: {domain} expires in {result['days_remaining']} days!")
mTLS (Mutual TLS)
Standard TLS only authenticates the server. Mutual TLS (mTLS) also authenticates the client, commonly used in:
- Service-to-service communication
- Zero Trust architectures
- API authentication
Standard TLS: Mutual TLS:
Client --> Server Client <--> Server
[cert] [cert] [cert]
Only server proves identity Both prove identity
mTLS in Service Mesh (Istio)
# Istio PeerAuthentication: Require mTLS for all services
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # Require mTLS, reject plaintext
Common TLS Misconfigurations
1. Expired Certificates
# Check expiration
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Set up monitoring/alerts for certificates expiring < 30 days
2. Missing Intermediate Certificates
# Test chain completeness
openssl s_client -connect example.com:443 </dev/null 2>&1 | \
grep -i "verify"
# Should show: Verify return code: 0 (ok)
# If not, you may be missing intermediate certs
3. Allowing Weak Protocols/Ciphers
# Test for weak protocols
nmap --script ssl-enum-ciphers -p 443 example.com | \
grep -E "SSLv|TLSv1.0|TLSv1.1"
# Use SSL Labs for comprehensive testing
# https://www.ssllabs.com/ssltest/
4. Missing HSTS Header
# Check for HSTS
curl -sI https://example.com | grep -i strict-transport
# Should return:
# Strict-Transport-Security: max-age=31536000; includeSubDomains
5. Certificate Hostname Mismatch
# Check certificate matches hostname
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | \
openssl x509 -noout -subject -ext subjectAltName
Testing TLS Configuration
SSL Labs
The gold standard for TLS testing: https://www.ssllabs.com/ssltest/
Aim for an A+ rating:
- TLS 1.2+ only
- Strong cipher suites
- HSTS enabled
- No vulnerabilities
testssl.sh
# Comprehensive local testing
git clone https://github.com/drwetter/testssl.sh.git
cd testssl.sh
./testssl.sh example.com
Summary
Key takeaways for TLS/SSL:
- Use TLS 1.3 when possible, TLS 1.2 minimum - disable all SSL and TLS 1.0/1.1
- Get certificates from Let's Encrypt - free, automated, trusted
- Include intermediate certificates - incomplete chains cause validation failures
- Monitor expiration dates - expired certs cause outages
- Enable HSTS - forces browsers to always use HTTPS
- Use mTLS for service-to-service - especially in zero trust environments
- Test with SSL Labs - aim for A+ rating
In the next section, we will explore PKI (Public Key Infrastructure) - the trust framework that makes certificates meaningful.
Found an issue?