What the error actually says

Every certificate carries two timestamps: notBefore and notAfter. Outside that window, a browser refuses it. NET::ERR_CERT_DATE_INVALID is Chrome saying that, by the clock on this device, the certificate is outside its window: either expired, or not valid yet.

Note the “by the clock on this device”. The browser has no other source of time. A perfectly good certificate looks expired to a computer that thinks it’s 2031, and a certificate issued this morning looks not-yet-valid to a computer that thinks it’s 2019.

Firefox shows the same thing as SEC_ERROR_EXPIRED_CERTIFICATE (expired) or MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE (not valid yet), and SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE when the expired certificate is an intermediate rather than the site’s own.

First question: one device, or everyone?

This splits the problem in two, and it’s worth answering before anything else.

  • Every site fails on one device (Google, Naver, your bank): the device clock is wrong. The sites are fine.
  • One site fails on every device, including your phone on mobile data: the site’s certificate has expired, or the site is serving an old one. The site owner has to fix it.
  • One site fails on one device: usually still the clock, off by hours rather than years, combined with a certificate that is either brand new or about to expire.

The SSL check shows the certificate’s validity dates as seen from outside your network. If it says the certificate is valid and your browser disagrees, look at your clock.

If you’re the visitor: fix the clock

Chrome sometimes says it outright (“Your clock is behind” or “Your clock is ahead”). Even when it doesn’t, the error page’s details show the date your computer currently believes. Compare it with the real date.

  • Windows: Settings → Time & language → Date & time. Turn on “Set time automatically”, check the time zone, then press “Sync now”. From an admin command prompt, w32tm /resync does the same.
  • macOS: System Settings → General → Date & Time → “Set time and date automatically”.
  • Linux: timedatectl shows System clock synchronized: yes when NTP is working. If not, sudo timedatectl set-ntp true.
  • Android and iPhone: turn on automatic date and time in Settings. A phone with a manually set date is the usual culprit when only the phone fails.

Two patterns to recognise:

  • The clock resets to an old date after every power-off. That’s a dead CMOS battery on a desktop motherboard. Syncing fixes it until the next cold start; replacing the coin cell fixes it for good. Most Raspberry Pi models have no battery-backed clock either, so they boot with a stale time until they reach an NTP server.
  • The time looks right, but the time zone is wrong. If the displayed time is correct but the zone is set to, say, US Pacific instead of Seoul, the computer’s idea of UTC is off by many hours. That only bites with certificates issued in the last few hours or expiring in the next few, which is why it seems random.

If a whole office or fleet of devices shows the error at once on sites that work elsewhere, suspect the shared time source: an NTP server that drifted, or a router handing out a bad one.

If it’s your site: read the dates the server actually sends

Check what goes out on the wire, for the exact name that fails:

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

notAfter=Oct 7 00:00:00 2026 GMT is in UTC. Compare it with date -u, not with your wall clock.

Then compare it with the file on disk:

sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates

The two results tell you which of the following you have:

  • Both old: renewal failed. Fix the renewal.
  • Disk new, wire old: the renewal worked, but the server is still serving the old certificate from memory, or something in front of it is.
  • Both new and valid, and only some users complain: go back to the clock section; their devices are wrong.

Why it expired when you thought renewal was automatic

Renewal failed and nobody noticed

Let’s Encrypt’s standard certificates last 90 days and certbot by default renews them with about 30 days to spare, so a broken renewal gives you about a month of silence before anything visible happens. Let’s Encrypt also stopped sending expiry reminder emails in 2025, so don’t count on a warning from them.

Check the state of things:

sudo certbot certificates          # every certificate and its expiry date
sudo certbot renew --dry-run       # does renewal work right now?
systemctl list-timers | grep -i certbot

The timer is certbot.timer with distribution packages and snap.certbot.renew.timer with the snap. If there’s no timer and no cron entry, nothing has been renewing anything.

Typical reasons a renewal fails:

  • HTTP-01 validation can’t reach the server. Port 80 is closed in the firewall, or a redirect sends /.well-known/acme-challenge/ somewhere that doesn’t serve it.
  • The --standalone method can’t bind port 80 because nginx or Apache is now running on it.
  • DNS moved. The name now points at a CDN or another server, so the validation request never reaches the machine running certbot.
  • DNS-01 credentials expired. The API token for your DNS provider was rotated or revoked.

The log at /var/log/letsencrypt/letsencrypt.log says which one.

Renewed on disk, still old on the wire

nginx and Apache read certificates when they start or reload. A new file on disk does nothing until then.

Certbot’s nginx and Apache plugins reload the server for you. With certonly --webroot or --standalone, they don’t, so you need a deploy hook:

sudo certbot renew --deploy-hook "systemctl reload nginx"

To make that permanent, put an executable script in /etc/letsencrypt/renewal-hooks/deploy/. The same applies to everything else that holds the certificate in memory: mail servers (Postfix, Dovecot), HAProxy, Java applications with their own keystore.

Another copy you forgot about

The certificate the browser sees comes from whatever terminates TLS, and that’s often not the server you renewed.

  • A CDN with an uploaded certificate. Cloudflare renews its own edge certificates, but a custom certificate you uploaded stays exactly as you uploaded it. The same goes for certificates imported into AWS Certificate Manager: ACM renews certificates it issued itself, not imported ones.
  • A load balancer with its own copy of the certificate.
  • Several servers behind one name. You renewed one; the others still serve the old certificate, so the error comes and goes depending on which server you hit.

Running openssl s_client against each server’s IP with the same -servername shows which one is behind.

“Not yet valid”

The opposite case is rarer and almost always about time on one side or the other.

  • A brand-new certificate and a client clock that’s behind. CAs usually set notBefore slightly in the past to absorb small clock errors (Let’s Encrypt backdates by about an hour), but a device that is a day behind will still reject it.
  • A certificate deployed before its start date. Some commercial CAs let you choose a future start date. If you installed it early, wait, or reissue.

One thing that does not matter here: your server’s clock. The browser judges the certificate by its own clock, never the server’s. (Keep NTP running on the server anyway; logs, tokens and renewal jobs depend on it.)

When the certificate is fine but something above it expired

A certificate chain is only as valid as its oldest link. If the intermediate certificate your server sends has expired, or the root it leads to has, clients reject the chain even though your own certificate is current.

The best-known case: on 30 September 2021 the DST Root CA X3 root expired. Let’s Encrypt chains still led to it for compatibility, and devices that didn’t already trust the newer ISRG Root X1, mostly old iPhones and Macs that no longer got updates, plus some older systems using OpenSSL 1.0.x, began failing on sites that had nothing wrong with them. Current browsers built a path to the newer root and carried on.

What to check on your side:

  • Look at the whole chain your server sends: openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null. Every certificate in it should be within its dates.
  • Don’t keep an old intermediate bundled in a chain.pem you assembled by hand years ago. Use the chain your CA gives you with each renewal (with certbot, fullchain.pem).

If the error only happens on old devices you don’t control, there’s a limit to what you can do. Those devices need OS updates, or a browser that brings its own trust store, such as Firefox.

Making sure it doesn’t happen again

Certificate lifetimes keep getting shorter. Since March 2026, a publicly trusted certificate can be valid for at most 200 days, and the CA/Browser Forum has set further cuts for 2027 and 2029. A yearly calendar reminder no longer works; renewal has to be automatic, and you need something that tells you when it isn’t.

  • Check expiry from outside, the way users see it, not by reading files on the server:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -checkend 1209600 || echo "expires within 14 days"

-checkend takes seconds; 1209600 is 14 days. Put that in a cron job that alerts you, for every hostname you serve.

  • Make sure every place holding a copy of the certificate gets the new one: servers, load balancers, CDN uploads.
  • After any renewal, run the SSL check once to confirm the new expiry date is what the outside world sees.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.