curl: (60) SSL certificate problem: self-signed certificate means the server's certificate was signed by itself, and curl had nothing it trusts that vouches for it. Give curl that certificate with --cacert after checking it is the right one; switching verification off with -k hides the problem instead of answering it.

Copy the server's certificate (the public .crt, not the key) to the client, compare its fingerprint with the one on the server, and pass it with --cacert, as in curl -sS --cacert /root/web.crt -o /dev/null -w '%{http_code}\n' https://web:8443/.

The error

$ curl -sS https://web:8443/
curl: (60) SSL certificate problem: self-signed 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. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
exit=60

Why it happens

The test server's certificate was made with openssl req -x509, so its subject and its issuer are the same name:

$ openssl x509 -in /root/web.crt -noout -subject -issuer
subject=CN = web
issuer=CN = web
exit=0

No certificate authority in the client's store signed it, only its own key, and curl refused the connection with exit code 60. The request never reached the web server: its log later held a GET line for each of the three requests that succeeded in this test, and none for the failed ones.

The fix

> docker exec fix10-ln-web bash /root/run.sh "openssl x509 -in /root/web.crt -noout -fingerprint -sha256"
$ openssl x509 -in /root/web.crt -noout -fingerprint -sha256
sha256 Fingerprint=2A:5D:1E:3F:ED:66:61:13:35:24:09:F2:87:E2:96:DD:11:8C:B9:A1:8F:0B:07:2C:17:C3:6B:73:87:14:7A:6F
exit=0
> docker exec fix10-ln-c bash /root/run.sh "openssl x509 -in /root/web.crt -noout -fingerprint -sha256"
$ openssl x509 -in /root/web.crt -noout -fingerprint -sha256
sha256 Fingerprint=2A:5D:1E:3F:ED:66:61:13:35:24:09:F2:87:E2:96:DD:11:8C:B9:A1:8F:0B:07:2C:17:C3:6B:73:87:14:7A:6F
exit=0
> docker exec fix10-ln-c bash /root/run.sh "curl -sS --cacert /root/web.crt -o /dev/null -w '%{http_code}\n' https://web:8443/"
$ curl -sS --cacert /root/web.crt -o /dev/null -w '%{http_code}\n' https://web:8443/
200
exit=0

This block comes from a second run with recreated containers and a new certificate made the same way (the first run gave the same result). The fingerprint printed in the client container (fix10-ln-c) matched the one printed in the server container (fix10-ln-web), so the file was the right certificate, and --cacert made curl trust it for this command: the request returned 200. The second tested option was the client's system store, after which curl accepted the server without any flag:

$ cp /root/web.crt /usr/local/share/ca-certificates/web.crt
exit=0
$ update-ca-certificates
Updating certificates in /etc/ssl/certs...
...
1 added, 0 removed; done.
...
$ curl -sS -o /dev/null -w '%{http_code}\n' https://web:8443/
200
exit=0

The certificate must also name the host you type. It was made with -subj /CN=web -addext subjectAltName=DNS:web; calling the same server by its IP address with --cacert still failed with a different code 60 message, SSL: no alternative certificate subject name matches target host name '172.20.0.4'. With -k that same mismatched request returned 200, which is why -k is no answer: it accepted a certificate that did not name the host at all.

How it was reproduced

Reproduced inside two ubuntu:24.04 containers on one user-defined Docker network on Docker engine 29.9.3, with curl 8.5.0 and OpenSSL 3.0.13 from apt. The server container, alias web, made a self-signed RSA certificate with openssl req -x509 -newkey rsa:2048 -nodes -days 30 and served it on port 8443 with a six-line Python 3.12.3 http.server wrapped in ssl. The containers and the network were removed afterwards.

Frequently asked

How do I fix curl: (60) SSL certificate problem: self-signed certificate?
Copy the server's certificate to the client, check its fingerprint against the server's copy, and pass it with --cacert. In this test that returned 200 without turning verification off.
Should I use curl -k for a self-signed certificate?
Not as the fix. In this test -k returned 200 even for an address the certificate did not name. Passing the real certificate with --cacert keeps the check and fixed the error.
How do I make curl trust a self-signed certificate permanently on Ubuntu?
Copy the .crt file to /usr/local/share/ca-certificates/ and run update-ca-certificates. After that, curl accepted the server without --cacert in this test.

The same kind of trust failure from .NET is covered in HttpClient: 'The SSL connection could not be established' (UntrustedRoot). More decoded errors in the Fixes category.