Introduction

Load balancer SSL certificate errors occur when TLS/SSL handshake fails between clients and the load balancer, or between the load balancer and backend servers, causing connection failures, security warnings, and service outages. These errors manifest as ERR_SSL_PROTOCOL_ERROR in browsers, curl SSL handshake failures, certificate validation errors, and cipher negotiation failures. Common causes include expired or revoked certificates, incomplete certificate chain (missing intermediate CA), certificate-domain name mismatch, TLS protocol version incompatibility (client/server support different versions), cipher suite mismatch (no common cipher negotiated), SNI (Server Name Indication) misconfiguration, certificate rotation failures, OCSP stapling errors, client certificate authentication failures, and certificate key mismatch. The fix requires diagnosing whether the issue is certificate validity, chain configuration, protocol/cipher compatibility, or infrastructure-related. This guide provides production-proven troubleshooting for load balancer SSL certificate errors across AWS ALB, NGINX, HAProxy, and Kubernetes ingress controllers.

Symptoms

  • Browser shows ERR_SSL_PROTOCOL_ERROR or ERR_CERT_AUTHORITY_INVALID
  • curl fails with "SSL certificate problem: unable to get local issuer certificate"
  • Load balancer health checks fail with SSL handshake error
  • Backend connections fail with certificate validation error
  • Certificate expiration warnings in monitoring
  • TLS handshake timeout or failure
  • Cipher suite negotiation failures
  • SNI routing not working for multiple domains
  • OCSP stapling causing connection delays
  • Client certificate authentication failing unexpectedly

Common Causes

  • Certificate expired or expiring soon
  • Certificate chain incomplete (missing intermediate)
  • Certificate CN/SAN doesn't match domain name
  • TLS protocol version not supported (TLS 1.0/1.1 deprecated)
  • Cipher suite configuration too restrictive or incompatible
  • SNI not configured for multiple certificates
  • Certificate private key mismatch
  • Backend using self-signed certificate not trusted by load balancer
  • Certificate rotation failed (new cert not deployed)
  • OCSP responder unreachable causing stapling timeout
  • Client certificate required but not provided
  • Certificate revoked (CRL/OCSP check failed)

Step-by-Step Fix

### 1. Diagnose SSL certificate issues

Check certificate status:

```bash # Check certificate expiration and details openssl s_client -connect lb.example.com:443 -servername lb.example.com </dev/null 2>&1 | \ openssl x509 -noout -dates -subject -issuer

# Output: # notBefore=Jan 1 00:00:00 2026 GMT # notAfter=Dec 31 23:59:59 2026 GMT # subject=CN=lb.example.com # issuer=C=US, O=Let's Encrypt, CN=R3

# Check certificate chain openssl s_client -connect lb.example.com:443 -showcerts </dev/null 2>&1 | \ grep -E "s:|i:|Certificate chain"

# Verify certificate chain openssl s_client -connect lb.example.com:443 -servername lb.example.com \ -CAfile /etc/ssl/certs/ca-certificates.crt </dev/null 2>&1 | \ grep "Verify return code"

# Verify return code: 0 (ok) = Success # Verify return code: 20 = Unable to get local issuer certificate # Verify return code: 21 = Unable to verify the first certificate ```

Check certificate expiration:

```bash # Check days until expiration openssl s_client -connect lb.example.com:443 </dev/null 2>&1 | \ openssl x509 -noout -enddate | \ awk -F= '{print $2}' | \ xargs -I {} bash -c 'echo "Expires: {}"; if [ $(date -d "{}" +%s) -lt $(date -d "+30 days" +%s) ]; then echo "WARNING: Certificate expires in less than 30 days!"; fi'

# Check multiple domains for domain in api.example.com www.example.com app.example.com; do echo -n "$domain: " echo | openssl s_client -connect $domain:443 2>/dev/null | \ openssl x509 -noout -enddate | \ awk -F= '{print $2}' done

# Certificate monitoring script #!/bin/bash CERT_FILE="/etc/ssl/certs/lb.crt" EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_FILE" | cut -d= -f2) EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))

if [ $DAYS_LEFT -lt 30 ]; then echo "CRITICAL: Certificate expires in $DAYS_LEFT days" # Send alert fi ```

Test SSL connection:

```bash # Full SSL handshake test with details openssl s_client -connect lb.example.com:443 -servername lb.example.com -tls1_2 </dev/null 2>&1 | \ grep -E "Protocol|Cipher|Verify"

# Expected output: # Protocol : TLSv1.2 # Cipher : ECDHE-RSA-AES256-GCM-SHA384 # Verify return code: 0 (ok)

# Test with curl curl -vI https://lb.example.com 2>&1 | grep -E "SSL|subject|issuer"

# Check certificate transparency logs # https://crt.sh/?q=example.com curl -s "https://crt.sh/?q=example.com&output=json" | jq '.[0]' ```

### 2. Fix certificate chain issues

Install complete certificate chain:

```bash # Certificate bundle should include: # 1. Server certificate (your domain) # 2. Intermediate certificate(s) (CA chain) # 3. Root certificate (usually in client trust store, not needed)

# Download intermediate certificate (Let's Encrypt example) curl -o intermediate.pem https://letsencrypt.org/certs/lets-encrypt-r3.pem

# Combine server cert with intermediate cat server.crt intermediate.pem > fullchain.pem

# Or use certbot to get full chain certbot certonly --standalone -d example.com # Full chain: /etc/letsencrypt/live/example.com/fullchain.pem ```

AWS ALB certificate configuration:

```bash # Import certificate to AWS ACM aws acm import-certificate \ --certificate fileb://fullchain.pem \ --private-key fileb://privkey.pem \ --certificate-chain fileb://intermediate.pem

# Or request certificate through ACM (recommended) aws acm request-certificate \ --domain-name example.com \ --subject-alternative-names www.example.com api.example.com \ --validation-method DNS

# Associate certificate with ALB aws elbv2 update-listener \ --listener-arn arn:aws:elasticloadbalancing:region:account:listener/app/alb-name/abc123/def456 \ --certificates CertificateArn=arn:aws:acm:region:account:certificate/cert-id

# Verify certificate is attached aws elbv2 describe-listeners \ --listener-arn <listener-arn> \ --query 'Listeners[0].Certificates' ```

NGINX certificate configuration:

```nginx # /etc/nginx/sites-available/example.com

server { listen 443 ssl http2; server_name example.com www.example.com;

# Full chain certificate (server + intermediate) ssl_certificate /etc/ssl/certs/example.com.fullchain.pem; ssl_certificate_key /etc/ssl/private/example.com.key;

# SSL configuration ssl_protocols TLSv1.2 TLSv1.3; 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;

# Session configuration ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;

# OCSP stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s;

# HSTS (after testing) add_header Strict-Transport-Security "max-age=63072000" always; }

# Test configuration nginx -t

# Reload after changes systemctl reload nginx ```

HAProxy certificate configuration:

```haproxy # /etc/haproxy/haproxy.cfg

global # Combine cert and key in one file for HAProxy # cat server.crt server.key > /etc/haproxy/certs/example.com.pem

ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

frontend https_front bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1

# SNI for multiple domains bind *:443 ssl crt /etc/haproxy/certs/example.com.pem crt /etc/haproxy/certs/api.example.com.pem alpn h2,http/1.1

# Route based on SNI acl host_example hdr(host) -i example.com use_backend example_backend if host_example

default_backend default_backend

# Test configuration haproxy -c -f /etc/haproxy/haproxy.cfg

# Reload systemctl reload haproxy ```

### 3. Fix TLS protocol and cipher issues

Configure TLS versions:

```nginx # NGINX - Modern TLS configuration # /etc/nginx/conf.d/ssl.conf

# TLS 1.2 and 1.3 only (recommended) ssl_protocols TLSv1.2 TLSv1.3;

# TLS 1.3 cipher suites (separate from 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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers off;

# Test which protocols are supported nmap --script ssl-enum-ciphers -p 443 lb.example.com

# Or with openssl openssl s_client -connect lb.example.com:443 -tls1_2 </dev/null 2>&1 | grep Protocol openssl s_client -connect lb.example.com:443 -tls1_3 </dev/null 2>&1 | grep Protocol openssl s_client -connect lb.example.com:443 -tls1_1 </dev/null 2>&1 | grep -i error # Should fail ```

AWS ALB TLS policy:

```bash # Use AWS managed SSL policy (recommended) # TLS 1.2 policy aws elbv2 modify-listener \ --listener-arn <listener-arn> \ --ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01

# Available policies: # - ELBSecurityPolicy-TLS-1-2-2017-01 (TLS 1.2 only) # - ELBSecurityPolicy-TLS-1-2-Ext-2018-06 (TLS 1.2 extended) # - ELBSecurityPolicy-TLS-1-1-2017-01 (TLS 1.1+ - legacy) # - ELBSecurityPolicy-2016-08 (legacy, not recommended)

# Custom security policy aws elbv2 create-ssl-policy \ --name Custom-TLS-Policy \ --protocols TLSv1.2 TLSv1.3 \ --ciphers ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256

# Apply custom policy aws elbv2 modify-listener \ --listener-arn <listener-arn> \ --ssl-policy Custom-TLS-Policy ```

Test cipher suite compatibility:

```bash # Test specific cipher openssl s_client -connect lb.example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384' </dev/null 2>&1 | grep Cipher

# List all supported ciphers nmap --script ssl-enum-ciphers -p 443 lb.example.com

# Output shows: # TLSv1.2: # ciphers: # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (A) # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (A) # protocols: # TLSv1.2 (A)

# Grade indicates strength: # A = Strong # B = Acceptable # C = Weak # D = Insecure ```

### 4. Fix SNI configuration

Configure SNI for multiple domains:

```nginx # NGINX - Multiple SSL certificates with SNI

server { listen 443 ssl http2; server_name example.com www.example.com;

ssl_certificate /etc/ssl/certs/example.com.fullchain.pem; ssl_certificate_key /etc/ssl/private/example.com.key;

location / { proxy_pass http://backend; } }

server { listen 443 ssl http2; server_name api.example.com;

ssl_certificate /etc/ssl/certs/api.example.com.fullchain.pem; ssl_certificate_key /etc/ssl/private/api.example.com.key;

location / { proxy_pass http://api-backend; } }

# Test SNI openssl s_client -connect lb.example.com:443 -servername example.com </dev/null 2>&1 | grep "subject=" openssl s_client -connect lb.example.com:443 -servername api.example.com </dev/null 2>&1 | grep "subject=" ```

AWS ALB multiple certificates:

```bash # ALB supports multiple certificates on same listener with SNI

# Add additional certificate to listener aws elbv2 add-listener-certificates \ --listener-arn <listener-arn> \ --certificates CertificateArn=arn:aws:acm:region:account:certificate/cert-id-2

# View all certificates on listener aws elbv2 describe-listener-certificates \ --listener-arn <listener-arn>

# Default certificate is used when SNI not provided # Additional certificates matched by SNI domain

# Remove certificate aws elbv2 remove-listener-certificates \ --listener-arn <listener-arn> \ --certificates CertificateArn=arn:aws:acm:region:account:certificate/cert-id-2 ```

### 5. Fix certificate rotation

Automated certificate renewal with certbot:

```bash # Install certbot apt-get install certbot python3-certbot-nginx

# Obtain certificate certbot --nginx -d example.com -d www.example.com

# Auto-renewal (certbot installs cron job automatically) # Check cron job cat /etc/cron.d/certbot

# Test renewal certbot renew --dry-run

# Force renewal certbot renew --force-renewal

# Hook to reload nginx after renewal certbot renew --deploy-hook "systemctl reload nginx"

# Or create renewal hook script cat > /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh << 'EOF' #!/bin/bash systemctl reload nginx EOF chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh ```

AWS ACM auto-renewal:

```bash # ACM certificates auto-renew when domain validation is current

# Check certificate renewal status aws acm describe-certificate \ --certificate-arn arn:aws:acm:region:account:certificate/cert-id \ --query 'Certificate.{Status:Status,NotAfter:NotAfter,Type:Type}'

# Status values: # - PENDING_VALIDATION: Need to re-validate # - ISSUED: Active and valid # - EXPIRED: Expired, need new certificate # - INACTIVE: Validation failed # - REVOKED: Revoked # - FAILED: Issuance failed

# For DNS validation, ensure CNAME records still exist # ACM checks validation before auto-renewal

# If validation fails, re-create validation records aws acm list-domains-validation-options \ --certificate-arn <cert-arn>

# Create new validation records in Route53 aws route53 change-resource-record-sets \ --hosted-zone-id <zone-id> \ --change-batch file://validation-records.json ```

Kubernetes cert-manager rotation:

```yaml # Install cert-manager kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Create ClusterIssuer apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@example.com privateKeySecretRef: name: letsencrypt-prod-account-key solvers: - http01: ingress: class: nginx

# Create Certificate apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: example-com namespace: default spec: secretName: example-com-tls duration: 2160h # 90 days renewBefore: 360h # Renew 15 days before expiry subject: organizations: - Example Inc commonName: example.com dnsNames: - example.com - www.example.com issuerRef: name: letsencrypt-prod kind: ClusterIssuer

# cert-manager automatically renews before expiry # Check certificate status kubectl get certificate example-com -o yaml ```

### 6. Fix OCSP stapling issues

Configure OCSP stapling:

```nginx # NGINX OCSP stapling configuration server { listen 443 ssl;

ssl_certificate /etc/ssl/certs/example.com.fullchain.pem; ssl_certificate_key /etc/ssl/private/example.com.key;

# Enable OCSP stapling ssl_stapling on; ssl_stapling_verify on;

# Resolver for OCSP responder resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s;

# Trust CA certificates for OCSP response verification ssl_trusted_certificate /etc/ssl/certs/example.com.fullchain.pem; }

# Test OCSP stapling openssl s_client -connect example.com:443 -status -servername example.com </dev/null 2>&1 | \ grep -A 5 "OCSP response"

# Expected output: # OCSP response status: successful # Certificate status: good ```

Disable OCSP stapling if causing issues:

```nginx # If OCSP stapling causing problems, disable temporarily server { listen 443 ssl;

# Disable OCSP stapling ssl_stapling off; ssl_stapling_verify off;

# ... rest of config }

# Reload nginx systemctl reload nginx

# Check error logs for OCSP issues tail -f /var/log/nginx/error.log | grep -i stapling ```

### 7. Debug SSL handshake failures

Enable SSL debugging:

```nginx # NGINX SSL debug logging # Add to nginx.conf

error_log /var/log/nginx/error.log debug;

# Or specific SSL debug error_log /var/log/nginx/ssl_debug.log debug;

events { worker_connections 1024; }

http { # ... config ...

# After debugging, reduce log level # error_log /var/log/nginx/error.log warn; } ```

Capture SSL traffic:

```bash # Capture SSL handshake with tcpdump tcpdump -i any -s 0 -w ssl_handshake.pcap port 443

# Analyze with Wireshark wireshark ssl_handshake.pcap

# Look for: # - Client Hello: TLS version, cipher suites, SNI # - Server Hello: Selected TLS version, cipher suite # - Certificate: Server certificate chain # - Alert: Error codes (certificate_unknown, handshake_failure, etc.) # - RST flag: Connection reset

# Analyze alerts with tshark tshark -r ssl_handshake.pcap -Y "ssl.alert_message" -T fields -e ssl.alert_description ```

Common SSL errors:

```bash # SSL_ERROR_RX_RECORD_TOO_LONG # Cause: HTTP on HTTPS port or wrong protocol # Fix: Ensure using https:// not http://

# SSL_ERROR_ZERO_RETURN # Cause: Connection closed during handshake # Fix: Check firewall, proxy, load balancer configuration

# CERT_AUTHORITY_INVALID # Cause: Certificate from unknown CA # Fix: Install intermediate certificate or use trusted CA

# CERT_EXPIRED # Cause: Certificate past expiration date # Fix: Renew certificate immediately

# CERT_COMMON_NAME_INVALID # Cause: Domain doesn't match certificate CN/SAN # Fix: Use correct certificate for domain or add domain to SAN ```

Prevention

  • Set up certificate expiration monitoring with 30-day alerts
  • Use automated certificate management (certbot, ACM, cert-manager)
  • Implement certificate chain validation in CI/CD pipeline
  • Document certificate rotation procedures
  • Test certificate renewal in staging environment
  • Use TLS 1.2 and 1.3 only, disable legacy protocols
  • Configure OCSP stapling for improved certificate validation
  • Maintain certificate inventory with expiration dates
  • Use wildcard or multi-domain certificates where appropriate
  • Implement blue-green certificate rotation for zero downtime
  • **502 Bad Gateway**: Backend SSL certificate validation failed
  • **503 Service Unavailable**: SSL handshake failed with backend
  • **504 Gateway Timeout**: SSL connection timeout
  • **ERR_CERT_DATE_INVALID**: Certificate expired or not yet valid
  • **ERR_CERT_AUTHORITY_INVALID**: Certificate from untrusted CA