Old Android and OpenSSL Clients Failing TLS After the Root CA Transition
Your site works. It works in Chrome, in Safari, in Firefox, in curl, in every check you can think of. And yet a customer on an old Android phone, or a partner's ancient integration server, gets a TLS failure and cannot connect. You regenerate the certificate. No change. You regenerate it again, this time with --force-renewal. Still no change. You start to wonder if the client is lying.
They are not lying, and the certificate was never the problem. This is a client-side trust-store issue, and it is one of the most misdiagnosed failures in web operations because the fix people reach for (make a new certificate) is aimed at the one thing that is working correctly. The certificate is fine. The client does not trust the root at the top of the chain you are serving.
There is an added wrinkle in 2026 that makes most of the older advice on this actively wrong: the famous --preferred-chain "DST Root CA X3" workaround no longer does anything, because that chain has been retired by Let's Encrypt. Let me walk through what actually broke, what still works, and why regenerating the cert is a dead end.
A TLS failure that hits only old Android or ancient OpenSSL clients while modern browsers work is a trust-store problem, not a certificate misconfiguration. Regenerating the certificate changes nothing. Since Let's Encrypt retired the DST Root CA X3 cross-sign in 2024, the old --preferred-chain "DST Root CA X3" workaround no longer works. For pre-2016 Android that does not trust ISRG Root X1, there is now no server-side fix; the durable answer is to serve those clients from a certificate under a CA their trust store already knows, or accept that unpatched decade-old devices are out of scope. Always test against the real failing OS version, not curl.
The symptom: fine everywhere except one class of old client
There is rarely a single tidy error string, because the message depends on the client. On an old Android device inside an app you often see a generic:
javax.net.ssl.SSLHandshakeException:
java.security.cert.CertPathValidatorException:
Trust anchor for certification path not found.
On an old OpenSSL 1.0.x client (an ancient Linux box, a legacy integration server) you may instead see:
certificate verify failed: unable to get local issuer certificate
Both messages are saying the same thing in different accents: I walked up the chain you served and I do not trust the certificate at the top. Notice what neither says: nothing about the certificate being expired, malformed, or for the wrong hostname. If it were any of those, modern clients would fail too. The fact that only old clients fail is the entire diagnosis.
Why this happens: chains, roots, and whose trust store we are talking about
When a server presents a certificate, it also sends a chain of intermediate certificates leading toward a root. The client does not need you to send the root; it needs to recognise a certificate in that chain as signed by a root already in its own trust store. Trust lives on the client, not on your server. You cannot install trust into someone else's phone by reissuing your certificate.
Let's Encrypt's modern chain terminates at a root called ISRG Root X1. New devices have ISRG Root X1 baked into their trust store, so they validate happily. But ISRG Root X1 is relatively young. Devices that shipped before roughly 2016 and never received a system update do not have it. For years, Let's Encrypt bridged this gap with a clever trick: they also offered a chain where ISRG Root X1 was cross-signed by an older, very widely trusted root called DST Root CA X3. Old Android devices did not know ISRG Root X1, but they knew DST Root CA X3, so serving that longer chain let them validate.
What changed in 2024, and why the old fix is dead
The DST Root CA X3 cross-sign was always a temporary bridge, and Let's Encrypt sunset it on a published schedule. Three dates matter, and I am stating them because most blog posts predate them and quietly give you advice that no longer works:
| Date | What happened |
|---|---|
| Feb 2024 | Let's Encrypt stopped serving the DST Root CA X3 cross-signed chain by default. |
| 6 June 2024 | The cross-sign was removed from the Let's Encrypt API entirely. You can no longer request it. |
| 30 September 2024 | The DST Root CA X3 root certificate itself expired. |
The practical consequence: certbot renew --preferred-chain "DST Root CA X3" now silently gives you the ISRG Root X1 chain anyway, because the cross-signed chain is no longer on offer. You can run it, it will not error, and it will change nothing. This is precisely why regenerating the certificate "does nothing" for these teams. There is no longer a longer chain to fall back to. If you read a tutorial telling you to select the DST chain, that tutorial is describing a world that ended in 2024.
You can confirm the details of this transition on Let's Encrypt's own changes-to-issuance-chains post. The reason they felt safe retiring it: the share of active Android devices that trust ISRG Root X1 rose past 93 percent, so the bridge was carrying a shrinking population.
Step 1: Confirm it is a trust-store issue, not a real cert fault
Prove the certificate is valid before you chase trust stores. Look at the chain you are actually serving:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
Check the dates are current and the subject matches your hostname. If they do, the certificate is fine and you have confirmed this is about trust, not the cert. A modern browser succeeding is the same confirmation.
Step 2: Reproduce on the real failing client, not curl
This is the step teams skip and then waste a day. curl on your modern laptop uses your laptop's trust store, which has ISRG Root X1, so it will always succeed and tell you nothing. You must test against a client with the same trust store as the one that fails. Options, in order of fidelity:
- An Android emulator running the actual old API level (for example Android 6.0 / API 23) with no updated trust store.
- The genuine old device, if a customer or your test drawer can supply one.
- An old OpenSSL build for the OpenSSL-1.0.x variant of the failure, so you can see
unable to get local issuer certificateyourself.
Testing against the real trust store is the difference between guessing and knowing. If it fails on API 23 and passes on API 26, you have pinned the boundary of affected devices exactly.
Step 3: Choose the response that actually exists
Because the server-side bridge is gone, the honest set of options is smaller than the old articles imply:
- Serve the affected clients from a different CA. If those old devices are important (a fleet of legacy Android POS terminals, say), put the endpoint they hit behind a certificate issued by a CA whose root is already in their trust store. This is a real fix because it targets trust, not the cert format.
- Update the client's trust store, where you control the client. If it is your own app on a managed device, ship an updated CA bundle or pin ISRG Root X1 explicitly. You cannot do this for random consumer phones, only for devices you administer.
- Accept the scope. For genuinely unpatched consumer devices from before 2016, there is no server-side action that restores trust in 2026. The devices are the problem and the number is shrinking. Document it, tell the affected customer what OS update fixes it, and move on rather than degrading security for everyone else.
What people get wrong
Regenerating the certificate. Named first because it is the reflex and it is useless here. Trust is on the client. A new certificate with the same chain reaching the same root will fail on the same clients for the same reason. Doing it twice, with --force-renewal, does not make it more true.
Reaching for --preferred-chain "DST Root CA X3". This was a real fix until 2024. It is now a no-op because the chain was withdrawn. Copy-pasting it from an old answer wastes time and, worse, convinces you that you "tried the chain fix" when you did not actually change anything.
Disabling certificate validation on the client. The moment someone suggests setHostnameVerifier that returns true, or curl -k in production, stop. Turning off validation to make old devices connect means every one of those connections is now trivially interceptable, which is the exact attack TLS exists to prevent. You would be trading a compatibility inconvenience for a security hole. Never ship this.
When it is still broken
If you have confirmed trust is the issue and old clients still fail after choosing a response:
- Check you are serving the full intermediate chain. A missing intermediate looks like a trust failure on stricter clients even when the root is trusted. Some clients fetch the missing intermediate via the AIA extension and some do not, which is why it fails on only some clients. Serve the complete chain your CA gives you.
- Look at the handshake, not just the cert. A very old client may also fail because it only speaks TLS 1.0 and your server, correctly, requires TLS 1.2 or higher. That is a protocol mismatch masquerading as a trust error, and the fix is not to re-enable TLS 1.0. A separate but related class of "only some backends answer" is covered in ACME TLS handshake failure behind a load balancer.
- Do not confuse this with renewal problems. If your certificate is genuinely expiring rather than untrusted, that is a different failure with a different fix, covered in certbot renewal fails silently. And if certbot keeps minting new certificates instead of renewing, see the --cert-name explainer.
The reason this problem is so sticky is that the instinctive fix and the actual fix point in opposite directions. Your certificate is not broken; your server is not misconfigured; the client simply does not trust the root you legitimately serve. Once you internalise that trust lives on the client, the whole thing becomes tractable: test against the real device, decide whether those devices are in scope, and stop regenerating a certificate that was never the problem.
Frequently asked questions
- Why does my site work in browsers but fail TLS on old Android phones?
- It is a client-side trust-store problem, not a certificate fault. Modern Let's Encrypt certificates chain to ISRG Root X1, which pre-2016 Android devices do not have in their trust store, so they cannot validate the chain even though the certificate is perfectly valid. Browsers on updated systems trust ISRG Root X1 and succeed, which is why only old clients fail.
- Does certbot --preferred-chain "DST Root CA X3" still work in 2026?
- No. Let's Encrypt stopped serving that cross-signed chain by default in February 2024, removed it from their API on 6 June 2024, and the DST Root CA X3 root itself expired on 30 September 2024. Running the flag no longer errors, but it silently gives you the ISRG Root X1 chain, so it changes nothing. Advice recommending it predates the retirement.
- Will regenerating my certificate fix old clients that fail TLS?
- No. Trust lives in the client's trust store, not on your server, so a new certificate reaching the same untrusted root fails on the same clients for the same reason. Regenerating, even with --force-renewal, cannot install trust into someone else's device. The fix is to serve those clients from a CA their trust store already knows, update the trust store where you control the device, or accept them as out of scope.
- How do I test a TLS trust failure for old Android?
- Do not use curl on a modern laptop, because it uses your machine's up-to-date trust store and will always succeed. Reproduce on a client with the same trust store as the failing one: an Android emulator running the actual old API level such as API 23, the genuine old device, or an old OpenSSL 1.0.x build. Only the real trust store tells you the truth.