TLS & Security Error Guide: 'curl: (60) SSL certificate problem'
Fix curl error 60 SSL certificate problem: diagnose missing chain, untrusted CA, hostname mismatch, expired cert, and wrong CA bundle without using --insecure.
- #security
- #troubleshooting
- #errors
- #curl
Overview
curl: (60) is curl’s exit code for CURLE_PEER_FAILED_VERIFICATION — curl could not verify the server’s TLS certificate against its CA bundle. The trailing text after “SSL certificate problem:” tells you why: a missing local issuer, an expired certificate, a self-signed cert, or a hostname mismatch. The error code is always 60, but the cause varies, so the suffix is what you triage on.
A typical failure:
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it.
The tempting “fix” is curl --insecure (or -k), which disables verification entirely and turns a TLS error into a silent security hole. This guide diagnoses and fixes the underlying trust problem so verification passes for real. The same numeric code 60 also covers certificate has expired, self-signed certificate, and SSL: no alternative certificate subject name matches.
Symptoms
- curl exits with status
60and prints “SSL certificate problem: …”. curl -ksucceeds but plaincurlfails (verification is the only thing broken).- Other tools on the same host may also fail (shared CA bundle) or may not (different bundles).
- The exact suffix differs: “unable to get local issuer”, “certificate has expired”, “self signed certificate”.
curl -v https://api.internal.example.com/health 2>&1 | grep -E 'SSL certificate|subject:|issuer:|expire'
echo "exit=$?"
* subject: CN=api.internal.example.com
* issuer: CN=Example Intermediate CA R3
* SSL certificate problem: unable to get local issuer certificate
The issuer line shows the chain stops at an intermediate curl cannot anchor.
Common Root Causes
1. Missing intermediate / incomplete chain (most common)
curl received only the leaf and cannot build a path to a trusted root.
curl -v https://api.internal.example.com/ 2>&1 | grep -i 'SSL certificate problem'
echo | openssl s_client -connect api.internal.example.com:443 -showcerts 2>/dev/null \
| grep -c 'BEGIN CERTIFICATE'
* SSL certificate problem: unable to get local issuer certificate
1
A single cert sent for a publicly-signed leaf means a missing intermediate.
2. Untrusted / private CA not in curl’s bundle
The server uses an internal CA that curl’s bundle does not contain.
curl -v https://api.internal.example.com/ 2>&1 | grep -E 'CAfile|SSL certificate problem'
* CAfile: /etc/ssl/certs/ca-certificates.crt
* SSL certificate problem: self signed certificate in certificate chain
“self signed certificate in certificate chain” means the chain ends at a root curl does not trust.
3. Expired certificate
Code 60 with an “expired” suffix is a validity-window failure, not a trust gap.
curl -sS https://api.internal.example.com/ 2>&1 | head -1
echo | openssl s_client -connect api.internal.example.com:443 2>/dev/null | openssl x509 -noout -enddate
curl: (60) SSL certificate problem: certificate has expired
notAfter=Jun 1 00:00:00 2026 GMT
4. Hostname mismatch (SAN does not match)
The cert is valid and trusted, but its SubjectAltName does not include the host curl requested.
curl -sS https://api.internal.example.com/ 2>&1 | head -1
echo | openssl s_client -connect api.internal.example.com:443 -servername api.internal.example.com 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
curl: (60) SSL: no alternative certificate subject name matches target host name 'api.internal.example.com'
DNS:internal.example.com, DNS:www.example.com
The SAN list lacks api.internal.example.com.
5. A bad CURL_CA_BUNDLE override
An environment variable points curl at the wrong or stale bundle.
env | grep -iE 'CURL_CA_BUNDLE|SSL_CERT_FILE'
curl -v https://api.internal.example.com/ 2>&1 | grep -i CAfile
CURL_CA_BUNDLE=/opt/old/certs.pem
* CAfile: /opt/old/certs.pem
curl honors the override and ignores the system store.
6. System CA bundle missing or stale (minimal image)
A slim container has no ca-certificates package, so curl has nothing to verify against.
ls -l /etc/ssl/certs/ca-certificates.crt 2>/dev/null || echo "no system bundle"
no system bundle
Every HTTPS request then fails with code 60.
Diagnostic Workflow
Step 1: Read the exact suffix and curl’s CAfile
curl -v https://<HOST>/ 2>&1 | grep -E 'CAfile|SSL certificate problem|subject:|issuer:'
The suffix (“unable to get local issuer” vs “expired” vs “self signed” vs “no alternative subject name”) routes the rest of triage.
Step 2: Reproduce with openssl to see the chain and verify code
echo | openssl s_client -connect <HOST>:443 -servername <HOST> 2>/dev/null | grep 'Verify return code'
echo | openssl s_client -connect <HOST>:443 -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
Step 3: Decide trust gap vs validity vs hostname
# expired?
echo | openssl s_client -connect <HOST>:443 2>/dev/null | openssl x509 -noout -dates
# hostname?
echo | openssl s_client -connect <HOST>:443 -servername <HOST> 2>/dev/null | openssl x509 -noout -ext subjectAltName
Step 4: Test against an explicit, known-good CA file
curl --cacert /path/to/correct-ca-or-fullchain.pem https://<HOST>/health -sS -o /dev/null -w '%{http_code}\n'
If this succeeds, the problem is the default bundle/override, not the server.
Step 5: Fix the real cause and re-verify (never settle on -k)
# private CA: trust it system-wide
sudo cp internal-root.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
# missing bundle in container
apk add --no-cache ca-certificates # alpine
# then confirm clean
curl -sS https://<HOST>/health -o /dev/null -w 'ok %{http_code}\n'
Example Root Cause Analysis
A deployment script that calls curl https://artifacts.internal.example.com/build.tar.gz fails only inside the new Alpine-based CI image; the old Debian image worked:
curl: (60) SSL certificate problem: unable to get local issuer certificate
The artifacts host uses a public LetsEncrypt cert, so the root should be well-known. Reproducing under openssl shows the chain is complete (two certs), which rules out a missing intermediate. Checking the image itself:
ls -l /etc/ssl/certs/ca-certificates.crt 2>/dev/null || echo "no system bundle"
no system bundle
The Alpine base image never had ca-certificates installed, so curl has no roots at all — every HTTPS endpoint fails with code 60, not just this one. The Debian image shipped the bundle by default, which is why it “worked.”
Fix: install the package in the image build:
apk add --no-cache ca-certificates
curl -sS https://artifacts.internal.example.com/build.tar.gz -o /dev/null -w '%{http_code}\n'
200
curl verifies cleanly with no -k and no per-call --cacert.
Prevention Best Practices
- Never resolve a code 60 with
-k/--insecurein committed scripts — it disables verification everywhere and hides the next real problem. - Always install
ca-certificatesin minimal/container base images and rebuild when roots rotate. - Serve full chains on the server side so curl never needs AIA fetching (which it does not do).
- Prefer trusting a private root in the system store over scattering
--cacert/CURL_CA_BUNDLEacross scripts. - When the suffix is “no alternative subject name”, fix the cert’s SAN list rather than the client; see the security hardening guides for SAN/hostname conventions.
- For a quick read on which of the code-60 variants you hit, the free incident assistant can classify the curl/openssl output into trust vs validity vs hostname.
Quick Command Reference
# Exact failure suffix + which CA file curl used
curl -v https://<HOST>/ 2>&1 | grep -E 'CAfile|SSL certificate problem|subject:|issuer:'
# Chain length and verify code
echo | openssl s_client -connect <HOST>:443 -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
echo | openssl s_client -connect <HOST>:443 -servername <HOST> 2>/dev/null | grep 'Verify return code'
# Expiry and SAN
echo | openssl s_client -connect <HOST>:443 2>/dev/null | openssl x509 -noout -dates -ext subjectAltName
# Test with an explicit CA file (proves server vs bundle)
curl --cacert /path/to/ca.pem https://<HOST>/ -sS -o /dev/null -w '%{http_code}\n'
# Check for a stray override
env | grep -iE 'CURL_CA_BUNDLE|SSL_CERT_FILE'
# Trust a private root
sudo cp internal-root.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
Conclusion
curl: (60) is always a peer-verification failure, but the suffix tells you which kind:
- “unable to get local issuer” — the server omitted the intermediate; serve the full chain.
- “self signed certificate in chain” — a private root curl does not trust; install it in the system store.
- “certificate has expired” — renew the cert; it is a validity, not a trust, problem.
- “no alternative subject name matches” — fix the certificate’s SAN list, not curl.
- A
CURL_CA_BUNDLE/SSL_CERT_FILEoverride or missingca-certificatespackage points curl at the wrong or empty bundle.
Read the suffix, reproduce with openssl s_client, and fix the actual cause — -k only masks the problem and removes the protection TLS was there to provide.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.