Skip to content

TLS: The Padlock, and Removing It

The sibling volume OpenSSL for Mere Mortals takes a handshake apart bolt by bolt; this chapter stays in the curl driver's seat. When curl fetches an https:// address, the connection is wrapped in TLS: the traffic is encrypted, and — the part this chapter is about — the server presents a certificate and curl verifies it before trusting anything. The lab's second server speaks TLS on port 8902, using the self-signed certificate from examples/lab-setup.sh:

$ openssl x509 -in lab-tls/cert.pem -noout -subject -ext subjectAltName
subject=CN = localhost
X509v3 Subject Alternative Name:
    DNS:localhost, IP Address:127.0.0.1

A specimen certificate: issued by itself, to itself, covering the names localhost and 127.0.0.1, good for a year. Everything that follows uses it.

The default is refusal

$ curl -sS https://127.0.0.1:8902/hello
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. ...
[exit 60]

(Message trimmed after its second paragraph; the full text is that explanatory.) Failure 60, and nothing was fetched. This is not curl being difficult. Verification asks three questions of the certificate: is it signed by an authority I trust (a chain leading to the trust store — the bundle of root certificates your system or curl install carries), is it in date, and does it name this host? The lab certificate fails the first question outright: it is signed by itself, and no trust store on earth lists it. A certificate can be honestly encrypted and still fail here — verification is about identity, not secrecy, and curl refuses the connection rather than let you confuse the two.

Real certificates pass all three checks silently. From the one public capture in this guide:

$ curl -s https://example.com -o /dev/null -w '%{http_code} %{ssl_verify_result} %{url_effective}\n'
200 0 https://example.com/

%{ssl_verify_result} is 0 when verification succeeded — the normal, unremarkable state of the public web: a chain to a trusted root, in date, naming example.com.

-k: the override, and its price

$ curl -sk https://127.0.0.1:8902/hello
Hello from the lab server.

-k (--insecure — the long name is the honest one) skips verification entirely: no chain, no name check, no dates. The connection is still encrypted, but to whom is now unexamined — the lab server, or any machine in the middle presenting any certificate at all. curl accepts all of them without a murmur.

There are legitimate minutes for -k: a first probe of a device whose certificate you're about to fix, a lab (this one) before the --cacert step below. As a habit, and in any script that outlives the afternoon, it converts HTTPS into expensive HTTP. The most dangerous sentence in operations is "we'll remove the -k later," written in a script that then runs for six years. Everything -k tolerates — expired, wrong-name, self-signed, intercepted — is something an attacker can also produce.

--cacert: trust, extended deliberately

The correct way to accept the lab's certificate is to name it as a trusted authority:

$ curl -sS --cacert lab-tls/cert.pem https://127.0.0.1:8902/hello
Hello from the lab server.
$ curl -sS --cacert lab-tls/cert.pem https://localhost:8902/hello
Hello from the lab server.

Both names work — the certificate's subject alternative names cover the IP address and the hostname alike, which is why the setup script put both in. Now verification runs in full: the chain (short: the certificate itself, presented as its own authority) is trusted because you said so, for this command, the name matches the address you asked for, and -v confirms the verdict:

* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* Server certificate:
*  SSL certificate verify ok.

(Grep-trimmed from --cacert's -v against localhost: TLS 1.3, a modern cipher, and the three words that matter.) This is also the pattern for real private infrastructure: a company's internal CA, a home lab CA — --cacert their-ca.pem, and curl trusts exactly that authority, no more. Had the certificate named only localhost, the 127.0.0.1 request would have failed 60 with "subject name does not match" — the same three questions, a different one answering no.

Where the everyday trust store lives depends on the build: Linux distributions point curl at the system bundle under /etc/ssl, while the Windows and macOS builds generally lean on the operating system's own store — which is why a certificate your browser trusts is occasionally one your script's curl does not, and why --cacert exists.

Client certificates — proving your identity with --cert and --key — are the mirror image of this chapter: real, useful in mutual-TLS setups, and named here rather than taught; curl's own documentation covers them once the server side makes sense.

Check: the plain request to 8902 fails with exit 60; -k fetches the page, having checked nothing; --cacert fetches it having checked everything, against an authority you chose. If you take one sentence from the chapter: -k answers the question "can I reach it?", --cacert answers "can I trust it?" — ask the second whenever the answer will matter tomorrow.

Next: time, failure, and exit codes — the scripting chapter.