Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 9 min read

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 60 and prints “SSL certificate problem: …”.
  • curl -k succeeds but plain curl fails (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/--insecure in committed scripts — it disables verification everywhere and hides the next real problem.
  • Always install ca-certificates in 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_BUNDLE across 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:

  1. “unable to get local issuer” — the server omitted the intermediate; serve the full chain.
  2. “self signed certificate in chain” — a private root curl does not trust; install it in the system store.
  3. “certificate has expired” — renew the cert; it is a validity, not a trust, problem.
  4. “no alternative subject name matches” — fix the certificate’s SAN list, not curl.
  5. A CURL_CA_BUNDLE/SSL_CERT_FILE override or missing ca-certificates package 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.

Free download · 368-page PDF

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.