Sign in
← Networking: advanced
13 min · medium · Lesson 1 of 4

TLS handshake and certificates

How HTTPS sets up encryption: the TLS handshake, certificates, the chain of trust, and what the browser checks.

Why a DevOps engineer needs this

HTTP sends everything in plain text: anyone on the path (Wi-Fi, an ISP, a proxy) sees the requests and responses. HTTPS is the same HTTP wrapped in TLS. TLS gives you three things: encryption (the content can't be read), integrity (bytes can't be swapped without being noticed) and authentication (you're talking to the real server, not an impostor).

Certificates and TLS settings cause one of the most common production outages: the certificate expired, the intermediate certificate was forgotten, the domain isn't listed in the certificate. The site still "works", but clients refuse to connect to it. You diagnose these cases from the output of curl -v and openssl s_client, and for that you need to understand what happens when a connection is made.

Order: TCP, then TLS, then HTTP

1. TCP: SYN → SYN-ACK → ACK            (connection established)
2. TLS handshake: agree on cipher, keys, and verify the certificate
3. HTTP: GET / over the now-encrypted channel

At the TCP stage there's no encryption yet, it only appears after the TLS handshake. That's why the first request goes out later than over plain HTTP. A full TLS 1.2 handshake costs two extra round trips (there and back), TLS 1.3 needs just one.

This is the scheme for HTTP/1.1 and HTTP/2. In HTTP/3, QUIC over UDP replaces TCP, and TLS 1.3 is built right into it, but certificates are checked the same way.

Versions in use: 1.2 and 1.3

TLS 1.0 and 1.1 are banned by RFC 8996 (2021): modern browsers don't support them, and security scanners immediately flag such a server in red. There are two working versions, TLS 1.2 and TLS 1.3.

In nginx the ssl_protocols directive controls the versions:

ssl_protocols TLSv1.2 TLSv1.3;

Since nginx 1.23.4 this is also the default value. People write the line anyway so the versions are visible in the config, and they don't list the old versions.

The client offers versions, the server picks. What was actually negotiated on a given connection shows up in curl -v's output:

* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF

If the server only supports TLS 1.2, you'll see TLSv1.2 there, even when the client supports 1.3.

What happens in the handshake

Simplified, for TLS 1.3:

The session key is symmetric and fast; it encrypts all the traffic. The certificate is for something else: proving that the key exchange happened with the actual domain owner, not with someone in the middle.

In TLS 1.2 the order is different: the certificate goes out in plain text, and the key is computed after it. That's why curl -v against a TLS 1.2 server shows Server key exchange and Client key exchange lines, which TLS 1.3 doesn't have.

Certificate and chain of trust

A certificate is the server's public key, the names it was issued for, an expiration date, and a certificate authority's signature (CA, Certificate Authority). The client trusts the server because the certificate is signed by a CA the client already trusts.

Trust is built as a chain:

Root CA (in the OS/browser store)
  └─ Intermediate CA (signed by the Root)
       └─ Server certificate (signed by the Intermediate)

Root certificates sit on the client ahead of time: in the OS store, the browser store, or in a file pointed to with curl's --cacert. The server sends its own certificate and the intermediates during the handshake. It doesn't send the root: it's useless to the client, which decides whether to trust it based on its own store.

The client walks up the chain to a root it trusts. If a link is missing, the chain breaks and the client refuses to connect.

What actually gets checked:

Revocation has changed. For years it was checked via OCSP, a request to the CA's server during the connection. On May 7, 2025 Let's Encrypt stopped including an OCSP address in new certificates, and on August 6, 2025 it shut down its OCSP responders. Revocation is now reported through CRLs, lists of revoked certificates that browsers download ahead of time. For a server owner the takeaway is simple: ssl_stapling from old guides for Let's Encrypt certificates no longer does anything.

Seeing it for yourself

openssl s_client shows the handshake in detail:

openssl s_client -connect example.com:443 -servername example.com
depth=3 C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
...
depth=0 CN=example.com
...
subject=CN=example.com
issuer=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
...
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
...
Verify return code: 0 (ok)

subject is who the certificate was issued to, issuer is who signed it. Verify return code: 0 (ok) means the chain came together and the certificate is valid. The command waits for input; exit with Ctrl+C or by adding </dev/null at the end.

The -servername flag sets SNI. OpenSSL 1.1.1 and newer takes the name from -connect on its own, if that's a domain and not an IP. LibreSSL, which ships on macOS by default, doesn't do that: without -servername, example.com aborts the handshake with alert handshake failure. So it's better to always write the flag. Connecting by IP means SNI only comes from -servername, otherwise a server with several sites serves its default certificate, which could turn out to be someone else's.

LibreSSL's line format is a bit different, but the fields are the same.

curl -v https://example.com shows the same thing in the lines starting with *:

* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* Server certificate:
*  subject: CN=example.com
*  start date: Sep 26 22:49:11 2026 GMT
*  expire date: Dec 25 22:56:35 2026 GMT
*  subjectAltName: host "example.com" matched cert's "example.com"
*  issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*  SSL certificate verify ok.

When verification fails, curl exits with code 60 and prints the reason:

curl's messageWhat's wrong
certificate has expiredthe validity period has expired
no alternative certificate subject name matches target hostnamethe name isn't in the SAN
unable to get local issuer certificatethe chain doesn't reach a trusted root

The -k (--insecure) flag disables verification. It helps you look at a certificate that doesn't pass verification, but with it you no longer get a "trust it or not" verdict: the SSL certificate verify result line isn't meaningful with -k.

Common mistakes

What you'll need for the tasks

In the next lesson we'll cover getting and automatically renewing certificates through Let's Encrypt, so they don't expire in production.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson networking-advanced-01-tls-handshake