The Trust / Security Headers section of the Mega Analyzer now carries two rows that no earlier version showed: TLS certificate valid for 37 more days (Let's Encrypt) and TLS certificate chain validates. Those are this site's own results from the morning I wrote this. Unlike almost every other row in the report, neither one reads the page. The analyzer asks the site's serverless TLS probe to open a real connection to your hostname and report what a strict client saw, and a strict client is one with an empty cache that does not go looking for whatever your server left out. That is what the two rows measure: whether the handshake succeeds for a visitor who has never been to your site, which is the safe assumption to make about anything that crawls you.
What the check actually tests
When you run the analyzer on a URL, the same gap probe that fetches your /.git/HEAD and your www twin calls /.netlify/functions/probe-tls?host= with one hostname: the one the page fetch ended on after any redirect, which is the www form if your bare domain forwards there. The function connects to port 443 with SNI set to the same name, a minimum of TLS 1.2, and rejectUnauthorized turned off, so a certificate that would make a browser refuse the connection still comes back with its details instead of an error. From the leaf certificate it reads the notAfter date and computes days_until_expiry as the whole days between now and then, rounded down. From the socket it reads Node's authorized flag and, when that flag is false, the authorizationError string.
The expiry row passes at 14 days or more and prints the issuer's organization in parentheses, taken from the O field of the issuer with the CN as a fallback. Below 14 it becomes a red fail reading TLS certificate expires in 9 days (Let's Encrypt). Once the date has passed the count goes negative and the issuer is dropped, so a certificate four days out of date reads TLS certificate expires in -4 days (expired 4 days ago). The threshold is inclusive: exactly 14 days left passes, 13 fails. Both rows are hard fails, the red cross rather than the amber exclamation mark of an advisory row, and both count against the Full Summary.
The chain row passes as TLS certificate chain validates whenever the socket reports authorized: true, which means OpenSSL built a path from the certificates your server sent to a root in Node's bundled Mozilla trust store, the current time falls inside every certificate's validity period, and the probed name appears in the leaf's Subject Alternative Names. When any of that fails, the row prints the reason in parentheses, cut to 60 characters. Node reports the reason as OpenSSL's error code name rather than its sentence, so the row you will most often see is TLS certificate does not validate (UNABLE_TO_VERIFY_LEAF_SIGNATURE). I ran the probe's exact handshake against the public badssl.com test hosts on 2026-09-21 to pin the wording down. A server that sends only its leaf returns UNABLE_TO_VERIFY_LEAF_SIGNATURE, the code behind the sentence "unable to verify the first certificate" that openssl s_client prints for the same host. An expired certificate returns CERT_HAS_EXPIRED. A certificate issued for a different name returns ERR_TLS_CERT_ALTNAME_INVALID. A self-signed leaf returns DEPTH_ZERO_SELF_SIGNED_CERT, and a chain that ends at a private root returns SELF_SIGNED_CERT_IN_CHAIN. An expired certificate therefore fails both rows at once, and an expired intermediate, whose date the expiry row never reads, shows up only here.
What does not trip either row matters as much. Only that one hostname is probed. Analyze example.com, get redirected to www.example.com, and the bare domain's certificate is never read; with no redirect it is the www twin that goes unread. Run both forms. The page fetch itself retries with verification relaxed when a strict fetch dies on a certificate error, which is why a leaf-only site still gets a full report rather than a blank one. The probe collects the protocol version, cipher, key size and OCSP stapling too, but none of that feeds these rows; the SSL Grade scores it. And if the handshake never completes, because the server only speaks TLS 1.0 or 1.1, takes longer than seven seconds, refuses port 443, or the probe function has hit its limit of 30 calls a minute during a batch, the function returns no standard object and neither row appears at all. A missing row is a skip, not a pass.
This is the raw shape the function returns, trimmed to the fields the two rows read. It is the same JSON the SSL Grade tool reads:
{
"host": "example.com",
"standard": {
"protocol": "TLSv1.3",
"authorized": false,
"authorization_error": "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"cert_issuer": { "C": "US", "O": "Let's Encrypt", "CN": "YE2" },
"cert_valid_to": "Oct 28 12:00:00 2026 GMT",
"days_until_expiry": 37
}
}
Why it matters
Expiry is the simple half. RFC 5280 defines the validity period as "the time interval during which the CA warrants that it will maintain information about the status of the certificate", and its path validation algorithm requires that "the certificate validity period includes the current time". A browser that finds the date has passed does not degrade anything; it puts up a full-page interstitial, and on a site that sends HSTS there is no button to click through, because RFC 6797 says the browser "MUST terminate the connection" on any certificate error for a known HSTS host, with "no user recourse". The reason the row exists now rather than three years ago is that the window for catching a failed renewal keeps shrinking. Let's Encrypt certificates have been 90 days for a decade, with renewal recommended every 60. Since March 15, 2026 the CA/Browser Forum's Baseline Requirements cap any publicly trusted TLS server certificate at 200 days, down from 398; the cap drops to 100 days on March 15, 2027 and to 47 days on March 15, 2029, under ballot SC-081v3, which passed in April 2025 with no votes against. A renewal job that silently stops today costs you an outage within two months; in 2029 it costs you one within six weeks. And the safety net a lot of people were quietly relying on is gone: Let's Encrypt stopped sending expiration emails on June 4, 2025.
The chain half is the one that fools site owners, because it is invisible from the owner's chair. Your CA does not sign your certificate with its root; it signs with an intermediate, and your server is expected to send that intermediate along with the leaf. RFC 8446 puts it in the TLS 1.3 handshake itself: "The sender's certificate MUST come in the first CertificateEntry in the list. Each following certificate SHOULD directly certify the one immediately preceding it", and only a trust anchor may be left out. Install cert.pem where fullchain.pem belonged and the intermediate never goes out. Browsers paper over this. Firefox pre-downloads every trusted intermediate in the web PKI precisely so that, in Mozilla's words, its users "avoid seeing an error page for one of the most common server configuration problems: not specifying proper intermediate CA certificates". Some clients fetch the missing certificate from the URL in the leaf's Authority Information Access extension, which RFC 5280 says exists "to aid certificate users in the selection of a certification path", or already have it cached from the last properly configured site that used the same CA. So you load your site, it works, and you move on.
Nothing that crawls you is promised that help. The probe behind this row takes the chain exactly as sent and stops, which is what my badssl test showed, and the same holds for anything that hands verification to OpenSSL, whose verifier builds its path from the certificates it was sent and the trust store it was given, nothing else. Whether any particular crawler adds one is not documented anywhere I could cite, so treat the strict result as the one that counts. To a client like that your site is a failed handshake and no content at all. Google's page on network errors never mentions TLS, but it is blunt about a URL that returns nothing: network errors are treated "similarly to 5xx server errors", and "already indexed URLs that are unreachable will be removed from Google's index within days". That is the outcome hiding behind the green padlock in your own browser.
How to fix it
Step 1: read the row before touching anything. The code in parentheses tells you which of four problems you have. UNABLE_TO_VERIFY_LEAF_SIGNATURE means the server sent the leaf alone; the fix is the chain file. CERT_HAS_EXPIRED means renewal failed; the fix is renewal, and the expiry row will be red too. ERR_TLS_CERT_ALTNAME_INVALID means the certificate is real but for a different name, usually a bare domain answering with a www-only certificate or the reverse; the fix is reissuing with both names. DEPTH_ZERO_SELF_SIGNED_CERT and SELF_SIGNED_CERT_IN_CHAIN mean nobody outside your own machines trusts the signer, which on a public site usually means the CA-issued certificate was never installed and the host's placeholder is still being served.
Step 2: confirm it from your own machine with a client that does not cheat. The -showcerts flag prints "the server certificate list as sent by the server", in the server's order, and the OpenSSL docs are explicit that it "is not a verified chain":
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
Read three things in the output. The Certificate chain block should list at least two entries: 0 with an s: line carrying your name, and 1 whose s: line matches entry 0's i: line. The Verify return code line near the end should read 0 (ok). A leaf-only server prints a single entry and 21 (unable to verify the first certificate). For the date alone, pipe the same handshake into the certificate parser:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
which prints one line, notAfter=Oct 28 12:00:00 2026 GMT, and is the entire logic of the expiry row.
Step 3: serve the full chain. With certbot the file is already on disk; the mistake is pointing the server at the wrong one. Certbot's documentation says it plainly: cert.pem is the leaf alone, chain.pem is the intermediates, and if you use one of those "you must provide both of them, or some browsers will show 'This Connection is Untrusted' errors". Use fullchain.pem and the question goes away. On nginx, whose docs require "the primary certificate comes first, then the intermediate certificates" in the same file:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
On Apache 2.4.8 or later, SSLCertificateFile accepts the whole chain "sorted from leaf to root", and SSLCertificateChainFile is obsolete:
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
If your certificate came from a commercial CA as a zip, the file you want is the one named bundle, ca-bundle or intermediate; concatenate your certificate first and that file second, and that is your fullchain.pem. On IIS the chain is assembled from the machine's certificate stores rather than from a file, so the CA's intermediate must be imported into the computer's Intermediate Certification Authorities store, and the same CA download page provides it. On Netlify, Cloudflare, Vercel and the other managed hosts the chain is handled for you unless you uploaded a custom certificate, in which case upload the bundle with it.
Step 4: prove renewal runs, then alert on it. Certbot installs a job that runs certbot renew twice a day and by default renews once less than a third of the lifetime remains, 30 days on a 90-day certificate; the failure mode is a job that exists and cannot complete, because port 80 was closed, the DNS plugin's API token expired, or the webroot moved with the last redesign. Run the same thing the job runs, against the staging environment so nothing real is issued:
sudo certbot renew --dry-run
A clean dry run ends with Congratulations, all simulated renewals succeeded. Anything else is the error your visitors would have met the day the certificate ran out. On a managed host, open the certificate panel and read the date; that is your dry run. Then add an alert, because nobody else will send you one. The DIY Uptime Alert Generator writes the configs for Gatus and for Prometheus with blackbox_exporter, and both carry a certificate condition. The Gatus template ships with [CERTIFICATE_EXPIRATION] > 48h; I would raise it to 336h, the same 14 days this row uses, so the alert fires while there is still time to fix a broken job by hand.
Step 5: re-run and widen. The analyzer's probe call carries a cache-busting parameter, so a fixed chain shows green on the next run without waiting. The TLS Cert Lifecycle Audit then reads the same probe plus the Certificate Transparency logs for your name: how long each certificate was issued for, how often they have been renewed, which CAs issued them, and the current public-key fingerprint so you can spot a key being reused across renewals. The SSL Grade rolls chain, expiry, protocol, cipher, key size, HSTS and post-quantum key exchange into one letter.
When to leave it alone
Let's Encrypt's opt-in six-day certificates renew every three days, so a healthy one always has under 14 days left and will always paint the expiry row red. If your ACME client is on the short-lived profile on purpose, the row is measuring the wrong thing for you; ignore it and watch the renewal log instead. Once the 47-day cap arrives in 2029 the same tension reaches everyone, since renewing at two thirds of the lifetime leaves about 16 days, and the threshold will have to move with it.
A staging host behind a private CA fails the chain row with SELF_SIGNED_CERT_IN_CHAIN, and correctly so: the row reports what the public internet sees, and it cannot know about the root you pushed to your own laptops. Do not chase it, and do not point the public analyzer at staging.
A certificate that chains to a root in one vendor's store but not in Mozilla's fails here even though some browsers accept it. That is not a false positive to wave off. Firefox, which trusts the same Mozilla list the probe does, sees much the same thing, and the fix is a certificate from a CA that sits in every major root program.
And if neither row is there, resist reading that as a pass. Analyze the www twin, run the Uptime Check for the handshake details, and if the server really is TLS 1.0 only, that is the finding.
Fact-check notes and sources
- Source: https://cabforum.org/working-groups/server/baseline-requirements/requirements/ establishes the Baseline Requirements validity schedule in section 6.3.2: 398 days through March 14, 2026, then "Maximum validity period of Subscriber Certificates is 200 days" from March 15, 2026, 100 days from March 15, 2027, and 47 days from March 15, 2029.
- Source: https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/ establishes that ballot SC-081v3 passed on April 11, 2025 (Certificate Issuers 25 yes, 0 no, 5 abstain; Certificate Consumers 4 yes, 0 no) with the "eventual reduction of maximum validity period from 398 days to 47 days".
- Source: https://letsencrypt.org/docs/faq/ establishes that "Our default certificates are valid for 90 days", that Let's Encrypt recommends "renewing 90 day certificates every 60 days", and that subscribers "can opt in to short-lived certificates which are valid for six days", renewed every three.
- Source: https://letsencrypt.org/2025/01/22/ending-expiration-emails/ establishes that Let's Encrypt ended expiration notification emails on June 4, 2025.
- Source: https://nodejs.org/api/tls.html#tlssocketauthorizationerror establishes that
authorizationError"Returns the reason why the peer's certificate was not been verified" and "is set only when tlsSocket.authorized === false", thatauthorizedis true "if the peer certificate was signed by one of the CAs specified when creating the tls.TLSSocket instance", and that withrejectUnauthorizedset to false verification failure does not raise an error. - Source: https://www.rfc-editor.org/rfc/rfc5280 establishes the validity period definition (section 4.1.2.5), the path validation requirement that "The certificate validity period includes the current time" (section 6.1.3), and that the caIssuers form of Authority Information Access "is intended to aid certificate users in the selection of a certification path that terminates at a point trusted by the certificate user" (section 4.2.2.1).
- Source: https://www.rfc-editor.org/rfc/rfc8446#section-4.4.2 establishes the certificate_list ordering rules quoted above and that "a certificate that specifies a trust anchor MAY be omitted from the chain".
- Source: https://www.rfc-editor.org/rfc/rfc6797#section-8.4 establishes that for a known HSTS host the user agent "MUST terminate the connection" on any secure transport error, including certificate validity errors, and section 12.1 describes this as "no user recourse".
- Source: https://developers.google.com/crawling/docs/troubleshooting/dns-network-errors establishes that Google treats network and DNS errors "similarly to 5xx server errors" and that "already indexed URLs that are unreachable will be removed from Google's index within days".
- Source: https://blog.mozilla.org/security/2020/11/13/preloading-intermediate-ca-certificates-into-firefox/ establishes that Firefox "pre-downloads all trusted Web Public Key Infrastructure (PKI) intermediate CA certificates" so users "avoid seeing an error page for one of the most common server configuration problems: not specifying proper intermediate CA certificates".
- Source: https://docs.openssl.org/master/man1/openssl-s_client/ establishes that
-showcerts"Displays the server certificate list as sent by the server" and "is not a verified chain", and that-servernamesets the SNI extension. - Source: https://eff-certbot.readthedocs.io/en/stable/using.html establishes the contents of
cert.pem,chain.pemandfullchain.pem, the warning that providing one of the first two requires both, that--dry-runtests against the staging server without saving certificates, the twice-daily renewal schedule, and that since Certbot 4.0.0 "a certificate is considered ready for renewal when less than 1/3rd of its lifetime remains". - Source: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate establishes the chain order in the certificate file. https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile establishes that intermediates "sorted from leaf to root" may be included since 2.4.8 and that
SSLCertificateChainFileis obsolete. - The five
authorizationErrorvalues and the 37-day figure are my own measurements on 2026-09-21, running the probe function's handshake options under Node 24 against the badssl.com test hosts and this site.
Related reading
- An A-F letter grade for your SSL config
- Why TLS Cert Lifecycle Audit Exists
- A 30-second uptime diagnostic when you just need to know if the site is up
- The six security headers every site should ship in 2026
If you keep more than one site, the renewal check is the first thing worth scripting across all of them, because the failure is silent and the deadline gets shorter every year; The $100 Network treats that kind of one-script-for-every-property routine as the base a multi-site operator builds on.
This post is informational, not legal advice. Mentions of third parties are nominative fair use. No affiliation is implied.