---
# === IDENTITY ===
id: software/debugging/ssl-tls-certificate-errors/2026
canonical_question: "How do I diagnose and fix SSL/TLS certificate errors?"
aliases:
  - "SSL certificate error"
  - "TLS certificate error"
  - "ERR_CERT_AUTHORITY_INVALID"
  - "ERR_CERT_DATE_INVALID"
  - "certificate expired"
  - "self-signed certificate"
  - "hostname mismatch SSL"
  - "SSL handshake failed"
  - "certificate chain incomplete"
  - "NET::ERR_CERT_INVALID"
  - "ERR_SSL_VERSION_OR_CIPHER_MISMATCH"
  - "CERTIFICATE_VERIFY_FAILED"
entity_type: software_reference
domain: software > debugging > ssl_tls_certificate_errors
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.95
version: 2.1
first_published: 2026-02-18

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "200-day max validity enforced by major CAs (DigiCert 2026-02-24, Sectigo 2026-03-12, GlobalSign 2026-03-14)"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Never disable certificate verification (verify=False, -k) in production — it enables MITM attacks"
  - "TLS 1.2 is the minimum acceptable protocol — TLS 1.0/1.1 are deprecated and blocked by all modern browsers since 2020"
  - "Servers must send the full certificate chain (leaf + intermediates) — relying on AIA fetching fails in non-browser clients (curl, Python, Java, Go)"
  - "Wildcard certs (*.example.com) do NOT cover sub.sub.example.com or the apex domain — use multi-SAN certs for multi-level subdomains"
  - "Max certificate validity is 200 days as of March 2026 (DigiCert/Sectigo/GlobalSign enforce 199 days), dropping to 100 days (2027) and 47 days (2029) per CA/B Forum ballot SC-081"
  - "Let's Encrypt's tlsserver ACME profile cuts over to 45-day certs on 2026-05-13; default classic profile drops to 64 days on 2027-02-10 — ACME automation is mandatory, not optional"
  - "Let's Encrypt 6-day 'shortlived' ACME profile is generally available (Jan 2026, 160-hour validity) — opt-in via the shortlived profile, renew every 2-3 days"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Error is CORS-related (Access-Control-Allow-Origin), not a certificate error"
    use_instead: "software/debugging/browser-cors-errors/2026"
  - condition: "Connection refused / port closed — not an SSL error, it is a networking or firewall issue"
    use_instead: "software/debugging/nodejs-econnrefused/2026"
  - condition: "HTTP 4xx/5xx error after TLS handshake succeeds — this is an application error, not a certificate issue"
    use_instead: "Check application logs and server configuration"

# === AGENT HINTS ===
inputs_needed:
  - key: error_message
    question: "What is the exact error message or browser error code (e.g., ERR_CERT_DATE_INVALID, CERTIFICATE_VERIFY_FAILED)?"
    type: text
  - key: client_type
    question: "Where does the error occur?"
    type: choice
    options: ["Browser (Chrome/Firefox/Safari)", "curl/wget", "Python (requests/urllib)", "Java (HttpsURLConnection)", "Node.js", "Go", "Other"]
  - key: cert_type
    question: "What type of certificate is being used?"
    type: choice
    options: ["Let's Encrypt (free/automated)", "Commercial CA (DigiCert, Sectigo, etc.)", "Self-signed", "Private/internal CA", "Unknown"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/ssl-tls-certificate-errors/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/browser-cors-errors/2026"
      label: "Browser CORS Errors"
    - id: "software/debugging/nodejs-econnrefused/2026"
      label: "Node.js ECONNREFUSED"
  solves:
    - id: "software/debugging/kubernetes-crashloopbackoff/2026"
      label: "Kubernetes CrashLoopBackOff (when caused by TLS misconfiguration)"
  often_confused_with:
    - id: "software/debugging/browser-cors-errors/2026"
      label: "Browser CORS Errors (different error, often mistaken for cert issues)"

# === SOURCES (5-8 authoritative sources) ===
sources:
  - id: src1
    title: "Cloudflare — SSL Certificate Errors"
    author: Cloudflare
    url: https://www.cloudflare.com/learning/ssl/ssl-certificate-error/
    type: technical_blog
    published: 2025-01-01
    reliability: authoritative
  - id: src2
    title: "MDN — Transport Layer Security"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src3
    title: "DigiCert — SSL Certificate Errors"
    author: DigiCert
    url: https://www.digicert.com/ssl-certificate-installation/ssl-certificate-errors.htm
    type: technical_blog
    published: 2024-06-01
    reliability: authoritative
  - id: src4
    title: "GlobalSign — Certificate Expiry"
    author: GlobalSign
    url: https://www.globalsign.com/en/blog/what-happens-when-ssl-certificate-expires
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src5
    title: "Sematext — SSL Certificate Monitoring"
    author: Sematext
    url: https://sematext.com/blog/ssl-certificate-monitoring/
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src6
    title: "CA/B Forum Ballot SC-081 — 47-Day Certificate Validity"
    author: CA/Browser Forum
    url: https://www.thesslstore.com/blog/47-day-ssl-certificate-validity-by-2029/
    type: industry_report
    published: 2025-04-01
    reliability: authoritative
  - id: src7
    title: "Let's Encrypt — Decreasing Certificate Lifetimes to 45 Days"
    author: Let's Encrypt (ISRG)
    url: https://letsencrypt.org/2025/12/02/from-90-to-45
    type: official_docs
    published: 2025-12-02
    reliability: authoritative
  - id: src8
    title: "RFC 5280 — Internet X.509 PKI Certificate"
    author: IETF
    url: https://www.rfc-editor.org/rfc/rfc5280
    type: rfc_spec
    published: 2008-05-01
    reliability: authoritative
  - id: src9
    title: "DigiCert — Moving to 199-Day Validity for Public TLS Certificates"
    author: DigiCert
    url: https://knowledge.digicert.com/alerts/public-tls-certificates-199-day-validity
    type: official_docs
    published: 2026-02-24
    reliability: authoritative
  - id: src10
    title: "Let's Encrypt — 6-Day and IP Address Certificates are Generally Available"
    author: Let's Encrypt (ISRG)
    url: https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
---

# How to Diagnose and Fix SSL/TLS Certificate Errors

## TL;DR

- **Bottom line**: SSL/TLS certificate errors fall into 5 main categories: expired certificate, untrusted CA (including self-signed), hostname mismatch, incomplete chain, and protocol/cipher mismatch. The first diagnostic step is always `openssl s_client -connect host:443` — it shows the full certificate chain, expiry, and handshake details in seconds.
- **Key tool/command**: `openssl s_client -connect example.com:443 -servername example.com` — shows certificate chain, expiry date, issuer, and whether verification succeeded.
- **Watch out for**: Incomplete certificate chains (missing intermediate certs) are the #1 cause of "works in Chrome but fails in curl/Python/Java". Servers must send the full chain, not just the leaf certificate.
- **Works with**: All TLS implementations (OpenSSL, BoringSSL, NSS, SChannel). Applies to HTTPS, SMTP/STARTTLS, LDAPS, any TLS-secured protocol.
- **2026 update**: 200-day max validity is LIVE — DigiCert enforced 199 days on 2026-02-24, Sectigo on 2026-03-12, GlobalSign on 2026-03-14. Drops to 100 days (2027), 47 days (2029). Let's Encrypt 6-day certs are GA (Jan 2026, `shortlived` ACME profile); tlsserver profile cuts over to 45-day certs on 2026-05-13. ACME automation is mandatory. [src6, src7, src9, src10]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Never disable certificate verification in production**: `verify=False` (Python), `-k` (curl), `rejectUnauthorized: false` (Node.js) all disable TLS security and enable MITM attacks. Fix the underlying cert issue instead. [src1, src2]
- **TLS 1.2 is the minimum**: TLS 1.0 and 1.1 are deprecated since 2020 and blocked by Chrome 84+, Firefox 78+, Safari 14+. Do not enable them as a "fix". [src2]
- **Always send the full certificate chain**: Servers must include leaf + intermediate(s). Chrome can auto-fetch missing intermediates via AIA, but curl, Python, Java, Go, and other non-browser clients cannot. [src1, src3]
- **Wildcard certs have limits**: `*.example.com` covers `www.example.com` but NOT `example.com` (apex) and NOT `sub.api.example.com` (multi-level). Use multi-SAN certs. [src3, src8]
- **Certificate validity is shrinking — and the 200-day cap is now live**: As of 2026-03-15 the CA/B Forum cap is 200 days; DigiCert/Sectigo/GlobalSign all issue at 199 days (one-day buffer for time-zone safety). Validity drops to 100 days (March 2027) and 47 days (March 2029) per ballot SC-081. Manual renewal is no longer viable. [src6, src9]
- **ClientAuth EKU removal**: From June 2026, Chrome will not trust new public TLS certs with the ClientAuth EKU. Use private CAs for mutual TLS / client certificate authentication. [src6]

## Quick Reference

| # | Error | Browser Message | Root Cause | Fix |
|---|---|---|---|---|
| 1 | Expired certificate | ERR_CERT_DATE_INVALID | Certificate past `notAfter` date | Renew certificate; automate with Let's Encrypt [src1, src4] |
| 2 | Untrusted CA | ERR_CERT_AUTHORITY_INVALID | CA not in trust store | Use trusted CA; install intermediate certs [src1, src3] |
| 3 | Self-signed | NET::ERR_CERT_INVALID | No CA chain | Use Let's Encrypt for production [src1, src7] |
| 4 | Hostname mismatch | ERR_CERT_COMMON_NAME_INVALID | CN/SAN doesn't match domain | Reissue cert with correct SANs [src1, src3] |
| 5 | Incomplete chain | ERR_CERT_AUTHORITY_INVALID | Missing intermediate cert | Install full chain on server [src1, src3] |
| 6 | Protocol mismatch | ERR_SSL_VERSION_OR_CIPHER_MISMATCH | TLS 1.0/1.1 only; no shared cipher | Enable TLS 1.2/1.3 on server [src2, src6] |
| 7 | Certificate revoked | ERR_CERT_REVOKED | CA revoked the cert (OCSP/CRL) | Reissue certificate [src1, src8] |
| 8 | Pinning failure | ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN | HPKP/cert pinning mismatch | Update pinned key or wait for pin expiry [src2] |
| 9 | Mixed content | Mixed Content warning | HTTPS page loads HTTP resources | Change all resource URLs to HTTPS [src1, src2] |
| 10 | Clock skew | ERR_CERT_DATE_INVALID | Client clock wrong | Sync system clock (NTP) [src1, src4] |
| 11 | Wildcard mismatch | ERR_CERT_COMMON_NAME_INVALID | `*.example.com` ≠ `sub.sub.example.com` | Use multi-SAN cert [src3, src8] |

## Decision Tree

```
START — SSL/TLS error in browser or application
├── Run: openssl s_client -connect host:443 -servername host
│   ├── "verify error:num=10:certificate has expired"
│   │   ├── Check expiry: openssl x509 -noout -dates -in cert.pem [src4]
│   │   ├── Server cert expired → renew immediately [src4, src5]
│   │   └── Client clock wrong → sync NTP [src1, src4]
│   ├── "verify error:num=20:unable to get local issuer certificate"
│   │   ├── Incomplete chain → install intermediate certs [src1, src3]
│   │   └── Self-signed → use Let's Encrypt or trusted CA [src1, src7]
│   ├── "verify error:num=18:self signed certificate"
│   │   └── Self-signed cert → replace with CA-signed cert [src1, src7]
│   ├── "hostname mismatch" or "SSL: CERTIFICATE_VERIFY_FAILED"
│   │   ├── Check SANs: openssl x509 -noout -text -in cert.pem | grep -A1 "Subject Alternative" [src3]
│   │   └── Reissue cert with correct hostname(s) [src3, src1]
│   ├── "no shared cipher" or "handshake failure"
│   │   ├── Check TLS versions: openssl s_client -tls1_2 -connect host:443 [src2]
│   │   └── Enable TLS 1.2/1.3 on server; disable TLS 1.0/1.1 [src2, src6]
│   └── "certificate has been revoked"
│       └── Reissue certificate from CA [src1, src8]
└── Works in browser but fails in code?
    ├── Python → check certifi bundle; use ssl.create_default_context() [src2]
    ├── Java → import cert into JVM truststore (keytool) [src2]
    ├── curl → use --cacert or update ca-certificates [src2]
    ├── Node.js → set NODE_EXTRA_CA_CERTS or use https.globalAgent [src2]
    └── Go → add cert to system trust store or use custom tls.Config [src2]
```

## Step-by-Step Guide

### 1. Inspect the certificate with openssl

The single most powerful diagnostic tool for any TLS issue. [src1, src3, src5]

```bash
# Full connection inspection — shows chain, expiry, issuer, verification result
openssl s_client -connect example.com:443 -servername example.com

# Check certificate dates
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

# Check Subject Alternative Names (hostnames covered)
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A5 "Subject Alternative Name"

# Check full certificate details
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text

# Check certificate from a file
openssl x509 -noout -text -in certificate.pem
openssl x509 -noout -dates -in certificate.pem
openssl x509 -noout -issuer -subject -in certificate.pem

# Verify certificate chain
openssl verify -CAfile chain.pem certificate.pem

# Check what TLS versions the server supports
openssl s_client -tls1_2 -connect example.com:443 -servername example.com
openssl s_client -tls1_3 -connect example.com:443 -servername example.com
```

**Verify**: `openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep "Verify return code"` → expected: `Verify return code: 0 (ok)`

### 2. Fix expired certificates

Expired certs must be renewed immediately. Automate to prevent recurrence. [src4, src5, src7]

```bash
# Check days until expiry
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

# Let's Encrypt — free, automated. Default classic profile = 90-day certs today,
# dropping to 64 days on 2027-02-10. The `tlsserver` profile switches to 45-day
# certs on 2026-05-13. A 6-day `shortlived` profile (160-hour validity) is GA
# (Jan 2026) for opt-in users — renew every 2-3 days.
# Install certbot
sudo apt install certbot python3-certbot-nginx  # Ubuntu/Debian
sudo yum install certbot python3-certbot-nginx  # RHEL/CentOS

# Issue certificate (nginx — uses default classic profile)
sudo certbot --nginx -d example.com -d www.example.com

# Issue short-lived (6-day) certificate via the 'shortlived' ACME profile
# Requires ACME client support — certbot 4.0+, acme.sh, lego all support profiles
sudo certbot --nginx -d example.com --preferred-profile shortlived

# Issue certificate (standalone — stops web server temporarily)
sudo certbot certonly --standalone -d example.com

# Auto-renewal (runs twice daily, renews if <30 days left)
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

# Test renewal
sudo certbot renew --dry-run

# Check renewal timer
sudo systemctl status certbot.timer
```

**Verify**: `sudo certbot certificates` → shows domain, expiry date, and certificate path

### 3. Fix incomplete certificate chains

Missing intermediate certs are the #1 cause of "works in browser, fails in code". [src1, src3]

```bash
# Diagnose: check how many certs are in the chain
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"
# Should be 2 (leaf + intermediate) or 3 (leaf + 2 intermediates)
# If 1 → chain is incomplete

# Download intermediate cert from CA's AIA extension
openssl x509 -noout -text -in cert.pem | grep "CA Issuers"
# Output: URI:http://certs.example-ca.com/intermediate.crt
curl -O http://certs.example-ca.com/intermediate.crt

# Build full chain file (order: leaf → intermediate → root)
cat certificate.pem intermediate.pem > fullchain.pem

# nginx — use fullchain
ssl_certificate /etc/ssl/fullchain.pem;
ssl_certificate_key /etc/ssl/private.key;

# Apache — use SSLCertificateChainFile (Apache < 2.4.8)
# or include chain in SSLCertificateFile (Apache >= 2.4.8)
SSLCertificateFile /etc/ssl/fullchain.pem
SSLCertificateKeyFile /etc/ssl/private.key

# Verify chain is complete
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
```

**Verify**: `openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep -c "BEGIN CERTIFICATE"` → expected: `2` or `3`

### 4. Fix hostname mismatches

Certificate must include all hostnames in Subject Alternative Names (SAN). [src1, src3]

```bash
# Check what hostnames the cert covers
openssl x509 -noout -text -in cert.pem | grep -A2 "Subject Alternative Name"
# Output: DNS:example.com, DNS:www.example.com

# Check what hostname you're connecting to
curl -v https://api.example.com 2>&1 | grep "SSL certificate"

# Fix: reissue cert with correct SANs
# Let's Encrypt — add all domains
sudo certbot --nginx -d example.com -d www.example.com -d api.example.com

# For internal/dev: generate self-signed with correct SANs
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 \
  -nodes -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com,DNS:api.example.com"
```

**Verify**: `openssl x509 -noout -text -in cert.pem | grep -A2 "Subject Alternative Name"` → shows all expected domains

### 5. Fix self-signed certificates in code

For development environments with self-signed certs. [src1, src2]

```python
# Python — trust a specific CA cert (development)
import ssl
import urllib.request

context = ssl.create_default_context(cafile='/path/to/ca.pem')
# Or for self-signed (development ONLY — never production):
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

# Better: add self-signed cert to context
context = ssl.create_default_context()
context.load_verify_locations('/path/to/self-signed.pem')
```

```bash
# curl — trust specific CA
curl --cacert /path/to/ca.pem https://internal.example.com

# curl — skip verification (development ONLY)
curl -k https://internal.example.com

# Add CA to system trust store (Ubuntu/Debian)
sudo cp ca.pem /usr/local/share/ca-certificates/my-ca.crt
sudo update-ca-certificates

# Add CA to system trust store (RHEL/CentOS)
sudo cp ca.pem /etc/pki/ca-trust/source/anchors/my-ca.pem
sudo update-ca-trust
```

**Verify**: `curl --cacert /path/to/ca.pem https://internal.example.com` → no SSL errors

### 6. Fix TLS protocol/cipher mismatches

Modern clients require TLS 1.2 minimum; TLS 1.3 is preferred. [src2, src6]

```nginx
# nginx — modern TLS configuration (Mozilla Intermediate profile)
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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# HSTS — tell browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=63072000" always;
```

```apache
# Apache — modern TLS
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...
SSLHonorCipherOrder off
SSLSessionTickets off
Header always set Strict-Transport-Security "max-age=63072000"
```

**Verify**: `openssl s_client -tls1_3 -connect example.com:443 -servername example.com 2>/dev/null | grep "Protocol"` → expected: `Protocol  : TLSv1.3`

## Code Examples

### Comprehensive SSL certificate diagnostic script

> Full script: [comprehensive-ssl-certificate-diagnostic-script.sh](scripts/comprehensive-ssl-certificate-diagnostic-script.sh) (72 lines)

```bash
#!/bin/bash
# Input:  hostname (and optional port, default 443)
# Output: Complete SSL/TLS diagnostic report
HOST="$1"
PORT="${2:-443}"
# ... (see full script)
```

### Python — robust HTTPS client with certificate handling

> Full script: [python-robust-https-client-with-certificate-handli.py](scripts/python-robust-https-client-with-certificate-handli.py) (70 lines)

```python
#!/usr/bin/env python3
"""
Input:  URL to check
Output: SSL certificate info + connection status
"""
# ... (see full script)
```

### Let's Encrypt automation with Certbot (nginx)

> Full script: [let-s-encrypt-automation-with-certbot-nginx.sh](scripts/let-s-encrypt-automation-with-certbot-nginx.sh) (35 lines)

```bash
#!/bin/bash
# Input:  Domain name(s) and email for Let's Encrypt
# Output: Automated certificate issuance + renewal setup
DOMAIN="$1"
EMAIL="$2"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Disabling certificate verification in production

```python
# ❌ BAD — disables all TLS security [src1, src2]
import requests
response = requests.get('https://api.example.com', verify=False)
# Warning: Unverified HTTPS request — this is dangerous!

# ❌ BAD — curl equivalent
curl -k https://api.example.com  # --insecure
```

### Correct: Fix the actual certificate issue

```python
# ✅ GOOD — fix the cert, or trust the specific CA [src1, src2]
import requests

# Option A: Use system CA bundle (default — works for public CAs)
response = requests.get('https://api.example.com')

# Option B: Trust a specific CA cert (internal/private CA)
response = requests.get('https://api.example.com', verify='/path/to/ca-bundle.pem')

# Option C: Add CA to system trust store (permanent fix)
# sudo cp ca.pem /usr/local/share/ca-certificates/my-ca.crt
# sudo update-ca-certificates
```

### Wrong: Serving only the leaf certificate (incomplete chain)

```nginx
# ❌ BAD — only the leaf cert; intermediate missing [src1, src3]
ssl_certificate /etc/ssl/certs/example.com.crt;  # Just the leaf
ssl_certificate_key /etc/ssl/private/example.com.key;
# Works in Chrome (downloads intermediates via AIA)
# Fails in curl, Python, Java, older clients
```

### Correct: Serve the full certificate chain

```nginx
# ✅ GOOD — full chain: leaf + intermediate(s) [src1, src3]
ssl_certificate /etc/ssl/certs/fullchain.pem;  # leaf + intermediate
ssl_certificate_key /etc/ssl/private/example.com.key;

# Build fullchain.pem:
# cat example.com.crt intermediate.crt > /etc/ssl/certs/fullchain.pem
```

### Wrong: Using wildcard cert for multi-level subdomains

```
# ❌ BAD — *.example.com does NOT cover sub.api.example.com [src3, src8]
Certificate SANs: DNS:*.example.com
Accessing: https://sub.api.example.com
→ ERR_CERT_COMMON_NAME_INVALID
```

### Correct: Use multi-SAN certificate

```bash
# ✅ GOOD — explicitly list all needed subdomains [src3, src8]
# Let's Encrypt:
certbot --nginx -d example.com -d www.example.com -d api.example.com -d sub.api.example.com

# Or use a multi-SAN cert from your CA with:
# subjectAltName = DNS:example.com, DNS:*.example.com, DNS:*.api.example.com
```

### Wrong: Ignoring certificate validity timeline changes

```bash
# ❌ BAD — assuming 398-day certs are still available [src6, src9]
# Buying a 1-year cert after March 15, 2026
# → Max validity is 200 days (DigiCert/Sectigo/GlobalSign issue at 199)
# → Manual annual renewal no longer works
# → 47-day certs in 2029 mean ~8 renewals per year
```

### Correct: Automate certificate lifecycle with ACME

```bash
# ✅ GOOD — fully automated certificate management [src6, src7]
# certbot handles issuance + renewal automatically
sudo certbot --nginx -d example.com --non-interactive --agree-tos --email admin@example.com
sudo systemctl enable certbot.timer

# For shorter-lived certs, use ACME Renewal Information (ARI)
# ARI tells the client exactly when to renew — supported by certbot 2.8+
```

## Common Pitfalls

- **Incomplete chain is the #1 silent failure**: Chrome downloads missing intermediates via AIA (Authority Information Access), so it "works" in Chrome. But curl, Python's `requests`, Java's `HttpsURLConnection`, and older clients fail. Always serve the full chain. [src1, src3]
- **Let's Encrypt lifetime reduction is happening now**: The `tlsserver` ACME profile starts issuing 45-day certs on **2026-05-13**, and the default classic profile drops to 64 days on **2027-02-10**. A 6-day `shortlived` profile (160-hour validity) went GA on **2026-01-15** for users who want maximum security via short-lived certs (renew every 2–3 days). Set up `certbot renew` as a systemd timer and use ACME Renewal Information (ARI, certbot 2.8+) for optimal timing. Manual renewal will not scale. [src7, src10]
- **Clock skew causes false expiry errors**: If the client's clock is wrong by more than a few minutes, valid certificates appear expired or not-yet-valid. Always check `date` on the client before debugging the cert. [src1, src4]
- **SNI (Server Name Indication) required**: Servers hosting multiple domains on one IP must use SNI. Always pass `-servername` to `openssl s_client`. Without it, you may get the wrong certificate. [src2, src3]
- **HSTS prevents HTTP fallback**: Once a browser has seen `Strict-Transport-Security`, it refuses HTTP for that domain for the `max-age` duration. If your cert breaks, users can't bypass it. Test HSTS carefully before setting long `max-age`. [src2]
- **Wildcard certs don't cover the apex**: `*.example.com` covers `www.example.com` but NOT `example.com` itself. Include both in your SAN list. [src3, src8]
- **ClientAuth EKU removal (June 2026)**: Chrome will stop trusting public TLS certs with ClientAuth EKU. Organizations using public certs for mutual TLS must switch to private CAs or S/MIME certs. [src6]
- **200-day max validity is LIVE (March 2026)**: Major CAs have already enforced the cap — DigiCert on 2026-02-24, Sectigo on 2026-03-12, GlobalSign on 2026-03-14. They issue at **199 days** (not 200) as a time-zone safety buffer. Existing 398-day certs remain valid until natural expiry. Plan for ~2× more frequent renewals or automate with ACME. [src6, src9]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (26 lines)

```bash
# === Core diagnostics ===
# Full TLS inspection
openssl s_client -connect host:443 -servername host
# Certificate dates
openssl s_client -connect host:443 -servername host 2>/dev/null | openssl x509 -noout -dates
# Subject Alternative Names
openssl s_client -connect host:443 -servername host 2>/dev/null \
  | openssl x509 -noout -text | grep -A3 "Subject Alternative Name"
# Chain count (should be 2+)
openssl s_client -connect host:443 -servername host 2>/dev/null | grep -c "BEGIN CERTIFICATE"
# Verify chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
# TLS version testing
openssl s_client -tls1_2 -connect host:443 -servername host
openssl s_client -tls1_3 -connect host:443 -servername host
# curl verbose
curl -v https://host 2>&1 | grep -E "SSL|TLS|certificate|expire"
curl --cacert /path/to/ca.pem https://internal-host
# Days until expiry
echo | openssl s_client -connect host:443 -servername host 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2
# OCSP revocation check
openssl s_client -connect host:443 -servername host -status 2>/dev/null | grep -A3 "OCSP"

# === Online tools ===
# SSL Labs: https://www.ssllabs.com/ssltest/
# Chain check: https://whatsmychaincert.com/
# Certificate Transparency: https://crt.sh/
```

## Version History & Compatibility

| Version / Standard | Behavior | Notes |
|---|---|---|
| TLS 1.3 (RFC 8446, 2018) | Current best | Faster handshake; forward secrecy mandatory; 93% of Cloudflare traffic (2025) [src2] |
| TLS 1.2 (RFC 5246, 2008) | Required minimum | Widely supported; still secure with modern ciphers [src2] |
| TLS 1.1 (RFC 4346, 2006) | Deprecated | Disabled in Chrome 84+, Firefox 78+, Safari 14+ [src2] |
| TLS 1.0 (RFC 2246, 1999) | Deprecated | Disabled in all modern browsers (2020) [src2] |
| SSL 3.0 | Broken (POODLE) | Never use; disabled everywhere [src2] |
| Let's Encrypt (2015+) | Free, automated | 90-day certs default; 6-day `shortlived` profile GA Jan 2026; tlsserver → 45-day on 2026-05-13; classic → 64-day on 2027-02-10 [src4, src7, src10] |
| Chrome 58+ (2017) | CN deprecated | Only checks SANs, ignores Common Name [src3] |
| Chrome 80+ (2020) | TLS 1.0/1.1 blocked | ERR_SSL_VERSION_OR_CIPHER_MISMATCH for old TLS [src2] |
| CA/B Forum SC-081 (2025) | Validity shrinking | 200 days (Mar 2026, LIVE) → 100 days (2027) → 47 days (2029) [src6] |
| DigiCert / Sectigo / GlobalSign (Feb–Mar 2026) | 199-day cap enforced | DigiCert 2026-02-24, Sectigo 2026-03-12, GlobalSign 2026-03-14 — all issue at 199 days for time-zone safety [src9] |
| Chrome 138+ (June 2026) | ClientAuth EKU removed | Public TLS certs can no longer support client authentication [src6] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Browser shows certificate error | CORS error (different issue) | Fix CORS headers on server |
| `openssl s_client` shows verify error | Connection refused (port closed) | Check firewall/service |
| Certificate expired or expiring | DNS resolution failure | Fix DNS records |
| Hostname mismatch in error | HTTP 4xx/5xx errors | Debug application logic |
| "Works in browser, fails in code" | WebSocket errors after handshake | Debug application-level WS protocol |

## Important Caveats

- **Never use `verify=False` or `-k` in production code**: It disables all TLS security and makes MITM attacks trivial. Fix the underlying certificate issue instead. [src1, src2]
- **Certificate Transparency (CT) logs**: Since 2018, all publicly-trusted CAs must log certificates to CT logs. Certificates not in CT logs are rejected by Chrome. This is automatic for Let's Encrypt and major CAs. Use [crt.sh](https://crt.sh) to search CT logs. [src8]
- **OCSP Must-Staple**: If your certificate has the `must-staple` extension, your server must provide a valid OCSP staple or clients will reject the connection. Ensure your web server is configured to staple OCSP responses. [src8]
- **Private key security**: The private key must never be shared or committed to version control. If compromised, revoke the certificate immediately via your CA and reissue. [src1]
- **CAA DNS records**: Add `CAA` DNS records to restrict which CAs can issue certificates for your domain. This prevents unauthorized certificate issuance. Example: `example.com. CAA 0 issue "letsencrypt.org"` [src8]
- **Java uses its own truststore**: Java applications use `$JAVA_HOME/lib/security/cacerts`, not the OS trust store. Add custom CAs with `keytool -importcert`. [src2]
- **Certificate validity timeline (2026-2029)**: The 200-day cap is now active (DigiCert 2026-02-24, Sectigo 2026-03-12, GlobalSign 2026-03-14 all issue at 199 days). CA/B Forum ballot SC-081 mandates further drops to 100 days from March 2027 and 47 days from March 2029. Let's Encrypt's `tlsserver` profile cuts to 45-day certs on 2026-05-13; the default classic profile drops to 64 days on 2027-02-10. A `shortlived` 6-day profile is GA for opt-in users. Plan accordingly. [src6, src7, src9, src10]

## Related Units

- [Browser CORS Errors](/software/debugging/browser-cors-errors/2026)
- [Kubernetes CrashLoopBackOff](/software/debugging/kubernetes-crashloopbackoff/2026)
- [Node.js ECONNREFUSED](/software/debugging/nodejs-econnrefused/2026)
