SSL/TLS Certificate Expired: Diagnosis and Renewal Guide
Quick answer
💡An expired SSL/TLS certificate causes browsers and API clients to refuse connections with ERR_CERT_DATE_INVALID or SSL_ERROR_EXPIRED_CERT_ALERT. Run certbot renew --force-renewal for an immediate fix, then verify that your systemd timer or cron job is active and that a post-hook reloads NGINX or Apache. For Kubernetes, deploy cert-manager with a Let's Encrypt ClusterIssuer so certificates renew automatically 30 days before expiry.
Error symptoms
- ✕
ERR_CERT_DATE_INVALID in Chrome — NET::ERR_CERT_DATE_INVALID displayed in red lock - ✕
curl: (60) SSL certificate problem: certificate has expired - ✕
javax.net.ssl.SSLHandshakeException: PKIX path validation failed: validity check failed - ✕
SSL_ERROR_EXPIRED_CERT_ALERT in Firefox developer console - ✕
x509: certificate has expired or is not yet valid in Go service logs - ✕
OpenSSL error X509_V_ERR_CERT_HAS_EXPIRED (code 10) in server or reverse-proxy logs
Common causes
- •Let's Encrypt 90-day certificate with a broken certbot auto-renewal timer or failed ACME challenge
- •Wildcard certificate from a commercial CA renewed in the dashboard but deployment step was skipped
- •Kubernetes TLS secret managed manually, rotated once at launch and then forgotten
- •Cloudflare proxy masking an expired origin certificate until DNS mode was switched to DNS-only
- •Staging certificate renewed in one environment but not propagated to other regions or replicas
- •ACME DNS-01 challenge failing silently because the DNS provider API key was rotated
When it happens
- •Exactly 90 days after a Let's Encrypt certificate was issued when renewal automation was never tested
- •After migrating servers or changing reverse-proxy configuration that broke the certbot webroot path
- •When a wildcard certificate covers both production and staging but monitoring only watched production
- •After rotating cloud provider credentials, breaking DNS-01 challenge renewal for wildcard certificates
- •During incident response when a certificate was manually force-renewed in one region but not all
Examples and fixes
A common setup where certbot renew runs but NGINX is never reloaded, so the server keeps serving the old expired certificate that was loaded at startup.
Certbot renewal with NGINX reload hook
❌ Wrong
# /etc/cron.d/certbot
0 */12 * * * root certbot renew --quiet
# Problem: NGINX is never signaled after renewal.
# The new certificate is on disk but the process
# keeps serving the expired cert loaded at startup.
# Users see ERR_CERT_DATE_INVALID until next restart.✅ Fixed
# /etc/cron.d/certbot
0 */12 * * * root certbot renew --quiet \
--deploy-hook "systemctl reload nginx"
# --deploy-hook runs only when at least one cert
# was actually renewed, avoiding unnecessary reloads.
# For graceful reload with zero downtime use reload,
# not restart. Verify with: nginx -t before deploy.Certbot writes new certificate files to disk but does not signal running processes. NGINX reads certificate files only at startup or on an explicit reload signal. Without a deploy-hook, the old expired certificate stays in memory indefinitely. Using --deploy-hook is preferred over --post-hook because it runs only when at least one certificate was actually renewed, not on every invocation. The --pre-hook stop and --post-hook start pattern introduces a brief downtime window; prefer --deploy-hook reload in production environments.
Manually managed TLS Secrets in Kubernetes expire silently. A cert-manager Certificate resource renews the secret automatically 30 days before expiry.
cert-manager Certificate resource for Kubernetes
❌ Wrong
# Manual TLS Secret — no auto-renewal mechanism
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: production
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # base64 cert, will expire
tls.key: LS0tLS1CRUdJTi... # base64 key✅ Fixed
# cert-manager Certificate — auto-renews 30 days before expiry
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-tls
namespace: production
spec:
secretName: example-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
renewBefore: 720hA manually created Kubernetes TLS Secret has no renewal mechanism. When the certificate inside it expires, every Ingress using that Secret will serve the expired certificate to all clients. cert-manager watches Certificate resources and initiates an ACME challenge when remaining validity falls below the renewBefore threshold (default is one-third of total validity, overridden here to 30 days via 720h). The resulting Secret is kept up to date automatically. cert-manager also emits Kubernetes events and Prometheus metrics you can alert on for complete observability.
Why SSL Certificates Expire Unexpectedly
Every X.509 certificate carries a notAfter field that is verified by every TLS implementation. Once the current UTC time passes that timestamp, every compliant client — browsers, curl, Java's PKIX validator, Go's crypto/tls package, and Python's ssl module — rejects the handshake with a fatal alert. The certificate itself has not changed; only the wall clock has moved past its validity window.
Let's Encrypt deliberately issues short 90-day certificates to encourage automation. The certbot client sets up a systemd timer (certbot.timer) or a cron job under /etc/cron.d/certbot. This renewal process attempts to renew certificates expiring within the next 30 days, running twice per day. The design works perfectly until something breaks it. Common breakage points include: the ACME HTTP-01 challenge failing because a firewall rule now blocks port 80, the webroot path for the challenge being wrong after an NGINX reconfiguration, or the certbot binary being updated in a way that moved the configuration file location.
Commercial certificates from DigiCert, Sectigo, or AWS ACM have longer validity periods (the CA/Browser Forum capped maximum validity at 398 days in 2020 and is moving toward 90-day maximums by 2027). These are usually renewed through a dashboard, and the deployment step — uploading the new certificate to load balancers or web servers — is easy to forget or misalign between environments.
Cloudflare Universal SSL adds a layer of complexity. Cloudflare terminates TLS at its edge, issuing its own certificate to clients. Your origin certificate can expire without end users seeing any error, right up until Cloudflare stops proxying (for example, when you switch a DNS record to DNS-only mode). This creates a false sense of security where your monitoring shows green but your origin certificate is months past expiry.
In containerized environments, certificates stored as Kubernetes Secrets or Docker Swarm secrets are static blobs. There is no built-in rotation mechanism. cert-manager solves this by introducing Certificate custom resources that watch validity and trigger ACME or Vault renewal automatically. Without it, every certificate requires a manual rotation pipeline that teams routinely forget under operational pressure.
How to Check Expiry and Diagnose Renewal Failures
The fastest command-line diagnosis is openssl s_client. Run openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates and you will see notBefore and notAfter in UTC. Always include -servername when checking domains behind a load balancer or CDN — without SNI you may receive the default certificate rather than the specific one you intend to inspect.
For scripted monitoring, use the openssl x509 -checkend flag which exits with code 1 if the certificate will expire within N seconds. The command openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -checkend 2592000 exits non-zero if expiry is within 30 days (2592000 seconds). Wire this into a monitoring cron job or a Nagios/Icinga check_ssl_certificate plugin with -w 30 -c 14 thresholds.
For Let's Encrypt renewals that are silently failing, inspect the certbot systemd service: systemctl status certbot.timer and journalctl -u certbot --since '30 days ago'. The log will show whether the ACME challenge succeeded or what error certbot encountered. The most common failure is HTTP-01 returning a 404, meaning the /.well-known/acme-challenge/ path is not reachable. Verify no NGINX rewrite rule is intercepting that path before hitting the webroot directory.
For Kubernetes cert-manager, kubectl describe certificate example-tls -n production shows the Ready condition and the last renewal timestamp. kubectl describe certificaterequest and kubectl describe order will surface the ACME challenge status. A challenge stuck in pending usually means a DNS propagation problem for DNS-01 challenges or a cert-manager webhook misconfiguration.
For Cloudflare-managed domains, check both the edge certificate and the origin certificate separately. The Cloudflare dashboard under SSL/TLS > Edge Certificates shows Universal SSL expiry. To check the origin certificate, temporarily set the DNS record to DNS-only (bypassing the proxy) and re-run openssl s_client, then re-enable proxying immediately after.
Renewing Certificates: certbot, Cloudflare, cert-manager
For a Let's Encrypt certificate managed by certbot, the immediate fix is certbot renew --force-renewal followed by systemctl reload nginx or systemctl reload apache2. Avoid --force-renewal in routine operation — it renews even certificates not near expiry and counts against Let's Encrypt's rate limit of 5 duplicate certificates per 7 days. If you use the standalone authenticator, stop the web server first since certbot standalone opens its own listener on port 80.
For Cloudflare, set the SSL/TLS encryption mode to Full (strict) under SSL/TLS > Overview. Cloudflare will renew its Universal SSL certificate automatically. For origin certificates, use Cloudflare Origin CA certificates (valid up to 15 years, generated under SSL/TLS > Origin Server). These certificates are trusted only by Cloudflare's proxies — they are not browser-trusted when accessed directly. This is intentional and safe for Cloudflare-proxied services.
For Kubernetes with cert-manager, if a Certificate resource is stuck, delete the CertificateRequest to force re-issuance: kubectl delete certificaterequest -n production -l cert-manager.io/certificate-name=example-tls. cert-manager creates a new CertificateRequest immediately. To force renewal before the automatic window triggers, annotate the certificate: kubectl annotate certificate example-tls cert-manager.io/force-renewal="$(date)" -n production.
For multi-region deployments, use a centralized secrets store (AWS Secrets Manager, HashiCorp Vault) to hold renewed certificates and trigger Lambda functions or Ansible playbooks to deploy to all regions after renewal. Never update certificates in one region and rely on manual memory for the others.
Security warning: when HSTS is active with a long max-age, certificate expiry is more damaging than without HSTS. Browsers that have cached the HSTS policy refuse HTTP connections even if you attempt a temporary HTTP fallback during renewal. Always ensure your renewal pipeline is automated and tested before configuring HSTS max-age values above 300 seconds, and never set includeSubDomains until every subdomain has reliable certificate automation in place.
Edge Cases: HSTS Preload, Wildcards, Clock Skew
HSTS (HTTP Strict Transport Security) with max-age=31536000 and includeSubDomains means that once a browser has visited your site, it will refuse to make an HTTP connection for up to one year. If your certificate expires and renewal fails, you cannot serve even a plain HTTP fallback page to users who have cached the HSTS policy. The only recovery path without a valid certificate is to wait for the max-age to expire or contact browser vendors to remove your domain from the HSTS preload list — a process that takes weeks. Only enable HSTS preloading after you have verified that certificate automation is reliable and tested under realistic failure scenarios.
Wildcard certificates (*.example.com) cover all direct subdomains but not sub-subdomains (staging.api.example.com is not covered by *.example.com). They require DNS-01 ACME challenges because the HTTP-01 challenge cannot prove ownership of a wildcard name. DNS-01 requires API access to your DNS provider. Rotating those API credentials without updating certbot's configuration will silently break wildcard renewals. Maintain a separate alert for DNS provider credential expiry, since cloud IAM tokens and API keys often have their own expiry dates independent of certificate expiry.
Clock skew between servers and clients causes rare but confusing certificate errors. If a server's clock is several minutes ahead of UTC, a freshly issued certificate may not yet be valid from a client's perspective reading the notBefore field. Conversely, if a client's clock is behind, it may incorrectly accept an expired certificate. Keep NTP synchronization enabled (timedatectl status should show synchronized: yes). On AWS EC2, use the Amazon Time Sync Service at 169.254.169.123 for low-jitter synchronization.
AWS Certificate Manager (ACM) auto-renews certificates it issues, but imported third-party certificates are not renewed automatically. ACM sends expiry email notifications 45, 30, 15, and 1 day before expiry. After renewing an imported certificate manually, you must re-import it into ACM and update every service reference (ALB listeners, CloudFront distributions) pointing to it.
For mTLS (mutual TLS) setups where both server and client present certificates, client certificate expiry often goes unnoticed because there is no end-user browser warning — the server simply rejects the connection with a TLS alert. Monitor client certificate expiry the same way you monitor server certificates, especially for service-to-service communication in microservices architectures.
Certificate Renewal Mistakes That Cause Outages
The most frequent mistake is never testing the certbot renewal timer after installation. Installing certbot creates the automation, but that automation has never actually renewed a certificate until it first runs around day 60. Run certbot renew --dry-run immediately after setup to verify the challenge mechanism works end to end. Schedule a recurring calendar reminder to re-run the dry run after any server or DNS reconfiguration.
Using certbot --standalone in an environment where port 80 is occupied by an application server causes silent failure. The standalone authenticator binds to port 80 to respond to ACME HTTP-01 challenges. If your application is already listening on port 80, certbot fails with Address already in use. Use the webroot authenticator pointing certbot at the document root your web server already serves, or use the DNS-01 authenticator to avoid port conflicts entirely.
Using a single SAN certificate for both internet-facing and internal services creates renewal complexity. Internal services on RFC-1918 addresses or .internal domains cannot participate in public ACME HTTP-01 challenges. Use your internal CA or cert-manager with a Vault ClusterIssuer for internal services. Mixing them into one certificate means the entire certificate must renew through a process that requires public internet connectivity.
Serving incomplete certificate chains is a common source of intermittent errors. A valid leaf certificate without its intermediate CA chain will be accepted by most browsers (which cache intermediates from prior visits) but will fail for many automated API clients and older OpenSSL builds. Verify the full chain with openssl s_client -connect example.com:443 -showcerts. If only one certificate is printed, configure your server to serve the full chain (leaf plus intermediates concatenated) in the ssl_certificate directive.
Storing private keys in version control, unencrypted S3 buckets, or shared secret managers without access control is a critical security mistake. When renewing a certificate, certbot generates a new private key by default (--reuse-key disables this). Reusing an old private key means a past compromise of that key still compromises future traffic. Treat private keys with the same access controls as database credentials or API signing keys.
Production Certificate Management Best Practices
Automate everything and treat manual certificate management as a temporary emergency measure. Let's Encrypt, AWS ACM, and Google-managed SSL certificates all provide free automated renewal. The only legitimate reasons to manage certificates manually today are Extended Validation (EV) organizational requirements or strict change control policies. Even then, automate the deployment step through CI/CD pipelines that detect new certificates and run the deploy-and-reload workflow without human involvement.
Monitor expiry independently of your renewal tooling. Configure a synthetic monitor (Datadog, Uptime Robot, Better Uptime, or a curl-based cron job) that checks certificate expiry from outside your network. This catches cases where renewal succeeded locally but the new certificate was never deployed to the load balancer or CDN edge. Alert at 30 days as a warning and 14 days as a critical alert. Never wait until 7 days — ACME rate limits may block recovery if previous failed attempts consumed your rate limit budget.
For multi-region deployments, confirm certificate renewal propagates to all regions. AWS ACM certificates on CloudFront replicate globally automatically. For certificates on EC2 instances across multiple regions, use AWS Secrets Manager or HashiCorp Vault to store renewed certificates and Lambda or Ansible to deploy them consistently. Treat certificate deployment as infrastructure code, not an ad-hoc operational step.
Enable OCSP stapling to reduce TLS handshake latency and improve client privacy. Without stapling, clients query the CA's OCSP responder during every new TLS session, adding a network round-trip and revealing to the CA which clients connect to your site. With OCSP stapling (ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8; in NGINX), your server caches the OCSP response and serves it directly to clients.
For HSTS, follow the OWASP recommendation of max-age=31536000; includeSubDomains; preload for production services, but only after confirming your certificate automation is reliable. Start with max-age=300 for the first two weeks, verify no issues, then increase gradually. Never enable includeSubDomains until every subdomain in your zone has working HTTPS. Submit to the HSTS preload list only after you are certain certificate management will remain automated indefinitely, as removal from the preload list is slow and difficult.
Quick fix checklist
- ✓Run openssl s_client -connect domain.com:443 to confirm expiry and see exact notAfter timestamp
- ✓Run certbot renew --dry-run to test the renewal process without modifying anything
- ✓Run certbot renew --force-renewal then systemctl reload nginx to immediately renew and deploy
- ✓Check journalctl -u certbot for silent renewal failures in the past 30 days
- ✓Verify the ACME challenge path /.well-known/acme-challenge/ returns HTTP 200 from a remote machine
- ✓For Kubernetes, run kubectl describe certificate and delete any stuck CertificateRequest objects
- ✓Set up a monitoring alert at 30-day and 14-day expiry thresholds on all production certificates
- ✓Verify HSTS max-age is set conservatively (start with 300s) until renewal automation is fully tested
Related guides
- → SSL Handshake Error Fix
- → Content Security Policy Errors
- → JWT Decoder Tool
- → Base64 Encode/Decode Tool
Frequently asked questions
How do I check when my SSL certificate expires without a browser?
Run openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates in your terminal. The notAfter field shows the exact expiry time in UTC. You can also use curl -vI https://yourdomain.com 2>&1 | grep -i expire to see the expiry date in curl's verbose SSL output without needing to parse OpenSSL directly.
Why does certbot renew say the certificate is not due for renewal yet?
Certbot only renews certificates with fewer than 30 days of validity remaining by default. To force immediate renewal regardless of remaining validity, run certbot renew --force-renewal. Use caution: repeated force renewals count against Let's Encrypt's rate limit of 5 duplicate certificates per 7 days for the same domain. Always run --dry-run first to confirm the challenge mechanism works before forcing renewal.
Does Cloudflare automatically renew SSL certificates?
Cloudflare Universal SSL certificates (edge certificates facing end users) are renewed automatically by Cloudflare with no action required. However, your origin certificate — between Cloudflare and your backend server — is not managed by Cloudflare unless you use Cloudflare Origin CA. If you installed a third-party certificate at your origin, you are responsible for renewing it before expiry.
What is cert-manager and when should I use it for Kubernetes?
cert-manager is a Kubernetes add-on that automates the management and issuance of TLS certificates from Let's Encrypt, HashiCorp Vault, Venafi, or self-signed CAs. Use it whenever you have TLS Secrets in Kubernetes that would otherwise need manual rotation. It integrates with Ingress resources via the cert-manager.io/cluster-issuer annotation and emits Prometheus metrics for expiry monitoring.
Can an expired certificate be used temporarily while renewing?
No. Every standards-compliant TLS client rejects expired certificates without exception. If renewal is failing, fix the root cause as a priority. A self-signed certificate can serve as a temporary stopgap in development environments, but it produces browser security warnings in production and breaks automated clients. There is no valid bypass for expired certificates in a production system.
What are the HSTS implications of letting a certificate expire?
HSTS caches in browsers for up to max-age seconds. An expired certificate means no valid HTTPS, but HSTS also prevents HTTP fallback, giving users a hard error with no bypass option. For HSTS-preloaded domains the situation is worse: recovery requires contacting browser vendors to remove the preload entry, which takes weeks. This is why HSTS preloading should only be enabled after fully automated, tested certificate renewal is in place.
How do I monitor certificate expiry across many services automatically?
Use the Prometheus ssl_exporter to scrape certificate expiry metrics from all HTTPS endpoints and alert when days-remaining drops below your threshold. A simpler alternative is a shell script using openssl x509 -checkend combined with a monitoring cron job. Commercial tools like Datadog Synthetics, New Relic synthetic monitors, and Pingdom include built-in SSL expiry monitoring that requires no custom scripting.
Why did my certificate expire even though certbot was installed?
The most common reasons are: the certbot systemd timer was disabled after a system update, the ACME HTTP-01 challenge started failing because of a firewall rule or web server configuration change, or Let's Encrypt rate limits blocked renewal after too many previous failed attempts. Run journalctl -u certbot and certbot renew --dry-run to surface the specific failure message and identify the exact point where automation broke.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.