curl can't build the chain to a trusted root. Usually a missing intermediate on the server, a private CA not in the trust store, or a corporate MITM proxy re-signing traffic.
Browsers hide this via AIA fetching; curl does not. openssl s_client -showcerts | grep -c 'BEGIN CERT' returning 1 = only the leaf is served.
Company TLS-inspection proxies re-sign with an internal CA. curl uses the system CA bundle; without the corporate root installed, every HTTPS call fails with 60.
openssl s_client -connect host:443 -servername host -showcerts </dev/null 2>/dev/null | grep -c 'BEGIN CERT'
# nginx: ssl_certificate /etc/letsencrypt/live/host/fullchain.pem — leaf + intermediates
sudo cp corp-root.pem /usr/local/share/ca-certificates/corp-root.crt && sudo update-ca-certificates
curl --cacert /path/to/ca.pem https://host/ # trust exactly that CA for this call — better than -k everywhere
Python requests/Node hit the same wall and use the same OS trust store (or certifi) — the CA fix helps your whole toolchain, not just curl. '-k' (insecure) in scripts is a smell: it disables validation permanently in that code path. --cacert with the right CA is the disciplined version.
Browsers fetch missing intermediates (AIA) and carry their own roots; curl uses only the system bundle and only what the server sends. Serve the full chain and install any private CA — then both agree.
For quick experiments, maybe. In pipelines it silently disables MITM protection forever. Install the right CA (--cacert or update-ca-certificates) so validation stays on.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.