P2MediumRunbook

Certificate Not Trusted Runbook

Browser showing 'Not Secure' or certificate errors? This runbook helps you diagnose the specific problem and fix it quickly.

15-30 minutes
Last Updated: December 2025
Progress: 0/29 complete0%

When to Use

  • Browser shows "Your connection is not private"
  • Users reporting certificate warnings
  • ERR_CERT errors in browser
  • API clients failing with SSL verification errors

Do NOT Use For

  • Certificate is expired (use Emergency Replacement Runbook)
  • Known key compromise (use Key Compromise Response Runbook)

Quick Reference (TL;DR)

  1. Identify the specific error (chain, expired, name mismatch, revoked)
  2. Run diagnostic commands to confirm
  3. Fix based on error type
  4. Verify from external sources

1Identify the Error

Objective: Determine exactly what type of trust error you're dealing with

💡 ERR_CERT_AUTHORITY_INVALID → Chain/trust problem

💡 ERR_CERT_DATE_INVALID → Expired or not yet valid

💡 ERR_CERT_COMMON_NAME_INVALID → Wrong domain name

💡 NET::ERR_CERT_REVOKED → Certificate was revoked

💡 SSL_ERROR_RX_RECORD_TOO_LONG → Not HTTPS or wrong port

2Quick Diagnosis

Objective: Run commands to understand what the server is sending

View certificate being served:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -ext subjectAltName

Check chain completeness:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  grep -E "(depth|verify|Certificate chain)"

Full chain dump:

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null

Check from curl:

curl -vI https://example.com 2>&1 | grep -E "(SSL|certificate|subject|issuer|expire)"

3Fix: Chain Incomplete

Objective: Add missing intermediate certificates to complete the chain

Find the issuer of your certificate:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -issuer

Create proper bundle (order matters):

# Order: Your cert first, then intermediates (root optional)
cat your-cert.crt intermediate.crt > bundle.crt

# For nginx:
ssl_certificate /path/to/bundle.crt;
ssl_certificate_key /path/to/private.key;

Reload after update:

# nginx
sudo nginx -t && sudo systemctl reload nginx

# Apache
sudo apachectl configtest && sudo systemctl reload apache2

💡 Download intermediates from your CA's repository page

💡 Common intermediate download pages: DigiCert, Sectigo, Let's Encrypt

💡 Use our Chain Builder demo to help construct the correct chain

4Fix: Certificate Expired

Objective: Replace the expired certificate

Check expiration:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -dates

5Fix: Wrong Domain Name

Objective: Ensure certificate covers the correct domains

View CN and SANs:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -subject -ext subjectAltName

💡 Wildcard *.example.com does NOT cover example.com (apex)

💡 Wildcard only covers one level: *.example.com covers www.example.com but not a.b.example.com

💡 Check if accessing via IP - certificates don't cover IPs unless explicitly in SAN

6Fix: Certificate Revoked

Objective: Replace the revoked certificate

Check revocation status via OCSP:

# Get OCSP responder URL from certificate
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -ocsp_uri

# Query OCSP (requires issuer cert)
openssl ocsp -issuer issuer.crt -cert server.crt \
  -url http://ocsp.digicert.com -resp_text

💡 Revoked certificates CANNOT be un-revoked - must get new certificate

💡 If compromised: generate NEW key, don't reuse old key

7Fix: Self-Signed Certificate

Objective: Replace with trusted CA-issued certificate

Check if self-signed:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -subject -issuer

# If Subject and Issuer are identical = self-signed

💡 For internal use only: distribute root cert to clients instead of replacing

💡 For public facing: must use trusted CA (Let's Encrypt is free)

8Verify the Fix

Objective: Confirm the trust issue is resolved

Verify trust from command line:

# Should show: Verify return code: 0 (ok)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  grep "Verify return code"

Test with curl (will fail if untrusted):

curl -I https://example.com

Troubleshooting

Problem: Fixed chain but some clients still fail

Solution: Some clients cache the old chain. Wait for cache to expire or test from new client/device.

Problem: Browser trusts it but curl doesn't

Solution: Curl uses system trust store which may differ from browser. Update CA certificates: sudo update-ca-certificates

Problem: Only fails on older devices/browsers

Solution: May need to include cross-signed intermediate for legacy compatibility. Check CA documentation.

Problem: Works from browser but fails from code/API

Solution: Application may be using own trust store. Check language/framework documentation for adding CA certs.