Back to all posts

Why curl can't reach example.com anymore

I run a self-hosted AI gateway (Moltis) in a NixOS microvm. It has an exec tool that runs shell commands on behalf of the AI — think fastmail-cli, curl, nix shell, that sort of thing. One day the agent reported that web requests were failing with TLS errors.

My first instinct: the microvm’s sandboxed environment is missing CA certs, or the env vars aren’t propagated. I spent a good while making sure SSL_CERT_FILE and NIX_SSL_CERT_FILE pointed to /etc/ssl/certs/ca-certificates.crt, that the cacert package was in the service PATH, that the cert bundle was non-empty (530 KB, looked legit). Everything checked out.

Then the agent narrowed it down: curl -I https://api.search.brave.com worked, but curl -I https://example.com didn’t.

curl -I https://example.com
curl: (60) SSL certificate OpenSSL verify result: certificate rejected (28)

Wait — example.com? That’s not some obscure endpoint. I tried the same curl on the host machine, outside the VM:

curl -I https://example.com
curl: (60) SSL certificate OpenSSL verify result: certificate rejected (28)

Same error. The bug wasn’t in my microvm config at all.

The certificate chain

Verbose curl output tells the story:

curl -vI https://example.com 2>&1 | tail -3
* subject: CN=example.com
* issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
* SSL certificate OpenSSL verify result: certificate rejected (28)

The chain goes: example.comCloudflare TLS Issuing ECC CA 3 (SSL Corporation) → SSL.com TLS Transit ECC CA R2AAA Certificate Services.

That last root was created by Comodo in January 2004. Its key material is over 22 years old.

Mozilla’s 15-year rule

Mozilla Root Store Policy v2.9 (September 2023) introduced a 15-year lifetime limit on root CA key material (Section 7.4). AAA Certificate Services blew past that years ago. Its TLS trust bit was set to be distrusted after April 15, 2025.

NixOS ships nss-cacert, derived from Mozilla’s NSS root store. The distrust landed in NSS 3.111, which Arch Linux picked up with ca-certificates-mozilla 3.111. NixOS inherited it in a later cacert update. Debian still ships the old root with full trust, so everything keeps working there.

Why Firefox doesn’t care

Browsers implement AIA (Authority Information Access) chasing. When Firefox encounters the distrusted AAA root at the end of the chain, it discovers an alternative path through SSL.com TLS ECC Root CA 2022 — a modern, trusted root. It silently uses that chain instead.

curl with OpenSSL doesn’t do AIA chasing. It can only validate using what the server sends plus the local trust store. If the server sends a chain ending at a distrusted root, that’s it — validation fails.

It’s on Cloudflare to fix

The fix needs to come from the server side. Cloudflare needs to serve chains terminating at SSL.com TLS ECC Root CA 2022 instead of the deprecated AAA Certificate Services cross-sign. Their certificate authorities page acknowledges that SSL.com certificates are cross-signed with a CA from 2004 — that’s the problematic path.

If you run a Cloudflare zone with the Advanced Certificate Manager add-on, you can order a certificate with Google Trust Services as the CA to avoid this. Free universal certificates don’t let you choose.

There’s no clean client-side fix. Adding the deprecated root back to your trust store defeats the purpose of the policy. Pinning an older cacert is a security regression.

How I found it

What threw me off was the context: TLS failures inside a microvm exec sandbox. Naturally I assumed it was a missing cert bundle, wrong env vars, or filesystem isolation. It wasn’t until I ran the same curl on the host that the real cause became obvious. Would’ve saved some time trying that first.

For a detailed writeup of the certificate chain mechanics, this post by outv is worth a read.

Comments