Expired SSL Certificate in Production

Beginner Security

Your HTTPS site is showing a browser security warning and users cannot connect because the TLS certificate has passed its expiry date. Search engines may also begin deindexing the site, and APIs clients will start rejecting connections with certificate validation errors.

Symptoms

  • Browser displays 'Your connection is not private' or NET::ERR_CERT_DATE_INVALID
  • curl returns 'SSL certificate problem: certificate has expired'
  • API clients throw SSLError or certificate verification failed exceptions
  • Monitoring tools report HTTPS health check failures
  • openssl s_client output shows 'Verify return code: 10 (certificate has expired)'

Possible Root Causes

  • Auto-renewal cron job or systemd timer for certbot/acme.sh disabled or failing silently
  • HTTP-01 ACME challenge blocked by firewall rule, Cloudflare setting, or redirect loop
  • Certificate was manually installed (not via certbot) and renewal was not scheduled
  • Domain validation failed during renewal because DNS record changed or domain expired
  • Certificate authority changed their ACME endpoint and the client was not updated

Diagnosis Steps

1. Check the current certificate expiry date

# Quick expiry check (replace yourdomain.com)
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
  | openssl x509 -noout -dates

# Output example:
# notBefore=Jan  1 00:00:00 2024 GMT
# notAfter=Jan  1 00:00:00 2025 GMT   <-- expired

2. Check certificate details (issuer, SANs, chain)

echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A2 "Subject Alternative Name"

# Check the full chain
echo | openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | grep -E "BEGIN|END|subject|issuer"

3. Check if certbot has a valid renewed cert locally

# List all certbot-managed certificates and their expiry
sudo certbot certificates

# Check if a timer/cron for renewal is active
sudo systemctl status certbot.timer
sudo systemctl list-timers | grep certbot

# See certbot renewal logs
sudo journalctl -u certbot --no-pager -n 50

4. Verify the cert file on disk

# Check what certificate nginx or apache is currently serving
sudo nginx -T 2>/dev/null | grep ssl_certificate
sudo apache2ctl -S 2>/dev/null | grep ssl

# Inspect the cert file on disk
sudo openssl x509 -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem -noout -dates

Solution

Option A: Renew with certbot (Let's Encrypt)

# Test renewal in dry-run mode first
sudo certbot renew --dry-run

# If dry-run succeeds, force a real renewal
sudo certbot renew --force-renewal

# Reload the web server to pick up the new certificate
sudo systemctl reload nginx    # or: sudo systemctl reload apache2

Option B: Manual renewal if ACME challenge is failing

# Use DNS-01 challenge if HTTP-01 is blocked (no need to open port 80)
sudo certbot certonly --manual --preferred-challenges dns -d yourdomain.com

# You will be prompted to add a TXT record:
# _acme-challenge.yourdomain.com TXT "xxxxxxx"
# After adding the DNS record, press Enter to complete validation.

Verify the new certificate is being served

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

# Confirm not-after is in the future (should be ~90 days from now for Let's Encrypt)

Re-enable the certbot timer

sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
sudo systemctl status certbot.timer

Prevention

  • Use certbot with systemd timer: The certbot.timer unit attempts renewal twice daily — ensure it is enabled and not masked.
  • Monitor expiry proactively: Use the IPFYI SSL Checker or an external monitor (UptimeRobot SSL monitoring, Cronitor) to alert you 30 days before expiry.
  • Test renewal in staging: Run certbot renew --dry-run monthly in a maintenance window to confirm the ACME challenge path is clear.
  • Avoid manual cert installs: Always use an automated client (certbot, acme.sh, Caddy's built-in ACME) so renewal is automatic.
  • Check Cloudflare proxy settings: If behind Cloudflare, ensure 'Full (strict)' SSL mode is set and the origin cert is valid, or use an origin certificate issued by Cloudflare.

Related Protocols

Related Terms

More in Security