# Two Mega Analyzer TLS rows: certificate expiry and a chain that does not validate

A server that sends only its leaf certificate works in the owner&#39;s browser and fails a strict client. What the two Mega Analyzer TLS rows read, and how to fix each one.

Author: J.A. Watte
Published: September 21, 2026
Source: https://jwatte.com/blog/blog-mega-analyzer-tls-certificate/

---

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](/tools/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:

```json
{
  "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:

```nginx
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:

```apache
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](/tools/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](/tools/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](/tools/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](/tools/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", that `authorized` is true "if the peer certificate was signed by one of the CAs specified when creating the tls.TLSSocket instance", and that with `rejectUnauthorized` set 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 `-servername` sets the SNI extension.
- **Source**: https://eff-certbot.readthedocs.io/en/stable/using.html establishes the contents of `cert.pem`, `chain.pem` and `fullchain.pem`, the warning that providing one of the first two requires both, that `--dry-run` tests 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 `SSLCertificateChainFile` is obsolete.
- The five `authorizationError` values 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](/blog/blog-tool-ssl-grade/)
- [Why TLS Cert Lifecycle Audit Exists](/blog/blog-tool-cert-lifecycle-audit/)
- [A 30-second uptime diagnostic when you just need to know if the site is up](/blog/blog-tool-uptime-check/)
- [The six security headers every site should ship in 2026](/blog/blog-modern-security-headers/)

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.*


---

Canonical HTML: https://jwatte.com/blog/blog-mega-analyzer-tls-certificate/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-mega-analyzer-tls-certificate.webp
