curl: (60) SSL certificate problem: unable to get local issuer certificate
A fix for curl error 60 'unable to get local issuer certificate': what the missing chain means, how to tell a server-side chain problem from a client CA-bundle problem, how to point curl at the right CA bundle, and why --insecure is the wrong fix. Use when curl fails on HTTPS that browsers accept, in minimal containers, or behind corporate proxies. Triggers: 'curl: (60)', 'unable to get local issuer certificate'. Not for: expired certs, hostname mismatches, or TLS version errors.
curl: (60) SSL certificate problem: unable to get local issuer certificate
TL;DR
curl cannot build a chain from the server's certificate to a trusted root, usually because the server forgot to send its intermediate certificates or curl's CA bundle is missing or stale. Check the server's chain with openssl first. If the server is at fault, fix the server to send the full chain. If the server is fine, point curl at a current CA bundle. Never ship --insecure; it turns off the check that just caught a real problem.
curl: (60) SSL certificate problem: unable to get local issuer certificateUse this when
- curl fails on an HTTPS URL that browsers open fine
- Scripts break in minimal containers or fresh VMs
- It started after a CA rotation or behind a corporate proxy
- CI runners fail while developer laptops work
Not for this skill when
- The certificate is expired (different error, different fix)
- The hostname does not match the cert
- The failure is a TLS version or cipher mismatch
Steps
Check whether the server sends its intermediates:
echo | openssl s_client -connect example.com:443 -servername example.com -showcertsLook at the verify return code at the end. Code 0 means the chain is complete and the problem is on curl's side. Code 21 ("unable to verify the first certificate") means the server is not sending intermediates. Expected: you know whether to fix the server or the client.
If the server is at fault, serve the full chain. Concatenate the server certificate with the intermediate certificates into the file your server presents, intermediates in order. Restart or reload the server and repeat step 1. Expected: openssl reports verify return code 0.
If the server is fine, fix curl's trust store. Point curl at a CA bundle explicitly:
curl --cacert /path/to/ca-bundle.crt https://example.comOn minimal containers, install the OS CA certificates package so the bundle exists and stays updated. For a permanent fix, set the CA bundle path in curl's config or environment rather than adding flags to every command. Expected: curl completes the request with verification enabled.
For corporate proxies that intercept TLS, add the proxy's root CA to the bundle. Get the CA certificate from your IT team and verify its fingerprint through a separate channel before trusting it. Then confirm curl works through the proxy. Expected: curl succeeds with verification on, and you know exactly which extra CA you trusted and why.
Variant: Python requests raising SSLError
Same root cause, different trust store: Python's requests uses the certifi bundle, not the OS one. Update certifi or point requests at your bundle. Fixing the OS store does nothing for Python.
Variant: Node.js with NODEEXTRACA_CERTS
Node ignores the OS store too. Point it at your CA file with the extra-CA environment variable instead of disabling verification in code.
Variant: corporate MITM proxy
The proxy presents its own certificate, signed by the corporate root CA. Every client (curl, Python, Node, Java) needs that root in its own trust store. There is no single fix; each runtime has its own store.
Variant: works with --insecure, so "just use that"
No. --insecure disables all verification, which is exactly the protection that caught this. It is acceptable for a one-off test against a host you control, and never acceptable in shipped code or automation.
Why this happens
TLS trust is a chain: the server's certificate is signed by an intermediate, which is signed by a root your client trusts. If the server omits the intermediate, or the client's bundle of roots is missing or outdated, the client cannot complete the chain and must refuse the connection. Browsers often work because they cache intermediates or ship fresher bundles.
Edge cases and pitfalls
- Forgetting the SNI servername flag in your openssl test can show you a default certificate that fails, misleading the diagnosis. Always pass -servername.
- Some servers send intermediates in the wrong order. Strict clients reject that; order them leaf-first down to (but not including) the root.
- A bundle that was current last year may lack newer roots. "It worked before" often means the bundle aged out, not that the server changed.
- Do not copy a CA bundle from a random gist. Use your OS package manager or your IT team's published bundle.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_W13Q9cNLQIU1NymDiBxPPg