TLS handshake failures break HTTPS APIs before the first response arrives, so one failed negotiation can look like a dead load balancer, a broken reverse proxy, or an unreachable service when the real problem is trust, protocol policy, or client identity.

When cURL connects over HTTPS, it sends SNI, negotiates protocol versions and cipher suites, validates the server certificate chain and hostname, and, when requested, sends a client certificate for mutual TLS. Verbose and trace options expose those stages directly, which makes them the fastest way to separate protocol mismatch, certificate validation failure, hostname mismatch, and mTLS errors.

Exact wording varies by the TLS backend underneath cURL, so OpenSSL, Schannel, Secure Transport, and GnuTLS do not print identical error strings for the same failure class. Trace files can also contain request headers, tokens, and certificate details, so they belong in protected locations and should be removed after troubleshooting.

Steps to debug TLS handshake with cURL:

  1. Capture a baseline handshake with verbose output against the exact URL, host name, and test IP involved in the failure.
    $ curl --verbose --silent --show-error \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    *   Trying 192.0.2.10:443...
    * Connected to api.example.test (192.0.2.10) port 443
    * ALPN: curl offers h2,http/1.1
    * TLSv1.3 (OUT), TLS handshake, Client hello (1):
    * TLSv1.3 (IN), TLS handshake, Server hello (2):
    * TLSv1.3 (IN), TLS handshake, Certificate (11):
    * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
    * 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.
    > GET / HTTP/1.1
    < HTTP/1.1 200 OK

    Use --resolve only when DNS must be pinned to a specific test IP. It keeps the URL hostname for SNI and certificate validation while forcing the network destination.

    If local cURL configuration may interfere with the repro, add -q as the first option to ignore ~/.curlrc and system curlrc files for that run.

  2. Write a time-stamped trace file when the failure needs packet-order context rather than a quick screen read.
    $ curl --silent --show-error \
      --trace-time --trace-ascii tls-trace.txt \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    
    $ grep -E 'Trying|ALPN|TLS handshake|Send SSL data|Recv SSL data' tls-trace.txt | head -n 6
    11:54:40.238129 == Info:   Trying 192.0.2.10:443...
    11:54:40.239004 == Info: ALPN: curl offers h2,http/1.1
    11:54:40.239118 == Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
    11:54:40.239119 => Send SSL data, 1560 bytes (0x618)
    11:54:40.241804 == Info: TLSv1.3 (IN), TLS handshake, Server hello (2):
    11:54:40.244517 <= Recv SSL data, 122 bytes (0x7a)

    Trace files show direction, timing, and TLS record boundaries, which helps identify whether the client, server, or a middlebox sends the first alert.

    Trace output can include HTTP headers and authentication material, so keep it out of shared tickets unless it has been reviewed and sanitized.

  3. Test protocol compatibility by tightening the allowed TLS version range and comparing the first failing alert with a successful retry.
    $ curl --verbose --silent --show-error \
      --tls-max 1.1 \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    * 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
    
    $ curl --verbose --silent --show-error \
      --tlsv1.2 --tls-max 1.2 \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    * TLSv1.2 (IN), TLS handshake, Server hello (2):
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
    *   SSL certificate verify ok.
    < HTTP/1.1 200 OK

    --tlsv1.2 sets the minimum to TLS 1.2, not the exact version. Pair it with --tls-max 1.2 when the goal is a true TLS 1.2 only retry.

  4. Restrict the offered cipher list on TLS 1.2 to confirm whether server policy and client preference still overlap.
    $ curl --verbose --silent --show-error \
      --tlsv1.2 --tls-max 1.2 \
      --ciphers ECDHE-RSA-AES128-GCM-SHA256 \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    * Cipher selection: ECDHE-RSA-AES128-GCM-SHA256
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    * TLSv1.2 (IN), TLS handshake, Server hello (2):
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
    *   SSL certificate verify ok.
    < HTTP/1.1 200 OK

    --ciphers applies to TLS 1.2 and earlier. When the endpoint negotiates TLS 1.3, use --tls13-ciphers for the same type of narrowing.

  5. Reproduce certificate-chain failures without custom trust first so the verification error is explicit before any workaround is added.
    $ curl --verbose --silent --show-error \
      --resolve api.example.test:443:192.0.2.10 \
      https://api.example.test/ --output /dev/null
    * 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

    Backends phrase this differently, but errors such as self-signed certificate in certificate chain, unable to get local issuer certificate, and certificate is not trusted all point at trust-store problems rather than protocol negotiation.

  6. Retry with the correct certificate authority file or directory to confirm that the failure is only a trust-anchor problem.
    $ curl --verbose --silent --show-error \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test/ --output /dev/null
    *  CAfile: /home/user/certs/example-internal-root-ca.pem
    *  CApath: none
    * Server certificate:
    *   issuer: CN=Example Internal Root CA
    *   SSL certificate verify ok.
    < HTTP/1.1 200 OK

    Use --cacert for one PEM bundle, or --capath when the trusted CA files live in a rehashed certificate directory.

  7. Check for hostname and SNI mismatches by repeating the request with the exact URL host that the certificate is supposed to cover.
    $ curl --verbose --silent --show-error \
      --resolve wrong.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://wrong.example.test/ --output /dev/null
    * 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'

    Changing Host: with --header does not change the TLS hostname. The URL host still controls SNI and certificate validation.

  8. Reproduce mutual TLS failures with and without the client certificate so the server-side certificate request is visible and the failure point is explicit.
    $ curl --verbose --silent --show-error \
      --resolve api.example.test:9443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      https://api.example.test:9443/ --output /dev/null
    * TLSv1.2 (IN), TLS handshake, Request CERT (13):
    * TLSv1.2 (OUT), TLS handshake, Certificate (11):
    } [7 bytes data]
    < HTTP/1.1 400 Bad Request
    
    $ curl --verbose --silent --show-error \
      --resolve api.example.test:9443:192.0.2.10 \
      --cacert ~/certs/example-internal-root-ca.pem \
      --cert ~/certs/api-client.crt \
      --key ~/certs/api-client.key \
      https://api.example.test:9443/ --output /dev/null
    * TLSv1.2 (IN), TLS handshake, Request CERT (13):
    * TLSv1.2 (OUT), TLS handshake, Certificate (11):
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
    < HTTP/1.1 200 OK

    Some servers abort the handshake immediately when no acceptable client certificate is presented, while others finish the handshake and return an application error such as 400 or 403. When the client certificate and key are already combined in one PEM file, --cert is enough. PKCS#12 client certificates use a separate flow.

  9. Use --insecure only to prove that the failure is limited to certificate verification and not to protocol, cipher, or routing.
    $ curl --verbose --silent --show-error \
      --insecure \
      --resolve wrong.example.test:443:192.0.2.10 \
      https://wrong.example.test/ --output /dev/null
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
    * Server certificate:
    *   subject: CN=api.example.test
    *   issuer: CN=Example Internal Root CA
    < HTTP/1.1 200 OK

    --insecure disables both hostname and certificate validation. Keep it out of production scripts and use it only long enough to prove where the handshake actually fails.

  10. Confirm the fixed path with one short verification command that returns a clean certificate result and the expected HTTP status.
    $ curl --silent --show-error \
      --resolve api.example.test:443:192.0.2.10 \
      --cacert ~/certs/example-internal-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 HTTP response confirms that the TLS handshake and certificate checks both completed successfully.