curl Cheatsheet
TLS Options
Use this curl reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Verification is on by default
curl verifies the certificate chain and the hostname on every https:// URL. When
it fails you get exit 60 and a clear message, and the fix is almost never -k.
curl https://expired.example.com/
# curl: (60) SSL certificate problem: certificate has expired| Flag | Effect | Use when |
|---|---|---|
-k / --insecure | Skip chain and hostname checks | Throwaway debugging only |
--cacert <f> | Verify against this PEM bundle | Corporate or private CA |
--capath <dir> | Directory of hashed CA certs | OpenSSL-style CA dir |
--ca-native | Also use the OS trust store | Windows or macOS system CAs |
--crlfile <f> | Check a CRL for revocation | Revocation list on hand |
--cert-status | Require a stapled OCSP response | Verifying stapling |
--pinnedpubkey <sha256//..> | Pin the public key | Pinning a known peer |
curl --cacert /etc/ssl/corp-root.pem https://internal.example.com/
curl --pinnedpubkey 'sha256//AbCd...=' https://ex.com/
-kdisables the check, it does not diagnose it. Anything that runs with-kis unauthenticated and can be intercepted, so never leave it in a script or in.curlrc. Fix the trust store instead.
Reading the certificate
-v prints the negotiated cipher, the peer's subject and issuer, and the validity
window, which is enough to spot an expired or wrong-host cert.
curl -vI https://example.com 2>&1 | grep -E 'subject|issuer|expire|SSL|ALPN' curl -so /dev/null -w '%{ssl_verify_result} %{http_version}\n' https://ex.com/
For the full chain, openssl is the better tool. curl tells you whether curl
trusts it, openssl tells you what was actually sent.
openssl s_client -connect example.com:443 -servername example.com \ </dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer
Pinning a TLS version
--tlsv1.2 and friends set a minimum, not an exact version. That surprises
people who expect --tlsv1.2 to reject TLS 1.3. Pair it with --tls-max for an
exact pin.
| Flag | Meaning |
|---|---|
--tlsv1.0 … --tlsv1.3 | Minimum acceptable TLS version |
--tls-max <1.0|1.1|1.2|1.3|default> | Maximum acceptable version |
--ciphers <list> | TLS 1.2 and earlier cipher list |
--tls13-ciphers <list> | TLS 1.3 cipher suites |
--curves <list> | Allowed key exchange curves |
--no-alpn | Do not advertise ALPN |
curl --tlsv1.2 --tls-max 1.2 https://ex.com/ # exactly TLS 1.2 curl --tlsv1.3 https://ex.com/ # 1.3 or better curl --tlsv1.2 --ciphers 'ECDHE-RSA-AES128-GCM-SHA256' https://ex.com/
--tls-max has no effect on an HTTP/3 transfer, because QUIC always uses TLS 1.3.
Which TLS backend you have
curl is built against a TLS library and the available options follow from it.
curl -V is the first thing to check when a --ciphers value is rejected.
curl -V
# curl 8.7.1 ... (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.67.1| Backend | Notes |
|---|---|
| OpenSSL / LibreSSL / BoringSSL | Widest option support, PEM --cacert |
| GnuTLS | Different --ciphers grammar |
| Schannel (Windows) | Uses the OS store, --cacert support is limited |
| Secure Transport (older macOS) | Uses the Keychain, deprecated upstream |
| wolfSSL / mbedTLS | Embedded builds, reduced feature set |
The system curl on macOS may be linked against Secure Transport, so --cacert
behaves differently from a Homebrew curl built on OpenSSL. Check which binary you
are running with command -v curl before debugging a trust problem.
--resolve, testing before DNS cutover
--resolve host:port:addr overrides DNS for one host and port pair while keeping
the real Host header and the real SNI name. That means TLS verification still
works, which is the whole point.
# hit the new server, but present and verify example.com curl --resolve example.com:443:203.0.113.10 https://example.com/health # same for the plain HTTP port curl --resolve example.com:80:203.0.113.10 http://example.com/ # every host on 443 goes to one box curl --resolve '*:443:203.0.113.10' https://example.com/
You need one entry per port. Extras worth knowing: [brackets] around an IPv6
address, several comma-separated addresses in one entry (7.59.0), * as the host
for a wildcard (7.64.0), and a + prefix to let the entry expire after curl's
one minute default (7.75.0).
curl --resolve 'ex.com:443:[2001:db8::1],203.0.113.10' https://ex.com/--resolve vs the alternatives
| Approach | Host header | SNI | TLS verify |
|---|---|---|---|
--resolve ex.com:443:IP | ex.com | ex.com | passes |
--connect-to ex.com::new.host: | ex.com | ex.com | passes |
https://IP/ -H 'Host: ex.com' | ex.com | IP | fails |
Editing /etc/hosts | ex.com | ex.com | passes, machine wide |
curl --connect-to 'example.com::staging.internal:443' https://example.com/
curl --doh-url https://dns.example/dns-query https://example.com/--connect-to redirects by name rather than address, so it works when the target
is itself a DNS name. The -H 'Host:' trick against a bare IP is the one to
avoid, since SNI no longer matches and you end up needing -k, which throws away
the verification you were trying to test.