Reliable TLS handshakes keep HTTPS traffic encrypted and authenticated, so handshake failures quickly turn into broken APIs, unavailable dashboards, or blocked automation. Inspecting the negotiation between client and server exposes protocol mismatches, expired certificates, and man‑in‑the‑middle devices that rewrite or terminate connections.

During a handshake, cURL passes the hostname, supported protocol versions, cipher suites, and extensions like SNI and ALPN to the TLS library, then validates the server certificate chain against local trust stores. Verbose and trace options show each phase of that process, including which side aborts the connection and which OpenSSL or other TLS-library error code explains the failure.

TLS debugging often involves private endpoints, internal certificate authorities, or client certificates that should not leak into logs or ticket systems. Trace files capture binary TLS records and sometimes HTTP headers or tokens, so storage locations deserve careful handling, and convenience options such as --insecure or ad‑hoc trust anchors must stay out of production scripts to avoid weakening security.

Steps to debug TLS handshake with cURL:

  1. Open a shell with network access to the target HTTPS endpoint.
    $ whoami
    user

    Any environment with cURL in $PATH is sufficient, including Linux, macOS, and Windows terminals.

  2. Capture a baseline TLS negotiation using verbose mode to reveal the overall handshake flow.
    $ curl --verbose "https://www.example.com/"
    *   Trying 93.184.216.34:443...
    * Connected to www.example.com (93.184.216.34) port 443 (#0)
    * ALPN, offering h2
    * ALPN, offering http/1.1
    * successfully set certificate verify locations:
    *   CAfile: /etc/ssl/certs/ca-certificates.crt
    *   CApath: /etc/ssl/certs
    * TLSv1.3 (OUT), TLS handshake, Client hello (1):
    * TLSv1.3 (IN), TLS handshake, Server hello (2):
    ##### snipped #####

    Lines beginning with TLSv1.x, Client hello, and Server hello confirm which protocol version and negotiation features reach the server before any error appears.

  3. Generate a detailed TLS trace file with timestamps to correlate failures with specific handshake records.
    $ curl --trace-time --trace-ascii tls-trace.txt "https://www.example.com/"
    $ head -n 12 tls-trace.txt
    13:45:01.123000 ====> Send SSL data, 517 bytes (0x205)
    0000: 16 03 01 02 00 01 00 01 fc 03 03 7a 8b 5d 8a e3
    0010: 2f 12 34 56 78 9a bc de f0 11 22 33 44 55 66 77
    13:45:01.456000 <==== Recv SSL data, 1514 bytes (0x5ea)
    0000: 16 03 03 05 d6 02 00 00 d2 03 03 5f 7a 1c 9b 42
    ##### snipped #####

    The trace distinguishes outgoing and incoming TLS records, helping pinpoint whether the client, server, or intermediary sends the first alert or closes the connection unexpectedly.

  4. Test protocol compatibility by constraining allowed TLS versions and observing how the server responds.
    $ curl --verbose --tls-max 1.0 "https://modern.example.com/"
    *   Trying 203.0.113.10:443...
    * Connected to modern.example.com (203.0.113.10) port 443 (#0)
    * TLSv1.0 (OUT), TLS handshake, Client hello (1):
    * error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version
    * Closing connection 0
    curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version
    
    $ curl --verbose --tlsv1.2 "https://modern.example.com/"
    *   Trying 203.0.113.10:443...
    * Connected to modern.example.com (203.0.113.10) port 443 (#0)
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    ##### snipped #####

    A protocol-version alert with weaker settings such as --tls-max 1.0 usually indicates that the server enforces newer standards and accepts only modern TLS versions.

  5. Narrow down cipher, key, or curve issues by restricting the cipher list to suites supported by current security policy.
    $ curl --verbose --tlsv1.2 --ciphers "ECDHE+AESGCM" "https://secure.example.com/"
    *   Trying 198.51.100.20:443...
    * Connected to secure.example.com (198.51.100.20) port 443 (#0)
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    * TLSv1.2 (IN), TLS handshake, Server hello (2):
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
    ##### snipped #####

    Errors mentioning no shared cipher or handshake failure suggest that the server and client share no overlapping cipher suites, which often happens when legacy algorithms are disabled on only one side.

  6. Diagnose certificate-chain problems by reproducing failures with verbose output and checking the error string from the TLS library.
    $ curl --verbose "https://self-signed.example.internal/"
    *   Trying 192.0.2.50:443...
    * Connected to self-signed.example.internal (192.0.2.50) port 443 (#0)
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    ##### snipped #####
    * SSL certificate problem: self signed certificate
    * Closing connection 0
    curl: (60) SSL certificate problem: self signed certificate

    Common messages such as unable to get local issuer certificate or self signed certificate in certificate chain indicate missing intermediate or private CA certificates on the client side.

  7. Supply the correct certificate authority bundle or directory to confirm whether missing trust anchors cause the handshake failure.
    $ curl --verbose --cacert /etc/ssl/private/internal-ca.pem "https://self-signed.example.internal/"
    *   Trying 192.0.2.50:443...
    * Connected to self-signed.example.internal (192.0.2.50) port 443 (#0)
    * successfully set certificate verify locations:
    *   CAfile: /etc/ssl/private/internal-ca.pem
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    ##### snipped #####

    Options --cacert and --capath test whether updating trust stores or distributing the correct internal CA certificate resolves verification errors without permanently bypassing validation.

  8. Investigate mutual TLS issues by attaching a client certificate and private key to the request.
    $ curl --verbose --cert client.pem --key client.key "https://mtls.example.com/"
    *   Trying 198.51.100.80:443...
    * Connected to mtls.example.com (198.51.100.80) port 443 (#0)
    * TLSv1.2 (OUT), TLS handshake, Certificate (11):
    * TLSv1.2 (IN), TLS handshake, Finished (20):
    ##### snipped #####

    Handshake failures after sending a client certificate often relate to certificate expiration, incorrect key usage, or a subject that does not match server-side authorization rules.

  9. Use the --insecure flag only as a controlled diagnostic to confirm that failures come from certificate validation rather than protocol or cipher issues.
    $ curl --verbose --insecure "https://self-signed.example.internal/"
    *   Trying 192.0.2.50:443...
    * Connected to self-signed.example.internal (192.0.2.50) port 443 (#0)
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
    > GET / HTTP/1.1
    > Host: self-signed.example.internal
    ##### snipped #####

    Flag --insecure disables hostname and certificate validation and exposes traffic to man-in-the-middle attacks, so its use belongs strictly to temporary debugging in controlled environments.

  10. Confirm a healthy handshake and HTTP response after any configuration change or server fix.
    $ curl --verbose "https://www.example.com/"
    *   Trying 93.184.216.34:443...
    * Connected to www.example.com (93.184.216.34) port 443 (#0)
    * ALPN, offering h2
    * ALPN, server accepted to use h2
    * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
    > GET / HTTP/2
    > Host: www.example.com
    ##### snipped #####

    Successful negotiation typically shows a selected TLS version and cipher suite plus a normal HTTP status such as HTTP/2 200 or HTTP/1.1 200 OK immediately after the handshake.

Discuss the article:

Comment anonymously. Login not required.