Introduction
Load balancer SSL handshake failures occur when the TLS negotiation between client and load balancer (or load balancer and backend) fails, causing connections to terminate before any HTTP traffic is exchanged. This error manifests as SSL_ERROR_RX_RECORD_TOO_LONG, ERR_SSL_VERSION_OR_CIPHER_MISMATCH, ssl_handshake_failure, or connection reset during TLS negotiation. Common causes include expired or revoked SSL certificate, incomplete certificate chain (missing intermediate CA), TLS protocol version mismatch (client supports only TLS 1.2, server requires TLS 1.3), no overlapping cipher suites between client and server, SNI (Server Name Indication) not sent by client or misconfigured on server, certificate hostname mismatch (CN/SAN doesn't match requested domain), OCSP stapling failures, certificate revoked and appearing on CRL, load balancer SSL policy too restrictive, and backend server certificate not trusted by load balancer. The fix requires systematic diagnosis of certificate validity, protocol compatibility, cipher suite overlap, and proper chain configuration. This guide provides production-proven troubleshooting for SSL handshake failures across AWS ALB/CLB/NLB, NGINX, HAProxy, Kubernetes ingress controllers, and cloud-native load balancing services.
Symptoms
- Browser shows
ERR_SSL_VERSION_OR_CIPHER_MISMATCH ssl_handshake_failurein load balancer access logsSSL_ERROR_RX_RECORD_TOO_LONGin FirefoxSSLV3_ALERT_HANDSHAKE_FAILUREin OpenSSL- Connection reset before HTTP response
- curl fails with
SSL certificate problem: unable to get local issuer certificate - curl fails with
error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure - Load balancer SSL negotiation timeout
- Backend connection fails with
certificate verify failed - TLS 1.3 clients cannot connect to TLS 1.2-only load balancer
Common Causes
- SSL certificate expired or not yet valid
- Certificate chain incomplete (missing intermediate certificate)
- TLS protocol version mismatch (client/server don't share common version)
- No common cipher suites between client and load balancer
- SNI not configured or client not sending SNI extension
- Certificate hostname mismatch (domain doesn't match CN/SAN)
- Self-signed certificate not trusted by client
- Certificate revoked (appears on CRL/OCSP)
- Load balancer SSL security policy too restrictive
- Backend certificate not trusted by load balancer (for HTTPS backends)
- MTU/path MTU issues causing TLS record fragmentation
- Load balancer SSL certificate not properly attached
Step-by-Step Fix
### 1. Diagnose SSL/TLS configuration
Test SSL handshake with OpenSSL:
```bash # Basic SSL connectivity test openssl s_client -connect lb.example.com:443 -servername lb.example.com
# Check certificate chain openssl s_client -connect lb.example.com:443 -showcerts
# Verify certificate openssl verify -CAfile /etc/ssl/certs/ca-bundle.crt server.crt
# Test specific TLS versions openssl s_client -connect lb.example.com:443 -tls1_2 openssl s_client -connect lb.example.com:443 -tls1_3 openssl s_client -connect lb.example.com:443 -tls1_1 # Should fail if disabled
# Test cipher suites openssl s_client -connect lb.example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384' openssl s_client -connect lb.example.com:443 -cipher 'AES128-SHA'
# Check what ciphers are supported nmap --script ssl-enum-ciphers -p 443 lb.example.com
# SSL Labs test (external) # https://www.ssllabs.com/ssltest/analyze.html?d=lb.example.com ```
Check certificate details:
```bash # Extract and analyze certificate echo | openssl s_client -connect lb.example.com:443 -servername lb.example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# Output to check: # subject=CN=lb.example.com # issuer=C=US, O=Let's Encrypt, CN=R3 # notBefore=Jan 1 00:00:00 2024 GMT # notAfter=Mar 31 23:59:59 2024 GMT # Check not expired # X509v3 Subject Alternative Name: # DNS:lb.example.com, DNS:www.lb.example.com # Check hostname matches
# Check certificate chain order # Should be: Server cert -> Intermediate -> Root echo | openssl s_client -connect lb.example.com:443 -showcerts 2>/dev/null \ | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ print }'
# Verify chain completeness echo | openssl s_client -connect lb.example.com:443 -showcerts 2>/dev/null \ | openssl verify -verbose ```
AWS ALB SSL configuration:
```bash # Check listener SSL policy aws elbv2 describe-listeners \ --load-balancer-arn arn:aws:elasticloadbalancing:region:account:loadbalancer/app/name/id
# Output shows: # { # "Listeners": [{ # "Protocol": "HTTPS", # "Port": 443, # "SslPolicy": "ELBSecurityPolicy-TLS-1-2-2017-01", # "Certificates": [{ # "CertificateArn": "arn:aws:acm:region:account:certificate/id", # "IsDefault": true # }] # }] # }
# Check certificate status in ACM aws acm describe-certificate --certificate-arn arn:aws:acm:region:account:certificate/id
# Look for: # - Status: ISSUED (not PENDING_VALIDATION or EXPIRED) # - InUseBy: shows load balancer ARN # - DomainValidationOptions: shows validation status
# List available SSL policies aws elbv2 describe-ssl-policies
# Common policies: # - ELBSecurityPolicy-TLS-1-3-1-2-2021-06 (Modern, TLS 1.2 + 1.3) # - ELBSecurityPolicy-TLS-1-2-2017-01 (Compatible, TLS 1.2 only) # - ELBSecurityPolicy-FS-1-2-2019-08 (Forward secrecy) # - ELBSecurityPolicy-FS-1-1-2019-08 (Includes TLS 1.1 for legacy) ```
### 2. Fix certificate chain issues
Upload complete certificate chain:
```bash # Certificate chain must include: # 1. Server certificate (your domain) # 2. Intermediate certificate(s) (CA chain) # 3. Root certificate (usually not needed, clients have it)
# Create bundled certificate file # Order matters: Server cert first, then intermediates cat server.crt intermediate.crt > bundle.crt
# Or in correct order (some CAs provide this) cat example_com.crt COMODORSAAddTrustCA.crt > bundle.crt
# AWS ACM - Upload certificate aws acm import-certificate \ --certificate fileb://bundle.crt \ --private-key fileb://private.key \ --certificate-chain fileb://intermediate.crt
# For ACM-requested certificates, chain is automatic # Only manual imports need explicit chain
# NGINX - Use bundled certificate server { listen 443 ssl; server_name lb.example.com;
ssl_certificate /etc/ssl/certs/bundle.crt; # Includes chain ssl_certificate_key /etc/ssl/private/private.key;
# Or separate chain file (NGINX 1.3+) # ssl_certificate /etc/ssl/certs/server.crt; # ssl_certificate_key /etc/ssl/private/private.key; # ssl_trusted_certificate /etc/ssl/certs/chain.crt; }
# HAProxy - Bundle certificate # HAProxy expects PEM with cert + key + chain in single file cat server.crt private.key intermediate.crt > haproxy.pem
# In haproxy.cfg: bind *:443 ssl crt /etc/haproxy/haproxy.pem ```
Fix intermediate certificate issues:
```bash # Download intermediate from CA # Let's Encrypt: https://letsencrypt.org/certificates/ # DigiCert: https://www.digicert.com/support/ # Comodo/Sectigo: https://support.sectigo.com/
# Let's Encrypt R3 intermediate curl -o intermediate.crt https://letsencrypt.org/certs/lets-encrypt-r3.pem
# Verify chain with openssl openssl verify -CAfile intermediate.crt -untrusted server.crt server.crt
# Should output: server.crt: OK
# If chain is broken, get correct intermediate from CA # Common issue: Using wrong intermediate for your CA ```
### 3. Fix TLS protocol mismatches
Update SSL security policy:
```bash # AWS ALB - Update SSL policy to support more protocols aws elbv2 modify-listener \ --listener-arn arn:aws:elasticloadbalancing:region:account:listener/xyz \ --ssl-policy ELBSecurityPolicy-TLS-1-3-1-2-2021-06
# Available policies and minimum TLS: # ELBSecurityPolicy-TLS-1-3-1-2-2021-06: TLS 1.2 + 1.3 (Modern) # ELBSecurityPolicy-TLS-1-2-2017-01: TLS 1.2 only # ELBSecurityPolicy-FS-1-2-2019-08: TLS 1.2+ with forward secrecy # ELBSecurityPolicy-FS-1-1-2019-08: TLS 1.1+ (legacy compatibility)
# For maximum compatibility (not recommended for security) # Use Custom SSL Policy aws elbv2 create-ssl-policy \ --name Custom-TLS-Policy \ --protocols TLSv1.2 TLSv1.3 \ --ciphers ECDHE-RSA-AES128-GCM-SHA256,ECDHE-RSA-AES256-GCM-SHA384
# NGINX - Configure TLS protocols server { listen 443 ssl;
# Modern configuration (TLS 1.2 + 1.3 only) ssl_protocols TLSv1.2 TLSv1.3;
# Or with TLS 1.1 for legacy clients (not recommended) # ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# TLS 1.3 ciphers (separate from TLS 1.2) ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
# TLS 1.2 ciphers 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 on; }
# HAProxy - Configure TLS versions global ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
# Modern cipher list ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
frontend https bind *:443 ssl crt /etc/haproxy/cert.pem alpn h2,http/1.1 ```
Test protocol compatibility:
```bash # Test which protocols are supported for version in TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; do echo -n "$version: " openssl s_client -connect lb.example.com:443 -$version 2>&1 | \ grep -E "Protocol|Cipher" | head -1 done
# Expected output for modern config: # TLSv1: (no match - disabled) # TLSv1.1: (no match - disabled) # TLSv1.2: Protocol : TLSv1.2, Cipher: ECDHE-RSA-AES256-GCM-SHA384 # TLSv1.3: Protocol : TLSv1.3, Cipher: TLS_AES_256_GCM_SHA384 ```
### 4. Fix cipher suite mismatches
Configure compatible ciphers:
bash
# AWS ALB - Ciphers are determined by SSL policy
# Check policy ciphers
aws elbv2 describe-ssl-policies --query 'SslPolicies[?Name==ELBSecurityPolicy-TLS-1-3-1-2-2021-06`].Ciphers'
# Output: # [ # {"Name": "TLS_AES_128_GCM_SHA256"}, # {"Name": "TLS_AES_256_GCM_SHA384"}, # {"Name": "TLS_CHACHA20_POLY1305_SHA256"}, # {"Name": "ECDHE-RSA-AES128-GCM-SHA256"}, # {"Name": "ECDHE-RSA-AES256-GCM-SHA384"} # ]
# If client doesn't support these ciphers, handshake fails # Solution: Use less restrictive policy or update client
# NGINX - Configure cipher suites server { listen 443 ssl;
# Modern secure ciphers (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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; ssl_prefer_server_ciphers off; # Let client choose
# For legacy compatibility (less secure) # ssl_ciphers 'HIGH:MEDIUM:!aNULL:!MD5:!RC4'; }
# Test cipher compatibility # List all ciphers a server supports nmap --script ssl-enum-ciphers -p 443 lb.example.com
# Test specific cipher openssl s_client -connect lb.example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384' ```
Diagnose cipher mismatch:
```bash # If handshake fails, check what ciphers client supports # Client-side testing
# List OpenSSL available ciphers openssl ciphers -v 'ALL'
# Test connection with each cipher for cipher in $(openssl ciphers 'HIGH:!aNULL:!MD5'); do echo -n "$cipher: " openssl s_client -connect lb.example.com:443 -cipher "$cipher" 2>&1 | \ grep -E "Cipher|error" | head -1 done
# Look for ciphers that complete handshake successfully # Compare with server's supported ciphers ```
### 5. Fix SNI configuration
SNI (Server Name Indication) requirements:
```bash # SNI is required when load balancer hosts multiple SSL certificates # Client must send hostname in TLS handshake
# Test without SNI (will fail if SNI required) openssl s_client -connect lb.example.com:443 </dev/null 2>&1 | \ grep -E "subject|error"
# Test with SNI (should succeed) openssl s_client -connect lb.example.com:443 -servername lb.example.com </dev/null 2>&1 | \ grep -E "subject|error"
# AWS ALB - SNI is automatic for multi-cert listeners # When multiple certs attached to same listener, ALB uses SNI
# Check listener certificates aws elbv2 describe-listener-certificates \ --listener-arn arn:aws:elasticloadbalancing:region:account:listener/xyz
# Output shows all certificates for listener # Default certificate used if SNI doesn't match any
# NGINX - SNI is automatic with multiple server blocks server { listen 443 ssl; server_name example.com; ssl_certificate /etc/ssl/certs/example.com.crt; }
server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/ssl/certs/www.example.com.crt; }
# Both can share same IP, SNI distinguishes them ```
SNI troubleshooting:
```bash # Legacy clients may not support SNI # Check client SNI support
# curl with SNI (default behavior) curl -v https://lb.example.com/
# curl without SNI (for testing) curl -v --resolve lb.example.com:443:1.2.3.4 https://lb.example.com/
# If certificate mismatch without SNI but works with SNI: # - Load balancer requires SNI # - Legacy clients need separate IP or certificate
# Solution for legacy clients: # 1. Use single certificate (no SNI needed) # 2. Assign dedicated IP for legacy clients # 3. Upgrade client to support SNI (TLS 1.2+) ```
### 6. Fix backend SSL verification
HTTPS backend certificate validation:
```bash # When load balancer connects to backend over HTTPS, # backend certificate must be valid and trusted
# AWS ALB - Backend protocol configuration aws elbv2 describe-target-groups \ --target-group-arn arn:aws:elasticloadbalancing:region:account:targetgroup/xyz
# Output: # { # "TargetGroups": [{ # "Protocol": "HTTPS", # "HealthCheckProtocol": "HTTPS", # "HealthCheckPath": "/health" # }] # }
# If backend uses self-signed cert, ALB needs to trust it # Option 1: Use ACM certificate on backend # Option 2: Disable backend verification (not recommended)
# NGINX - Backend SSL verification upstream backend { server backend1.example.com:443; }
server { location / { proxy_pass https://backend;
# Verify backend certificate proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt; proxy_ssl_verify_depth 2;
# Or disable verification (development only) # proxy_ssl_verify off; } }
# HAProxy - Backend SSL backend https_servers server backend1 10.0.1.1:443 ssl verify required ca-file /etc/ssl/certs/ca-bundle.crt
# Or skip verification (not recommended) # server backend1 10.0.1.1:443 ssl verify none ```
Backend certificate requirements:
```bash # Backend certificate must: # 1. Be valid (not expired) # 2. Chain to trusted CA (or add to load balancer trust store) # 3. Match backend hostname (CN/SAN)
# Generate self-signed cert for internal backend openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout backend.key -out backend.crt \ -subj "/CN=backend1.internal"
# Add to load balancer trust store # AWS: Import to ACM Private CA or use custom trust store # NGINX: Add to ca-bundle.crt cat backend.crt >> /etc/ssl/certs/ca-bundle.crt
# HAProxy: Reference in config backend https_servers server backend1 10.0.1.1:443 ssl verify required ca-file /etc/ssl/certs/custom-ca.crt ```
### 7. Debug SSL handshake failures
Enable SSL logging:
```bash # NGINX - SSL debug logging # Add to nginx.conf error_log /var/log/nginx/error.log debug;
# Or specific SSL debugging error_log /var/log/nginx/ssl_debug.log debug;
events { worker_connections 1024; }
http { # Log SSL variables log_format ssl_log '$remote_addr - $ssl_protocol/$ssl_cipher ' '"$request" $status $body_bytes_sent ' '"$ssl_server_name"';
access_log /var/log/nginx/ssl_access.log ssl_log; }
# HAProxy - SSL debugging global log /dev/log local0 debug
defaults log global
frontend https bind *:443 ssl crt /etc/haproxy/cert.pem capture request header Host len 64
# Log SSL handshake info log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r" ```
Analyze handshake failure:
```bash # OpenSSL verbose error output openssl s_client -connect lb.example.com:443 -debug -msg 2>&1 | \ grep -E "error|alert|handshake"
# Common error codes: # SSLV3_ALERT_HANDSHAKE_FAILURE: No common cipher or protocol # SSLV3_ALERT_CERTIFICATE_EXPIRED: Certificate expired # SSLV3_ALERT_CERTIFICATE_UNKNOWN: Certificate not trusted # SSLV3_ALERT_BAD_CERTIFICATE: Certificate validation failed
# TLS alert decoding # Alert format: level (warning=1, fatal=2) + description # 0x02 0x28 = Fatal + Handshake Failure (40)
# Wireshark capture for deep analysis # Filter: tls.handshake.type == 1 (Client Hello) # Filter: tls.alert_message (Alert messages) tcpdump -i any -s 0 -w ssl_capture.pcap host lb.example.com and port 443 ```
Prevention
- Use ACM or similar for automatic certificate renewal
- Monitor certificate expiration with 30-day alerts
- Test SSL configuration after any load balancer changes
- Use SSL Labs testing for security posture validation
- Document cipher suite and protocol requirements
- Implement certificate transparency monitoring
- Use OCSP stapling to improve revocation checking
- Maintain runbook for SSL certificate rotation
- Test legacy client compatibility before disabling old protocols
- Configure health checks to validate backend certificates
Related Errors
- **502 Bad Gateway**: Backend returned invalid response
- **503 Service Unavailable**: All backends unhealthy
- **504 Gateway Timeout**: Backend response timeout
- **CDN SSL certificate expired**: HTTPS failing at edge
- **mTLS authentication failed**: Client certificate rejected