SSL certificate errors are among the most stressful production incidents. Users see scary browser warnings, automated systems fail silently, and the root cause isn't always obvious. This guide covers the most common SSL errors, how to diagnose them, and how to fix them.
Before diving into specific errors, here are the commands you'll use repeatedly:
# Check what certificate a server presents
openssl s_client -connect example.com:443 -servername example.com
# Check certificate expiry
openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
# Verbose curl (shows certificate info)
curl -vI https://example.com 2>&1 | grep -A10 "Server certificate"
# Check certificate details from a file
openssl x509 -in cert.pem -noout -text
# Decode a live certificate and show SANs
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -ext subjectAltName
Tip: For a visual alternative, paste your certificate into the Certificate Viewer to see all fields, including SANs, key usage, and validity status with color-coded indicators.
Browser message: "NET::ERR_CERT_DATE_INVALID" or "SEC_ERROR_EXPIRED_CERTIFICATE"
Cause: The certificate's notAfter date has passed.
# Check expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# notBefore=Jan 15 00:00:00 2026 GMT
# notAfter=Apr 15 23:59:59 2026 GMT
Let's Encrypt (certbot):
# Force renewal
certbot renew --force-renewal
# Reload web server
systemctl reload nginx
# or
systemctl reload apache2
Manual certificate:
# Replace files
cp new-fullchain.pem /etc/nginx/ssl/fullchain.pem
cp new-privkey.pem /etc/nginx/ssl/privkey.pem
# Test config before reloading
nginx -t
# Reload
systemctl reload nginx
Prevention: Set up monitoring with certbot's built-in timer, or use a monitoring service that alerts before expiry.
For a complete step-by-step response, see the SSL Certificate Expired Runbook.
Browser message: "NET::ERR_CERT_COMMON_NAME_INVALID" or "SSL_ERROR_BAD_CERT_DOMAIN"
Cause: The domain you're visiting doesn't match the certificate's Common Name (CN) or Subject Alternative Names (SANs).
# Check which domains the certificate covers
echo | openssl s_client -connect example.com:443 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
www.example.com but cert only covers example.com (or vice versa)*.example.com) doesn't cover the bare domain (example.com)app.staging.example.com)If the domain is missing from the certificate, you need a new certificate that includes it:
# Let's Encrypt — add the missing domain
certbot certonly --nginx -d example.com -d www.example.com
# Or use a wildcard
certbot certonly --dns-cloudflare -d example.com -d "*.example.com"
If it's an SNI issue (multiple domains on one IP), ensure your server is configured to select the right certificate per domain:
# Nginx — separate server blocks per domain
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
}
server {
listen 443 ssl;
server_name other.com;
ssl_certificate /etc/ssl/other.com/fullchain.pem;
ssl_certificate_key /etc/ssl/other.com/privkey.pem;
}
Browser message: "NET::ERR_CERT_AUTHORITY_INVALID" or "unable to get local issuer certificate"
Cause: The browser can't build a chain from your certificate to a trusted root CA. Usually means intermediate certificates are missing.
# Count certificates the server sends
echo | openssl s_client -connect example.com:443 2>/dev/null | \
grep -c "BEGIN CERTIFICATE"
# If the answer is 1, intermediates are missing
# Should be 2 or more (server cert + intermediates)
# Full chain verification
openssl s_client -connect example.com:443 -servername example.com
# Look for "Verify return code: 21 (unable to verify the first certificate)"
Add intermediate certificates to your chain file:
# Download intermediate from your CA and concatenate
cat server.crt intermediate.crt > fullchain.pem
# For Let's Encrypt, use fullchain.pem (not cert.pem)
# /etc/letsencrypt/live/example.com/fullchain.pem
Verify the fix:
echo | openssl s_client -connect example.com:443 2>/dev/null | \
grep "Verify return code"
# Should show: Verify return code: 0 (ok)
Tip: Use the Chain Builder tool to automatically fetch intermediates from AIA extensions. Paste your server cert and it builds the full chain for you.
Browser message: "NET::ERR_CERT_AUTHORITY_INVALID" (same as untrusted chain)
Cause: The certificate was self-signed and isn't in the client's trust store.
If you need to trust a self-signed cert on Linux:
# Debian/Ubuntu
cp my-ca.crt /usr/local/share/ca-certificates/
update-ca-certificates
# RHEL/CentOS
cp my-ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust
# For curl specifically
curl --cacert my-ca.crt https://internal.example.com
For production, use Let's Encrypt — it's free and automatable.
Browser message: Console warnings about "Mixed Content" — the page loads over HTTPS but some resources use HTTP.
Cause: Your HTML references images, scripts, or stylesheets over HTTP.
Open browser DevTools (F12) → Console tab. Look for "Mixed Content" warnings.
https:// or protocol-relative //add_header Content-Security-Policy "upgrade-insecure-requests" always;
This tells browsers to automatically upgrade HTTP requests to HTTPS.
Browser message: "ERR_SSL_VERSION_OR_CIPHER_MISMATCH"
Cause: Server only supports TLS 1.0/1.1, which modern browsers have dropped.
# Test specific TLS versions
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
Update your server config to support TLS 1.2 and 1.3:
# Nginx
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;
Browser message: "ERR_CERT_REVOKED"
Cause: The certificate has been revoked by the CA (compromised key, domain ownership change, etc.).
# Check OCSP status
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -ocsp_uri
# Query OCSP responder
openssl ocsp -issuer chain.pem -cert server.pem \
-url http://ocsp.example.com -resp_text
You need a new certificate. The revoked one cannot be un-revoked.
# Let's Encrypt
certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem
certbot certonly --nginx -d example.com
Browser message: "NET::ERR_CERT_AUTHORITY_INVALID" with "You cannot proceed" (no override option)
Cause: The domain has HSTS (HTTP Strict Transport Security) enabled, and the certificate is invalid. HSTS prevents users from clicking through certificate warnings.
You must fix the certificate — there's no workaround for HSTS errors. If you're in a development environment:
# Chrome: clear HSTS for a domain
chrome://net-internals/#hsts
# Enter domain in "Delete domain security policies"
When you get an SSL error, run through this checklist:
DOMAIN="example.com"
# 1. Is the certificate expired?
echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | \
openssl x509 -noout -dates
# 2. Does the domain match?
echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | \
openssl x509 -noout -subject -ext subjectAltName
# 3. Is the chain complete?
echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | \
grep "Verify return code"
# 4. Which TLS versions are supported?
for v in tls1_2 tls1_3; do
echo -n "$v: "
echo | openssl s_client -connect $DOMAIN:443 -$v 2>/dev/null | \
grep "Protocol :" || echo "NOT SUPPORTED"
done
# 5. Is the private key matching?
# (requires access to the server's certificate and key files)
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5
Most SSL errors fall into a few categories: expired, name mismatch, incomplete chain, or outdated TLS configuration. The debugging process is always the same:
openssl s_client)For hands-on debugging: