← Blog

NET::ERR_CERT_DATE_INVALID: expired cert or wrong clock?

Chrome shows NET::ERR_CERT_DATE_INVALID when a certificate the site sends is outside its validity period: expired, or not valid yet. Either a certificate in the site’s chain really is out of date, or the visitor’s clock is wrong. Chrome checks the clock before it shows this warning, so the page you’re looking at usually tells you which.

Chrome checks your clock before blaming the site

When a certificate fails on dates, Chrome compares the device clock with the time it gets from the network. If the two differ by more than five minutes plus a margin for uncertainty, Chrome skips the usual warning and shows a different page headed “Your clock is ahead” or “Your clock is behind” that states the date and time your device is set to. If it hasn’t been able to get network time, it falls back to its own build date and only flags clocks set more than two days before that or over a year after. This logic lives in Chromium’s SSL error handler and its clock classifier.

So if you see the clock page, fix the date and time settings and turn on automatic time. If you see the regular “Your connection is not private” page with NET::ERR_CERT_DATE_INVALID and your clock is right, the problem is most likely the certificate. Clearing the cache, flushing DNS, or disabling extensions won’t change a certificate’s dates.

Read the dates the server sends

Run this against the hostname that shows the error:

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

Output for that test host:

issuer=C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
notBefore=Apr  9 00:00:00 2015 GMT
notAfter=Apr 12 23:59:59 2015 GMT

notAfter is the expiry date. The same command works with the LibreSSL openssl that ships with macOS.

That shows only the site’s own certificate. Chrome checks the dates of the intermediate certificates too (in BoringSSL’s chain verifier, every certificate except the root gets a validity check). With OpenSSL 3, this lists each certificate the server sends along with its dates:

openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com </dev/null 2>/dev/null \
  | grep -E '^ *[0-9] s:|v:NotBefore|Verify return code'
 0 s:OU=Domain Control Validated, OU=PositiveSSL Wildcard, CN=*.badssl.com
   v:NotBefore: Apr  9 00:00:00 2015 GMT; NotAfter: Apr 12 23:59:59 2015 GMT
 1 s:C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
   v:NotBefore: Feb 12 00:00:00 2014 GMT; NotAfter: Feb 11 23:59:59 2029 GMT
 2 s:C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Certification Authority
   v:NotBefore: May 30 10:48:38 2000 GMT; NotAfter: May 30 10:48:38 2020 GMT
    Verify return code: 10 (certificate has expired)

The v: lines are an OpenSSL 3 feature; LibreSSL doesn’t print them.

Match the dates to the cause

The site’s certificate expired

notAfter on the first certificate is in the past. The renewal failed, or nothing was set up to renew it. Renew it with your CA or hosting panel, then automate the next one: public certificate lifetimes are dropping to 100 days in March 2027 and 47 days in 2029, so a manual renewal won’t hold up for long.

You renewed, but the server still sends the old certificate

The openssl output shows the old notAfter even though your CA or hosting panel lists a new certificate. The renewal worked; the deployment didn’t. Common gaps:

  • The web server is still running with the old file loaded. If Certbot only obtains the certificate (certonly, for example with --webroot), nothing tells the web server about the new files. Add a deploy hook that reloads it; Certbot runs deploy hooks only after a successful renewal
  • A CDN or load balancer terminates TLS. The certificate visitors see lives there, not on your server, and has to be renewed or uploaded there
  • Several servers answer for the name, and only some got the new certificate. If the error comes and goes, list the addresses with dig +short example.com A and dig +short example.com AAAA, then rerun the first command once per address: put the address in -connect (-connect 203.0.113.10:443) and keep -servername example.com. For an IPv6 address, use -host 2001:db8::10 -port 443 in place of -connect; that form works in both OpenSSL 3 and LibreSSL

An intermediate certificate expired

The site’s certificate is fine, but a certificate further along the chain has a NotAfter in the past. Chrome rejects the chain with the same error unless it can reach a trusted root another way. Replace the chain file with the current intermediate bundle from your CA (with Certbot, serve fullchain.pem).

The example above has one: the third certificate expired on May 30, 2020, the same day as the old AddTrust root. Sectigo’s notice said updated devices would chain to its newer roots and show no errors; only older devices without those roots depended on AddTrust. Chrome finds that newer path, but not every TLS client builds an alternative chain, so take the expired certificate out of the chain file anyway.

Where Certly fits

Certly monitoring connects to your server and reads the expiry date of the certificate it’s actually sending, including after that date has passed. That covers the first two causes above: a certificate running out, and a renewal that never reached the server, because the served certificate keeps counting down until the new one is live. It reads the server’s own certificate only, so an expired intermediate won’t trigger an alert. The free plan covers two sites with email alerts; paid plans add Slack, Discord, and webhook alerts.

The free checker reads public Certificate Transparency logs and shows the issuer and expiry of the most recent certificate issued for your hostname. If it shows a certificate that’s good for months while openssl shows an older one, the renewal worked and the deployment didn’t.

The other two common certificate errors have different causes: a name missing from the certificate gives ERR_CERT_COMMON_NAME_INVALID, and an issuer Chrome doesn’t trust gives ERR_CERT_AUTHORITY_INVALID.

Sources