TLS handshake failures stop an HTTPS request before the server can return a normal application response, so the first useful clue usually appears in the transport log rather than in the response body.
In cURL, --verbose shows name resolution, ALPN, protocol negotiation, certificate validation, and client-certificate requests on the same run. --trace-time with --trace-ascii adds event ordering and timestamps when the handshake fails too early or too noisily for a quick verbose pass.
Exact TLS wording varies by backend, so OpenSSL, Schannel, Secure Transport, GnuTLS, and Rustls do not print identical failure text for the same problem. Saved traces can include headers, tokens, and certificate details, and --insecure is only a diagnostic shortcut to prove that the failure is limited to certificate verification.
$ curl --silent --show-error --verbose --output /dev/null https://api.example.test/ * Host api.example.test:443 was resolved. * Trying 192.0.2.10:443... * ALPN: curl offers h2,http/1.1 * TLSv1.2 (OUT), TLS handshake, Client hello (1): ##### snipped ##### * Server certificate: * subject: CN=api.example.test * issuer: CN=Example Internal Root CA * SSL certificate problem: unable to get local issuer certificate curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html
cURL prints the TLS failure before any normal HTTP status line, which makes the first curl: (…) message the fastest clue about whether the problem is trust, hostname matching, protocol policy, or client authentication.
$ curl --silent --show-error --trace-time --trace-ascii - --output /dev/null https://api.example.test/ 13:00:22.492555 == Info: Trying 192.0.2.10:443... 13:00:22.718655 == Info: ALPN: curl offers h2,http/1.1 13:00:22.720884 == Info: TLSv1.2 (OUT), TLS handshake, Client hello (1): 13:00:22.720886 => Send SSL data, 326 bytes (0x146) ##### snipped ##### 13:00:22.935206 == Info: TLSv1.2 (IN), TLS handshake, Certificate (11): 13:00:22.935209 <= Recv SSL data, 903 bytes (0x387) ##### snipped ##### 13:00:23.152856 == Info: SSL certificate problem: unable to get local issuer certificate curl: (60) SSL certificate problem: unable to get local issuer certificate
Trace output can include request headers, cookies, and certificate details, so write it to a protected path and remove it after troubleshooting if the trace needs to be saved instead of streamed to the terminal.
$ curl --silent --show-error --verbose \ --cacert ~/certs/example-root-ca.pem \ --output /dev/null \ https://api.example.test/ * CAfile: /home/user/certs/example-root-ca.pem * CApath: none * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 * Server certificate: * subject: CN=api.example.test * issuer: CN=Example Internal Root CA * subjectAltName: host "api.example.test" matched cert's "api.example.test" * SSL certificate verify ok. < HTTP/1.1 200 OK
Use --cacert for a PEM CA bundle, --capath for a hashed CA directory, or --ca-native when the operating system trust store is the intended trust source for that cURL build.
$ curl --silent --show-error --verbose \ --resolve wrong.example.test:443:192.0.2.10 \ --output /dev/null \ https://wrong.example.test/ * Added wrong.example.test:443:192.0.2.10 to DNS cache * Hostname wrong.example.test was found in DNS cache * Trying 192.0.2.10:443... ##### snipped ##### * Server certificate: * subject: CN=api.example.test * issuer: CN=Example Internal Root CA * subjectAltName does not match host name wrong.example.test * SSL: no alternative certificate subject name matches target host name 'wrong.example.test' curl: (60) SSL: no alternative certificate subject name matches target host name 'wrong.example.test'
--resolve keeps the hostname from the URL for SNI and certificate validation while forcing the network destination to one specific address. Related: How to override DNS resolution with cURL
$ curl --silent --show-error --verbose \ --cacert ~/certs/example-root-ca.pem \ --cert ~/certs/api-client.crt \ --key ~/certs/api-client.key \ --output /dev/null \ https://mtls-api.example.test:8443/health * TLSv1.2 (IN), TLS handshake, Request CERT (13): ##### snipped ##### * SSL certificate verify ok. > GET /health HTTP/1.1 < HTTP/1.1 200 OK
Some servers abort the handshake immediately when no acceptable client certificate is presented, while others finish the TLS exchange and then return 400 or 403. Use --cert-type P12 for a .p12 or .pfx archive instead of the PEM example shown here. Related: How to use PKCS#12 client certificates with cURL
$ curl --silent --show-error --verbose \ --tlsv1.1 --tls-max 1.1 \ --output /dev/null \ https://api.example.test/ * TLSv1.1 (OUT), TLS handshake, Client hello (1): * TLSv1.1 (IN), TLS alert, protocol version (582): curl: (35) error:0A00042E:SSL routines::tlsv1 alert protocol version
--tlsv1.1, --tlsv1.2, and --tlsv1.3 set the minimum accepted version, not the exact version, so pair them with --tls-max when the test must prove one specific protocol level.
$ curl --silent --show-error \
--cacert ~/certs/example-root-ca.pem \
--output /dev/null \
--write-out 'verify=%{ssl_verify_result} http=%{response_code}\n' \
https://api.example.test/
verify=0 http=200
A zero verify result plus the expected status confirms that the TLS handshake finished cleanly and that the request reached the application after the fix.