LearnThe Network StackThe Application Layer

TLS

openssl s_client - a hands-on Linux lab on a real virtual machine.

The handshake inside the handshake. Certificate chain, cipher negotiation, all visible in the terminal.

Two true things that should not both be true

There is a web server at 10.0.1.20 inside this VM, and it speaks HTTPS. In a few minutes you will read its certificate, the cipher it chose, and the exact number of bytes its handshake cost.

Then you will capture that same handshake on the wire, and most of it will be unreadable. Not the payload, which you would expect. The certificate too.

Both of those are true at the same time, on the same connection, from the same machine. Working out why is the whole lesson.

Every time you visit a website that starts with https://, something happens before any data moves. A handshake. Your browser and the server negotiate encryption, the server proves its identity, and both sides agree on a shared secret. All of it finishes in a fraction of a second, completely invisible. This lesson makes it visible.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The lab user is student and the machine is named tls-lab. Values that change on every run, such as byte counts, timestamps and the certificate dates, are printed as labels rather than numbers, because your run will produce its own.

What TLS actually is

TLS stands for Transport Layer Security. It is the encryption layer that sits between TCP and HTTP. When you see HTTPS, the "S" stands for "Secure," and that security comes from TLS.

Without TLS, HTTP traffic is plain text. Anyone on the same network can read it. With TLS, the data is encrypted. Even if someone captures the packets, they see nothing but gibberish labeled "Application Data."

That is one sentence with two halves worth separating. TLS does not hide that you connected, or to whom. It hides what you said.

Why half your tools still say SSL

TLS replaced an older protocol called SSL (Secure Sockets Layer). Netscape built SSL in the mid-1990s to put a padlock on web shopping. When the IETF took the protocol over and standardised it in 1999, they renamed it TLS, and version numbering started again at 1.0.

The old name never left. You will still see the word "SSL" in many tools and commands, including the openssl binary you are about to run and the words SSL handshake in its own output. They mean the same thing in practice. The current version is TLS 1.3.

Version numbers matter more here than in most protocols, because TLS 1.3 did not just add features. It changed what is visible from outside. A tutorial written for TLS 1.2 describes a capture you will never see on a modern server, and that mismatch is the single most common reason a first TLS capture looks broken.

The TLS handshake, message by message

Before any encrypted data moves, the client and server perform a TLS handshake. This lab negotiates TLS 1.3, so here is the TLS 1.3 version of it, step by step:

1. ClientHello - The client sends the cipher suites it supports, a random number, and a key share. The key share is its half of the key material, sent up front as a guess at what the server will accept. 2. ServerHello - The server picks one cipher suite and sends its own random number and its own key share. Both sides can now compute the same secret from the two halves. Everything after this message is encrypted. 3. Certificate - The server sends its certificate to prove who it is. 4. Finished - Each side sends a Finished message proving it derived the same keys. 5. Application Data - The HTTP request and response, encrypted with the session keys.

The whole thing takes one round trip.

Commit: how much of that can a capture read?

Read step 2 in that list again before you answer. It is the sentence the rest of this lesson hangs on.

Older material shows the Certificate arriving in the clear, and a separate Key Exchange step after it. That was TLS 1.2. TLS 1.3 moved the key share into the very first message and pulled everything from the Certificate onward behind encryption. If a tutorial shows you a readable certificate in a packet capture, it is showing you TLS 1.2 or older.

Your lab network

This lab builds two machines inside your one VM using network namespaces. A network namespace is a private copy of the network stack: its own interfaces, its own addresses, its own routing table. They are joined by a virtual cable:

  [client]  10.0.1.10  <-------->  10.0.1.20  [server]

The 10.0.1.0/24 network exists only inside those namespaces, so a command typed plainly in your shell cannot reach the server. To run something inside the client namespace, put sudo ip netns exec client in front of it. The connect-tls and cap helpers already do that.

The server runs an HTTPS service on port 443 with a self-signed certificate. A self-signed certificate is one that the server created and signed itself, rather than having a trusted authority (like the free certificate service Let's Encrypt) sign it. Browsers would show a warning for this cert, but the encryption works the same way.

These command lines get long, so lean on tab completion. Type connect- and press Tab and the shell finishes connect-tls. Tab completes command names and file paths alike, and it is the cheapest way to stop a typo halfway through a 60-character openssl line.

Your progress is tracked automatically. Just type the commands naturally and you will see a checkmark appear when each task completes.

Do the handshake by hand

openssl s_client is a tool that performs a TLS handshake and shows you everything that happened. It is like curl for TLS: it connects, negotiates, and dumps the details.

The helper runs sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 for you. You will see a wall of output, and the box below shows the key parts of it, with the values that change on every run written as labels.

Before you press Enter, commit to one thing: does this connection succeed or fail? The certificate is signed by nobody your machine trusts.

connect-tls

prompt: student@tls-lab:~$ answer: connect-tls ||| sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 </dev/null output: CONNECTED(00000003) --- Certificate chain 0 s:CN = server.lab i:CN = server.lab --- Server certificate -----BEGIN CERTIFICATE----- <the certificate, base64 encoded, many lines> -----END CERTIFICATE----- --- SSL handshake has read <N> bytes and written <N> bytes --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 hint: The helper that connects from the client namespace is connect-tls, or type the long form: sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 </dev/null

It succeeded. Read the last line first: New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384. You negotiated TLS 1.3 with a machine you have never met, agreed on a cipher, and received its certificate, all in one command.

Now read the line above the certificate: 0 s:CN = server.lab and i:CN = server.lab. The subject and the issuer are the same name, which is what self-signed means in one screen.

Making this connection and reading what it reports is the first of the five things your lab grades. But something is missing from this screen, and the next step is about what.

Commit: did openssl check that certificate?

Nothing on that screen complained. The certificate signed itself, no authority on this machine vouches for it, and openssl connected anyway without a word of protest.

Read the report top to bottom

Now walk the whole screen in order, because every line of it is telling you something you will want on a real server:

Watch the same handshake from the wire

You have seen the handshake from inside the connection. Now see it from outside, as a machine on the network would.

Order matters: the handshake is over in milliseconds, so the capture has to be running before the connection starts. This waits three seconds in the background while tshark gets going, then connects:

(sleep 3; connect-tls) &

Now start the capture in that window. -a duration:8 stops it after eight seconds so you get your prompt back, and -Y tls shows only TLS packets.

Before you run it, predict how many of the five handshake messages you will be able to name on screen.

cap -a duration:8 -Y tls

prompt: student@tls-lab:~$ answer: cap -a duration:8 -Y tls ||| cap -Y tls -a duration:8 output: 1 <time> 10.0.1.10 -> 10.0.1.20 TLSv1.3 Client Hello 2 <time> 10.0.1.20 -> 10.0.1.10 TLSv1.3 Server Hello, Change Cipher Spec, Application Data 3 <time> 10.0.1.10 -> 10.0.1.20 TLSv1.3 Change Cipher Spec, Application Data 4 <time> 10.0.1.20 -> 10.0.1.10 TLSv1.3 Application Data hint: The cap helper runs tshark inside the client namespace: cap -a duration:8 -Y tls

Two readable handshake messages, Client Hello and Server Hello. Everything after the Server Hello reads as Application Data, because in TLS 1.3 the keys are agreed by the end of the Server Hello and the rest of the handshake is already encrypted.

Put the two screens side by side. openssl printed you a certificate. This capture, of the very same connection, does not contain a readable one anywhere. The server certificate is in there, inside one of those Application Data records, and tshark cannot show it to you. openssl can, because openssl is one of the two parties and holds the keys.

That is the answer to the puzzle this lesson opened with, and catching these packets is the second of the five things your lab grades.

The "Change Cipher Spec" record is a leftover from TLS 1.2 that TLS 1.3 keeps purely so that old network equipment sees a handshake it recognises and does not drop the connection. It carries no meaning in TLS 1.3. The specification calls this middlebox compatibility mode.

Commit: what does that capture give away?

Somebody is sitting on this virtual cable with tshark running, exactly as you just were, for the whole session and not only the handshake.

Milestone: you have seen both sides of the same second

Stop and count what you have done. You performed a full TLS 1.3 handshake by hand and read the certificate, the chosen cipher and the byte cost off the screen. Then you captured that identical handshake on the wire and found the certificate gone.

Most people who deploy HTTPS have never seen either view, and almost nobody has held the two next to each other. The difference between them is not a trick of tooling. It is the security property, drawn in output.

What is left is detail: getting the certificate into a readable form, reading the cipher negotiation out of the packets, and taking the two Hellos apart field by field.

Get the certificate out of the connection

The capture could not show you the certificate, but openssl can: it holds the keys, so it decrypts what tshark cannot. The -showcerts flag prints the full certificate chain in PEM format.

The </dev/null part closes standard input immediately so the command does not sit there waiting for you to type.

sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 -showcerts </dev/null

prompt: student@tls-lab:~$ answer: sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 -showcerts </dev/null output: -----BEGIN CERTIFICATE----- <the certificate, base64 encoded, many lines> -----END CERTIFICATE----- hint: The same s_client connection with the flag that prints the whole chain: sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 -showcerts </dev/null

You will see the certificate between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers. On a real server with a chain of two or three certificates, each one appears as its own block, and reading how many blocks come back is often the whole diagnosis.

That block is base64 text, which is not meant for humans. It is the certificate in its transport form: correct, complete, and useless to read. The next step decodes it.

Decode it into something readable

openssl x509 is the tool that reads a certificate. -text prints the fields in readable form, and -noout says do not print the base64 copy again underneath. Pipe the connection straight into it.

The dates come from the moment the VM generated the certificate, so yours will read differently.

sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 </dev/null 2>/dev/null | openssl x509 -text -noout

prompt: student@tls-lab:~$ answer: sudo ip netns exec client openssl s_client -connect 10.0.1.20:443 </dev/null 2>/dev/null | openssl x509 -text -noout output: Issuer: CN = server.lab Validity Not Before: <the time this VM created the certificate> GMT Not After : <the same time, 365 days later> GMT Subject: CN = server.lab Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) hint: Send the connection into the certificate reader: openssl s_client ... </dev/null 2>/dev/null | openssl x509 -text -noout

The certificate tells you four things, and each one is a real failure mode on a production server:

Getting the certificate out of the connection and reading it is the third of the five things your lab grades. Either form counts: the raw chain from -showcerts, or these decoded fields.

Read the negotiation out of the packets

The cipher suite is the combination of algorithms the client and server agreed to use. You already saw the winner in the openssl output. You can also pull it straight out of the packets, because the negotiation happens in the two readable messages.

-T fields says do not print packet summaries, print field values, and -e names the field to print. Start the traffic first, exactly as before:

(sleep 3; connect-tls) &

One line comes out per matching packet. Before you run it, decide how many lines that should be, and what the difference between them will be.

cap -a duration:8 -Y tls -T fields -e tls.handshake.ciphersuite

prompt: student@tls-lab:~$ answer: cap -a duration:8 -Y tls -T fields -e tls.handshake.ciphersuite output: 0x1302,0x1303,0x1301,... 0x1302 hint: The same capture with field output switched on and one field named: cap -a duration:8 -Y tls -T fields -e tls.handshake.ciphersuite

Two lines, and the difference between them is the negotiation itself.

The first line is the Client Hello: every suite the client off

Practice TLS in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in The Application Layer