SSL Certificate Errors: Complete Troubleshooting
SSL certificate errors prevent secure HTTPS connections between clients and servers. Whether you see “Your connection is not private” in a browser, SSL: CERTIFICATE_VERIFY_FAILED in Python, or curl: (60) SSL certificate problem in the terminal, this guide covers every common SSL error and how to fix it.
Common SSL Certificate Errors
| Error | Meaning |
|---|---|
NET::ERR_CERT_DATE_INVALID | Certificate has expired |
NET::ERR_CERT_AUTHORITY_INVALID | Certificate not signed by a trusted CA |
NET::ERR_CERT_COMMON_NAME_INVALID | Certificate hostname does not match |
SSL_ERROR_HANDSHAKE_FAILURE_ALERT | TLS handshake failed |
ERR_SSL_VERSION_OR_CIPHER_MISMATCH | No shared TLS version or cipher |
CERTIFICATE_VERIFY_FAILED | Python cannot verify the certificate |
curl: (60) | cURL cannot verify the certificate |
curl: (35) | SSL connect error |
Diagnosing SSL Errors
With cURL
# Check SSL certificate details
curl -vI https://example.com 2>&1 | grep -A 5 "SSL certificate"
Show full certificate chain
openssl s_client -connect example.com:443 -servername example.com
Check certificate expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
Check certificate subject and issuer
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -subject -issuer
Show all certificates in the chain
openssl s_client -connect example.com:443 -servername example.com -showcerts
With Python
import ssl
import socket
hostname = "example.com"
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
print(f"Subject: {cert['subject']}")
print(f"Issuer: {cert['issuer']}")
print(f"Valid from: {cert['notBefore']}")
print(f"Valid until: {cert['notAfter']}")
print(f"SANs: {cert.get('subjectAltName', [])}")
Fix 1: Expired Certificate
Symptom: NET::ERR_CERT_DATE_INVALID or certificate notAfter date is in the past.
# Check expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate
Renew with Let's Encrypt / Certbot
sudo certbot renew
Or renew a specific certificate
sudo certbot certonly --nginx -d example.com -d www.example.com
Set up auto-renewal
sudo crontab -e
Add: 0 3 * certbot renew --quiet
Fix 2: Self-Signed Certificate
Symptom: NET::ERR_CERT_AUTHORITY_INVALID or unable to get local issuer certificate.
The certificate was not signed by a trusted Certificate Authority.
# For development: skip verification (NOT for production)
curl -k https://self-signed.example.com
Or specify the CA certificate
curl --cacert /path/to/ca-cert.pem https://self-signed.example.com
import requests
Skip verification (development only)
response = requests.get("https://self-signed.example.com", verify=False)
Proper fix: specify CA bundle
response = requests.get("https://self-signed.example.com", verify="/path/to/ca-cert.pem")
Production fix: Get a free certificate from Let’s Encrypt:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
Fix 3: Hostname Mismatch
Symptom: NET::ERR_CERT_COMMON_NAME_INVALID — the certificate is valid but was issued for a different domain.
# Check which domains the certificate covers
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -text | grep -A 1 "Subject Alternative Name"
Fix: Reissue the certificate to include the correct domain(s):
sudo certbot --nginx -d example.com -d www.example.com -d api.example.com
Fix 4: Incomplete Certificate Chain
Symptom: Works in browsers but fails in cURL/Python. unable to verify the first certificate.
The server is not sending the full certificate chain (missing intermediate certificates).
# Check the chain
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep -E "depth|verify"
Download and combine certificates
cat your-domain.crt intermediate.crt root.crt > fullchain.pem
Nginx:
ssl_certificate /etc/ssl/fullchain.pem; # Full chain, not just the domain cert
ssl_certificate_key /etc/ssl/private.key;
Apache:
SSLCertificateFile /etc/ssl/domain.crt
SSLCertificateKeyFile /etc/ssl/private.key
SSLCertificateChainFile /etc/ssl/intermediate.crt
Fix 5: TLS Version Mismatch
Symptom: ERR_SSL_VERSION_OR_CIPHER_MISMATCH — the client and server cannot agree on a TLS version.
# Check supported TLS versions
nmap --script ssl-enum-ciphers -p 443 example.com
Force a specific TLS version with cURL
curl --tlsv1.2 https://example.com
curl --tlsv1.3 https://example.com
Fix: Enable modern TLS versions on your server:
# 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';
ssl_prefer_server_ciphers on;
Fix 6: Outdated CA Bundle
Symptom: Certificate is valid but the client does not trust it because its CA bundle is outdated.
# Update CA certificates (Ubuntu/Debian)
sudo apt update && sudo apt install ca-certificates
sudo update-ca-certificates
Update CA certificates (CentOS/RHEL)
sudo yum install ca-certificates
sudo update-ca-trust
Update certifi for Python
pip install --upgrade certifi
Check certifi CA bundle location
python -c "import certifi; print(certifi.where())"
SSL Errors with Proxies
When using proxies, SSL errors can occur at two points: between your client and the proxy, or between the proxy and the target server.
# Test SSL through a proxy
curl -v -x http://user:pass@proxy.example.com:8080 https://example.com
If proxy uses HTTPS (CONNECT tunnel)
curl -v --proxy-cacert /path/to/proxy-ca.pem -x https://proxy.example.com:8443 https://example.com
import requests
proxies = {
"https": "http://user:pass@proxy.example.com:8080"
}
If proxy causes SSL issues, you may need to specify CA for the proxy
response = requests.get("https://example.com", proxies=proxies, verify="/path/to/ca-bundle.pem")
Quick Troubleshooting Flowchart
- Is the certificate expired? Run
openssl s_clientand check dates. Renew with Certbot. - Is it self-signed? Get a real certificate from Let’s Encrypt.
- Does the hostname match? Reissue the certificate with correct SANs.
- Is the chain complete? Use the full chain file, not just the domain cert.
- Is TLS version supported? Enable TLSv1.2+ on the server.
- Is the CA bundle outdated? Update system CA certificates.
FAQ
Should I ignore SSL errors in production?
Never. Disabling SSL verification (curl -k or verify=False) removes encryption protection and makes you vulnerable to man-in-the-middle attacks. Only use this for local development and testing.
How do I get a free SSL certificate?
Use Let’s Encrypt with Certbot. It provides free, auto-renewable certificates trusted by all major browsers.
Why does my SSL certificate work in Chrome but not in cURL?
Browsers are more forgiving and can fetch missing intermediate certificates automatically. cURL and other tools require the full certificate chain to be served by the server.
How often should I renew my SSL certificate?
Let’s Encrypt certificates expire every 90 days and should be auto-renewed. Commercial certificates typically last 1-2 years.