Introduction
SSL/TLS certificate errors occur when secure connections fail due to certificate validation problems, chain of trust issues, protocol mismatches, or cryptographic failures. These errors manifest as browser warnings, connection failures, or silent connection downgrades. Common causes include expired certificates, certificate chain incomplete or misordered, hostname mismatch (CN/SAN doesn't match domain), self-signed certificates not trusted by clients, intermediate CA certificates missing, TLS protocol version mismatch, cipher suite incompatibility, certificate revocation (CRL/OCSP), private key permission issues, SNI (Server Name Indication) misconfiguration, and certificate transparency requirements not met. The fix requires understanding PKI (Public Key Infrastructure), TLS handshake mechanics, certificate chain validation, and debugging tools. This guide provides production-proven troubleshooting for SSL/TLS errors across web servers, applications, and service-to-service communication.
Symptoms
- Browser shows
Your connection is not privatewarning SSL_ERROR_RX_RECORD_TOO_LONGin FirefoxERR_SSL_VERSION_OR_CIPHER_MISMATCHin Chromeopenssl s_client: certificate verify failedunable to get local issuer certificatecertificate has expiredornot yet validhostname mismatchorcertificate subject mismatchself signed certificateorself signed certificate in certificate chaincertificate revokedorunable to get certificate CRL- TLS handshake fails:
ssl3_get_record:wrong version number DH key too smallorweak ephemeral Diffie-Hellman key- Application logs:
SSL routines:ssl3_read_bytes:tlsv1 alert internal error
Common Causes
- Certificate expired (notBefore/notAfter dates passed)
- Certificate chain incomplete (missing intermediate CA)
- Certificate chain wrong order (leaf cert not first)
- Hostname in URL doesn't match CN or SAN in certificate
- Self-signed certificate not in client trust store
- Server configured with wrong certificate/key pair
- TLS 1.0/1.1 disabled by client but server requires it
- Cipher suites don't overlap between client and server
- Certificate revoked by CA (CRL/OCSP check fails)
- Private key file permissions too open
- SNI not sent by client or not configured on server
- Certificate Transparency (CT) logs not submitted
- Root CA not trusted by client (corporate MITM, internal CA)
Step-by-Step Fix
### 1. Diagnose SSL/TLS issues
Check certificate details:
```bash # Check certificate expiration and details openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer
# Output: # notBefore=Jan 1 00:00:00 2026 GMT # notAfter=Mar 31 23:59:59 2026 GMT # subject=CN = example.com # issuer=C = US, O = Let's Encrypt, CN = R3
# Check if expired openssl x509 -in certificate.crt -noout -enddate # notAfter=Dec 31 23:59:59 2025 GMT (EXPIRED if date passed)
# Check certificate chain openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null
# Shows full chain: # Certificate chain # 0 s:CN = example.com # i:C = US, O = Let's Encrypt, CN = R3 # 1 s:C = US, O = Let's Encrypt, CN = R3 # i:C = US, O = Internet Security Research Group, CN = ISRG Root X1 # 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1 # i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
# Verify certificate chain openssl verify -CAfile ca-bundle.crt certificate.crt # Should output: certificate.crt: OK
# If error: # openssl verify error:num=20:unable to get local issuer certificate ```
Check TLS protocol and ciphers:
```bash # Test TLS versions # TLS 1.2 openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>&1 | grep -E "Protocol|Cipher"
# TLS 1.3 openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>&1 | grep -E "Protocol|Cipher"
# Check what ciphers are supported nmap --script ssl-enum-ciphers -p 443 example.com
# Or use testssl.sh for comprehensive analysis git clone https://github.com/drwetter/testssl.sh.git cd testssl.sh ./testssl.sh example.com
# Output shows: # - Supported protocols (SSLv2, SSLv3, TLS 1.0-1.3) # - Cipher suites offered # - Certificate chain issues # - Security vulnerabilities (POODLE, BEAST, etc.) ```
Check hostname matching:
```bash # Check CN and SAN in certificate openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
# Output: # subject=CN = example.com # X509v3 Subject Alternative Name: # DNS:example.com, DNS:www.example.com
# If accessing api.example.com but not in SAN, will get hostname mismatch
# Check certificate matches private key openssl x509 -noout -modulus -in certificate.crt | openssl md5 openssl rsa -noout -modulus -in private.key | openssl md5 # Both should produce same hash
# If different, certificate and key don't match ```
### 2. Fix certificate expiration
Renew expired certificate:
```bash # Let's Encrypt with certbot certbot renew --force-renewal
# Or for specific domain certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
# Auto-renewal setup (certbot installs this automatically) # /etc/cron.d/certbot 0 */12 * * * root test -x /usr/bin/certbot && /usr/bin/certbot -q renew
# Check renewal status certbot certificates
# Output: # Saving debug log to /var/log/letsencrypt/letsencrypt.log # # Found the following certs: # Certificate Name: example.com # Domains: example.com www.example.com # Expiry Date: 2026-06-30 23:59:59+00:00 (VALID: 89 days) # Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem # Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
# Manual certificate renewal (non-Let's Encrypt) # Download new certificate from CA # Replace files and reload cp new-certificate.crt /etc/ssl/certs/ cp new-private.key /etc/ssl/private/ systemctl reload nginx # or apache2 ```
Fix certificate validity dates:
```bash # Certificate not yet valid (system clock wrong) # Check system time date timedatectl status
# Fix time synchronization timedatectl set-ntp true # Or systemctl restart systemd-timesyncd
# For certificate with wrong dates, must reissue from CA # Cannot fix dates on existing certificate ```
### 3. Fix certificate chain issues
Build correct certificate chain:
```bash # Certificate chain order (top to bottom in file): # 1. Leaf certificate (your domain) # 2. Intermediate CA certificate(s) # 3. Root CA certificate (usually not needed, in client trust store)
# Let's Encrypt provides fullchain.pem with correct order # For other CAs, concatenate manually
# Create fullchain cat example.com.crt intermediate.crt > fullchain.pem
# For multiple intermediates cat example.com.crt intermediate1.crt intermediate2.crt > fullchain.pem
# Verify chain openssl verify -CAfile root-ca.crt -untrusted intermediate.crt certificate.crt
# Nginx configuration server { listen 443 ssl; server_name example.com;
ssl_certificate /etc/ssl/certs/fullchain.pem; # Leaf + intermediates ssl_certificate_key /etc/ssl/private/privkey.pem; }
# Apache configuration <VirtualHost *:443> ServerName example.com
SSLCertificateFile /etc/ssl/certs/example.com.crt SSLCertificateChainFile /etc/ssl/certs/intermediate.crt # Or include in above SSLCertificateKeyFile /etc/ssl/private/privkey.pem </VirtualHost> ```
Fix missing intermediate certificate:
```bash # Error: unable to get local issuer certificate # Means intermediate CA certificate is missing
# Download intermediate from CA # Let's Encrypt: https://letsencrypt.org/certificates/ # DigiCert, Comodo, etc. provide intermediates on their websites
# Common intermediates: # Let's Encrypt R3: https://letsencrypt.org/certs/r3.pem # ISRG Root X1: https://letsencrypt.org/certs/isrgrootx1.pem
# Add to chain file cat intermediate.pem >> fullchain.pem
# Reload web server systemctl reload nginx ```
### 4. Fix hostname mismatch
Check SAN configuration:
```bash # Certificate only valid for exact hostnames in CN and SAN
# Check what's covered openssl x509 -in certificate.crt -noout -text | grep -A1 "Subject Alternative Name"
# Output: # X509v3 Subject Alternative Name: # DNS:example.com, DNS:www.example.com
# This certificate covers: # - example.com ✓ # - www.example.com ✓ # - api.example.com ✗ (not listed) # - *.example.com ✗ (wildcard not present)
# To cover subdomains, need wildcard certificate # DNS: *.example.com covers all first-level subdomains # *.example.com does NOT cover example.com (apex) # *.example.com does NOT cover sub.sub.example.com (only one level) ```
Request certificate with correct hostnames:
```bash # Let's Encrypt - add all hostnames certbot certonly --webroot -w /var/www/html \ -d example.com \ -d www.example.com \ -d api.example.com \ -d app.example.com
# Wildcard certificate (requires DNS validation) certbot certonly --dns-cloudflare -d "*.example.com" -d example.com
# Or manual DNS validation certbot certonly --manual --preferred-challenges dns -d "*.example.com"
# Commercial CA - submit CSR with all SANs # Most CAs allow adding SANs during certificate request ```
### 5. Fix TLS protocol and cipher issues
Configure secure TLS settings:
```nginx # Nginx - Modern TLS configuration server { listen 443 ssl http2;
# Only TLS 1.2 and 1.3 ssl_protocols TLSv1.2 TLSv1.3;
# Modern cipher suites (TLS 1.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; # Let client choose from server's list
# OCSP Stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s;
# Session settings ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # Disable session tickets for forward secrecy
ssl_certificate /etc/ssl/certs/fullchain.pem; ssl_certificate_key /etc/ssl/private/privkey.pem; } ```
```apache # Apache - Modern TLS configuration <VirtualHost *:443> ServerName example.com
# Only TLS 1.2 and 1.3 SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
# Cipher suites SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 SSLHonorCipherOrder off
# OCSP Stapling SSLUseStapling on SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
SSLCertificateFile /etc/ssl/certs/fullchain.pem SSLCertificateKeyFile /etc/ssl/private/privkey.pem </VirtualHost> ```
Fix legacy client compatibility:
```nginx # If you must support old clients (not recommended) # TLS 1.0/1.1 support (security risk!) ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
# Include older ciphers (weak!) ssl_ciphers 'HIGH:MEDIUM:!aNULL:!MD5:!RC4'
# Better: Use separate server block for legacy clients server { listen 8443 ssl; server_name legacy.example.com;
# Legacy settings on non-standard port ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'HIGH:MEDIUM:!aNULL:!MD5';
# Redirect main domain to standard port with modern TLS return 301 https://$host:443$request_uri; } ```
### 6. Fix self-signed certificate trust
Generate self-signed certificate:
```bash # Generate key and certificate openssl req -x509 -newkey rsa:4096 \ -keyout /etc/ssl/private/selfsigned.key \ -out /etc/ssl/certs/selfsigned.crt \ -days 365 \ -nodes \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"
# Add to system trust store # Debian/Ubuntu cp selfsigned.crt /usr/local/share/ca-certificates/ update-ca-certificates
# RHEL/CentOS cp selfsigned.crt /etc/pki/ca-trust/source/anchors/ update-ca-trust
# Application-specific trust # Node.js export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/selfsigned.crt
# Python (requests) export REQUESTS_CA_BUNDLE=/etc/ssl/certs/selfsigned.crt
# Java keytool keytool -import -alias selfsigned -file selfsigned.crt -keystore $JAVA_HOME/lib/security/cacerts ```
### 7. Fix SNI (Server Name Indication) issues
Configure SNI on server:
```nginx # Nginx - Multiple SSL sites with SNI server { listen 443 ssl; server_name example.com;
ssl_certificate /etc/ssl/certs/example.com/fullchain.pem; ssl_certificate_key /etc/ssl/private/example.com/privkey.pem; }
server { listen 443 ssl; server_name other-domain.com;
ssl_certificate /etc/ssl/certs/other-domain.com/fullchain.pem; ssl_certificate_key /etc/ssl/private/other-domain.com/privkey.pem; }
# Default certificate for non-SNI clients (TLS 1.2 and earlier) server { listen 443 ssl default_server; server_name _;
ssl_certificate /etc/ssl/certs/default/fullchain.pem; ssl_certificate_key /etc/ssl/private/default/privkey.pem;
return 444; # Close connection without response } ```
Test SNI:
```bash # Test without SNI (gets default certificate) openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject
# Test with SNI openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject
# curl with SNI curl -v --resolve example.com:443:1.2.3.4 https://example.com/
# Old clients without SNI support will get wrong certificate # Solution: Use separate IP addresses or upgrade clients ```
### 8. Fix certificate revocation issues
Check CRL/OCSP:
```bash # Check if certificate is revoked # CRL (Certificate Revocation List) openssl x509 -in certificate.crt -noout -text | grep -A5 "CRL Distribution"
# Download and check CRL CRL_URL=$(openssl x509 -in certificate.crt -noout -text | grep "CRL Distribution" -A2 | grep "URI:" | cut -d: -f2-) curl -O $CRL_URL openssl crl -in crl-file.crl -inform DER -noout
# OCSP check OCSP_URL=$(openssl x509 -in certificate.crt -noout -text | grep "Authority Info Access" -A2 | grep "OCSP" | cut -d: -f2-) openssl ocsp -issuer intermediate.crt -cert certificate.crt -url $OCSP_URL
# Response should be: good # If "revoked", certificate has been revoked by CA ```
OCSP stapling configuration:
```nginx # Nginx OCSP stapling server { listen 443 ssl;
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;
# Trusted CA for stapling verification ssl_trusted_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate /etc/ssl/certs/fullchain.pem; ssl_certificate_key /etc/ssl/private/privkey.pem; }
# Verify stapling is working echo | openssl s_client -connect example.com:443 -status 2>/dev/null | grep -A5 "OCSP response" ```
### 9. Monitor certificate expiration
Certificate expiration monitoring:
```bash # Check days until expiration check_ssl_expiry() { local host=$1 local expiry=$(echo | openssl s_client -connect $host:443 -servername $host 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2) local expiry_epoch=$(date -d "$expiry" +%s) local now_epoch=$(date +%s) local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "$host: $days_left days until expiration"
if [ $days_left -lt 30 ]; then echo "WARNING: Certificate expires in less than 30 days!" return 1 fi }
# Check multiple hosts check_ssl_expiry example.com check_ssl_expiry api.example.com check_ssl_expiry shop.example.com ```
Prometheus monitoring:
```yaml # Blackbox exporter for SSL monitoring # https://github.com/prometheus/blackbox_exporter
scrape_configs: - job_name: 'ssl-certs' metrics_path: /probe params: module: [http_2xx] static_configs: - targets: - https://example.com - https://api.example.com relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: blackbox-exporter:9115
# Key metrics: # probe_ssl_earliest_cert_expiry - Unix timestamp of cert expiry # probe_ssl_last_chain_expiry_timestamp_seconds # probe_ssl_cert_chain_expiry_timestamp_seconds
# Grafana alert rules groups: - name: ssl_certificates rules: - alert: SSLCertExpiringSoon expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 30 for: 1h labels: severity: warning annotations: summary: "SSL certificate expiring in less than 30 days" description: "{{ $labels.instance }} certificate expires in {{ $value | humanizeDuration }}"
- alert: SSLCertExpired
- expr: (probe_ssl_earliest_cert_expiry - time()) < 0
- for: 5m
- labels:
- severity: critical
- annotations:
- summary: "SSL certificate has expired"
- description: "{{ $labels.instance }} certificate expired"
`
Prevention
- Set up automated renewal (certbot, ACME clients)
- Monitor certificate expiration with alerts (30, 14, 7 days before)
- Use certificate automation (cert-manager for Kubernetes)
- Document certificate inventory (domains, expiry, CA)
- Implement TLS 1.2+ only, disable legacy protocols
- Use strong cipher suites, avoid weak algorithms
- Enable OCSP stapling for revocation checking
- Test SSL configuration regularly (testssl.sh, SSL Labs)
- Use wildcard certificates for subdomain flexibility
- Implement proper certificate chain in correct order
Related Errors
- **ERR_CONNECTION_RESET**: Connection terminated, possibly TLS-related
- **ERR_SSL_VERSION_OR_CIPHER_MISMATCH**: Protocol/cipher incompatibility
- **ERR_CERT_COMMON_NAME_INVALID**: Hostname mismatch
- **ERR_CERT_DATE_INVALID**: Certificate expired or not yet valid
- **ERR_CERT_AUTHORITY_INVALID**: Untrusted CA or self-signed