What's Actually Happening

SSL/TLS certificate validation fails when connecting to HTTPS endpoints. Clients reject the connection due to certificate issues.

The Error You'll See

```bash $ curl https://example.com

curl: (60) SSL certificate problem: unable to get local issuer certificate ```

Browser error:

bash
Your connection is not private
NET::ERR_CERT_AUTHORITY_INVALID

Python error:

python
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

Node.js error:

bash
Error: unable to verify the first certificate

Why This Happens

  1. 1.Self-signed certificate - Certificate not signed by trusted CA
  2. 2.Missing intermediate CA - Certificate chain incomplete
  3. 3.Expired certificate - Certificate validity period ended
  4. 4.Domain mismatch - Certificate domain doesn't match requested domain
  5. 5.Untrusted root CA - Root CA not in trust store
  6. 6.Clock skew - System time incorrect, outside validity period

Step 1: Check Certificate Details

```bash # Check certificate from server: openssl s_client -connect example.com:443 -servername example.com

# Get certificate: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text

# Check specific details: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -subject -issuer

# Check certificate chain: openssl s_client -connect example.com:443 -servername example.com -showcerts

# Check for errors: openssl s_client -connect example.com:443 -servername example.com 2>&1 | grep -i error

# Verify certificate: openssl verify example.com.crt

# Verify with CA bundle: openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt example.com.crt ```

Step 2: Check Certificate Expiration

```bash # Check expiration dates: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

# Output: # notBefore=Jan 1 00:00:00 2024 GMT # notAfter=Dec 31 23:59:59 2025 GMT

# Check if expired: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -checkend 0

# Returns: # Certificate will expire (or has expired) # OR # Certificate will not expire

# Check days until expiry: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2 | xargs -I {} date -d {} +%s | xargs -I {} echo "Expiry timestamp: {}"

# Current time: date +%s

# Compare to see if expired ```

Step 3: Check Certificate Chain

```bash # Check full chain: openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null

# Count certificates in chain: openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -c "BEGIN CERTIFICATE"

# Should be 2+ (leaf + intermediate(s))

# Check chain depth: openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -E "depth=[0-9]"

# Verify each certificate: openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > chain.pem

# Split and verify each: csplit chain.pem '/-----BEGIN CERTIFICATE-----/' '{*}' -f cert- -s for f in cert-*; do [ -s "$f" ] && openssl x509 -in "$f" -noout -subject -issuer done rm cert-* chain.pem ```

Step 4: Check Domain Match

```bash # Check certificate subject: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject

# Check Subject Alternative Names (SAN): openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

# SAN should include: # DNS:example.com, DNS:www.example.com

# If accessing different domain, certificate won't match

# Test with specific hostname: openssl s_client -connect example.com:443 -servername www.example.com

# Check CN (Common Name): openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject | grep CN

# Modern certificates should use SAN, not just CN ```

Step 5: Check Trust Store

```bash # Check system trust store: ls /etc/ssl/certs/ | head ls /etc/ssl/certs/ca-certificates.crt

# Update CA certificates: # Ubuntu/Debian: update-ca-certificates

# RHEL/CentOS: update-ca-trust

# Check trusted CAs: openssl storeutl -certs /etc/ssl/certs/ca-certificates.crt | grep -E "Subject:|Issuer:" | head -20

# Add custom CA: cp custom-ca.crt /usr/local/share/ca-certificates/ update-ca-certificates

# For Java trust store: keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit

# For Python: # Uses system trust store or certifi python -c "import certifi; print(certifi.where())" ```

Step 6: Fix Self-Signed Certificate Issues

```bash # For development with self-signed certificates:

# Option 1: Skip verification (testing only!): curl -k https://example.com

# Python: import ssl import urllib.request context = ssl._create_unverified_context() urllib.request.urlopen('https://example.com', context=context)

# Node.js: process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Not for production!

# Option 2: Add to trust store: # Get self-signed cert: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 > self-signed.crt

# Add to system trust: sudo cp self-signed.crt /usr/local/share/ca-certificates/self-signed.crt sudo update-ca-certificates

# Python add cert: import certifi import ssl ssl.create_default_context(cafile='/path/to/cert.pem')

# For specific application: export SSL_CERT_FILE=/path/to/cert.pem export REQUESTS_CA_BUNDLE=/path/to/cert.pem ```

Step 7: Fix Intermediate Certificate Issues

```bash # Download intermediate certificate: # From CA issuer URL: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep "CA Issuers - URI" | sed 's/URI://'

# Download: wget -O intermediate.der "http://CA_ISSUER_URL" openssl x509 -inform DER -in intermediate.der -out intermediate.pem

# Create full chain: cat cert.pem intermediate.pem > fullchain.pem

# Configure server with full chain: # Nginx: ssl_certificate /path/to/fullchain.pem;

# Apache: SSLCertificateFile /path/to/cert.pem SSLCertificateChainFile /path/to/intermediate.pem

# HAProxy: bind *:443 ssl crt /path/to/fullchain.pem ```

Step 8: Check System Time

```bash # Check current time: date

# Check against NTP: timedatectl status

# If time wrong, certificate validation fails

# Sync time: timedatectl set-ntp true

# Or: ntpdate pool.ntp.org

# Check timezone: timedatectl

# Set correct timezone: timedatectl set-timezone UTC

# For certificate validation, system time must be: # - After certificate notBefore # - Before certificate notAfter ```

Step 9: Debug Certificate Issues

```bash # Use testssl.sh: git clone https://github.com/drwetter/testssl.sh cd testssl.sh ./testssl.sh https://example.com

# Use SSL Labs: # https://www.ssllabs.com/ssltest/

# Debug with curl: curl -v https://example.com 2>&1 | grep -E "SSL|certificate"

# Debug with OpenSSL: openssl s_client -connect example.com:443 -servername example.com -tlsextdebug -status

# Check OCSP: openssl ocsp -issuer chain.pem -cert cert.pem -url http://ocsp.example.com -resp_text

# Check CRL: openssl crl -in crl.pem -noout -text

# Use online checker: curl https://example.com -v --trace-ascii /tmp/trace.txt cat /tmp/trace.txt ```

Step 10: SSL Certificate Verification Script

```bash # Create verification script: cat << 'EOF' > /usr/local/bin/check-ssl-cert.sh #!/bin/bash

HOST=$1 PORT=${2:-"443"}

echo "=== Certificate Details ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>/dev/null | openssl x509 -noout -subject -issuer -dates

echo "" echo "=== Certificate Chain ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST -showcerts 2>/dev/null | awk '/depth=/{print}'

echo "" echo "=== Verification ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>&1 | grep -E "Verify return|error|alert"

echo "" echo "=== Subject Alternative Names ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

echo "" echo "=== OCSP Status ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST -status 2>/dev/null | grep -A5 "OCSP response"

echo "" echo "=== TLS Version ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>/dev/null | grep "Protocol"

echo "" echo "=== Cipher ===" echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>/dev/null | grep "Cipher"

echo "" echo "=== Test Connection ===" curl -I https://$HOST 2>&1 | head -5

echo "" echo "=== Check Expiry (days) ===" EXPIRY=$(echo | openssl s_client -connect $HOST:$PORT -servername $HOST 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2) EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 )) echo "Days until expiry: $DAYS" if [ $DAYS -lt 30 ]; then echo "WARNING: Certificate expires soon!" fi EOF

chmod +x /usr/local/bin/check-ssl-cert.sh

# Usage: /usr/local/bin/check-ssl-cert.sh example.com 443

# Quick check: alias ssl-check='openssl s_client -connect' ```

SSL Certificate Checklist

CheckCommandExpected
Certificate validopenssl verifyOK
Not expiredopenssl x509 -datesWithin validity
Chain complete-showcertsIntermediate present
Domain matchesopenssl x509 -textSAN includes domain
CA trustedverify -CAfileVerify OK
System timedateCorrect time

Verify the Fix

```bash # After fixing certificate validation

# 1. Check certificate openssl s_client -connect example.com:443 | grep "Verify return" // Verify return code: 0 (ok)

# 2. Test connection curl https://example.com // Success, no SSL error

# 3. Check chain openssl s_client -connect example.com:443 -showcerts // Chain complete

# 4. Verify dates openssl s_client -connect example.com:443 | openssl x509 -noout -dates // Valid date range

# 5. Check in browser // Green lock, connection secure

# 6. Test with application python -c "import urllib.request; urllib.request.urlopen('https://example.com')" // No exception ```

  • [Fix SSL Certificate Expired](/articles/fix-ssl-certificate-expired)
  • [Fix Nginx SSL Certificate Not Trusted](/articles/fix-nginx-ssl-certificate-not-trusted)
  • [Fix HTTPS Redirect Loop](/articles/fix-https-redirect-loop)