# curl (60) SSL certificate problem

Exit code 60 from curl means certificate verification failed, and the phrase printed after "SSL certificate problem" identifies which check failed rather than being part of one single error. "unable to get local issuer certificate" means the chain is incomplete or the root is untrusted; "self-signed certificate" means the chain terminates in a certificate no CA vouched for; "certificate has expired" means the validity dates have passed; and a subject-name message means the certificate is valid but was issued for a different hostname. curl verifies against a CA bundle chosen when it was compiled, which is why `curl -v https://example.com` printing the CAfile path is the first useful diagnostic. Passing `-k` silences all four by disabling verification entirely and should never survive past the diagnosis.

Source: https://certguru.io/errors/curl-60-ssl-certificate-problem/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

> **The exact message**
>
> ```
>   curl: (60) SSL certificate problem: unable to get local issuer certificate
>   More details here: https://curl.se/docs/sslcerts.html
>   ```
>   The number is always 60. The phrase after the colon changes, and it is the part that matters.

## Exit code 60 is four different errors

curl returns 60 — `CURLE_PEER_FAILED_VERIFICATION` — for any failure of certificate verification.
Searching for the number alone mixes four unrelated problems together. Find your phrase in this
table first:

| Message after "SSL certificate problem" | What failed | Fix |
| --- | --- | --- |
| `unable to get local issuer certificate` | No path from the certificate to a trusted root | [Incomplete chain or untrusted root](#unable-to-get-local-issuer-certificate) |
| `self-signed certificate` (older curl: `self signed certificate`) | The certificate signed itself | [Self-signed certificate](#self-signed-certificate) |
| `self-signed certificate in certificate chain` | A CA in the chain is self-signed and untrusted | [Self-signed in chain](/errors/self-signed-certificate-in-chain/) |
| `certificate has expired` | `notAfter` is in the past | [Expired certificate](/errors/certificate-has-expired/) |

One more message also exits 60 but is not printed as a "certificate problem" at all:

```
curl: (60) SSL: no alternative certificate subject name matches target host name 'example.com'
```

That is a [hostname mismatch](#hostname-does-not-match), and nothing about the chain is wrong.

## First: which CA bundle is curl reading?

Almost every code-60 investigation is shortened by this one line. curl prints the bundle it was
compiled to use in its verbose handshake output:

*Print the CA bundle path*

```bash
curl -v https://example.com 2>&1 | grep -iE "CAfile|CApath"
```

Expected output:

```
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
```

> **Empty or missing CAfile**
>
> If `CAfile: none` comes back, curl has no bundle and every HTTPS request will fail with code 60
>   regardless of the site. This is the normal state of a minimal container image. Install
>   `ca-certificates` rather than reaching for `-k`.

Three things override that path, in increasing order of precision:

```bash
export CURL_CA_BUNDLE=/path/to/ca-bundle.crt   # every curl invocation in the shell
curl --cacert /path/to/root-ca.crt https://example.com   # this request only
curl --capath /etc/ssl/certs https://example.com         # a directory of hashed certs
```

## unable to get local issuer certificate

The most common of the four. curl could not find the CA that signed the certificate it was handed.
Ask the server what it sends and count the certificates:

*How many certificates does the server offer?*

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts /dev/null \
  | grep -c "BEGIN CERTIFICATE"
```

One certificate from a public CA means the **server** is misconfigured — it is not sending its
intermediates — and every non-browser client will fail against it. Two or more means the chain is
fine and the root is missing from curl's bundle, which is normal for a private or corporate CA.

The full diagnosis and both fixes are on
[unable to get local issuer certificate](/errors/unable-to-get-local-issuer-certificate/). To make
curl accept a private root without touching the system bundle:

```bash
curl --cacert /etc/ssl/certs/corp-root-ca.crt https://internal.example.com
```

## Self-signed certificate

The chain terminates in a certificate that signed itself, so there is no CA to appeal to. Confirm
by comparing subject and issuer — on a self-signed certificate they are identical:

*Subject and issuer of a self-signed certificate*

```bash
openssl x509 -in self-signed.crt -noout -subject -issuer
```

Expected output:

```
subject=CN=localhost
issuer=CN=localhost
```

For development and internal services this is expected, and the correct response is to trust that
specific certificate rather than to stop verifying:

```bash
curl --cacert self-signed.crt https://localhost:8443
```

For anything reachable from the internet, a self-signed certificate means no client will trust it
by default. [Get a free certificate from a public CA](/concepts/free-tls-certificate/) instead.

## Hostname does not match

The certificate is valid, correctly signed and in date — it was simply issued for a different name.
curl checks the Subject Alternative Name extension, and since RFC 6125 the Common Name is not
consulted at all by modern clients. List the names the certificate is actually valid for:

*Which names is this certificate valid for?*

```bash
openssl x509 -in certificate.crt -noout -ext subjectAltName
```

Expected output:

```
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com
```

If the name you requested is absent, the certificate needs reissuing with the right
[SAN entries](/concepts/san/). A certificate carrying only a Common Name and no SAN extension fails
this check against every current client, however correct the CN looks.

> **Connecting by IP address**
>
> `curl https://10.0.0.5/` requires the certificate to carry `IP:10.0.0.5` in its SAN list.
>   Certificates are almost never issued for IP addresses, so use the hostname and, if DNS is the
>   problem, `curl --resolve example.com:443:10.0.0.5 https://example.com/` — which keeps
>   verification intact while pinning the address.

## Fixing it for libcurl consumers

curl is a library long before it is a command, so the same failure surfaces under other names:

| Consumer | How to point it at the right bundle |
| --- | --- |
| git | `git config --global http.sslCAInfo /path/to/ca-bundle.crt` |
| PHP / Composer | `curl.cainfo` and `openssl.cafile` in `php.ini` |
| Node (`node-fetch`, axios) | `NODE_EXTRA_CA_CERTS=/path/to/root-ca.crt` |
| Python `requests` | `REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt` |
| Any shell | `CURL_CA_BUNDLE=/path/to/ca-bundle.crt` |

## What not to do

> **-k and --insecure are not fixes**
>
> Both flags stop curl checking whether the certificate belongs to the host it is talking to. The
>   traffic is still encrypted, but encrypted to whoever answered — which is precisely the attack
>   certificate verification exists to prevent. Use `-k` once, to confirm that verification is the
>   only thing failing, then remove it. `-k` inside a deployment script is a permanent silent hole.


## Frequently asked questions

### What is the difference between curl error 60 and error 77?

Error 60 means curl read its CA bundle and the certificate failed verification. Error 77, `error setting certificate verify locations`, means curl could not read the bundle file at all — the path is wrong or unreadable, so nothing was verified against anything.

### Why does the browser load the site but curl refuses it?

Browsers repair incomplete chains. They cache intermediates from earlier visits and fetch missing ones over the Authority Information Access extension, and curl does neither. A site that only works in a browser is genuinely misconfigured, and other clients will hit the same wall.

### Is curl -k safe for a script that runs on an internal network?

No. `-k` accepts any certificate from anything that answers on that address, so an internal network with a compromised host or a poisoned DNS entry is exactly where it does the most damage. Point curl at the internal CA with `--cacert` instead; it is one flag either way.

### How do I fix this for git rather than curl?

git uses libcurl and shows the same message. Set the bundle with `git config --global http.sslCAInfo /path/to/ca-bundle.crt`. Do not use `http.sslVerify false`, which is git's equivalent of `-k`.

### Why does the same curl command work on my machine but fail in Docker?

A slim base image often ships no CA bundle at all. `apk add ca-certificates` on Alpine or `apt-get install -y ca-certificates` on Debian installs one, and an image built `FROM scratch` needs the bundle copied in explicitly.


## References

- [curl error codes](https://curl.se/libcurl/c/libcurl-errors.html) — CURLE_PEER_FAILED_VERIFICATION, the code 60 maps to
- [curl: SSL certificate verification](https://curl.se/docs/sslcerts.html) — how curl selects a CA bundle and what --cacert overrides
- [RFC 6125: identifying application services in PKIX certificates](https://www.rfc-editor.org/rfc/rfc6125) — the hostname matching rules behind the subject-name variant

## Related pages

- https://certguru.io/errors/unable-to-get-local-issuer-certificate/
- https://certguru.io/errors/self-signed-certificate-in-chain/
- https://certguru.io/errors/certificate-has-expired/
- https://certguru.io/errors/certificate-verify-failed-python/
- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/openssl/get-certificate-from-server/
- https://certguru.io/openssl/verify-certificate-chain/
- https://certguru.io/concepts/san/

--
CertGuru — https://certguru.io
