Learn › The Network Stack › The Application Layer
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.
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.
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.
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.
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.
Read step 2 in that list again before you answer. It is the sentence the rest of this lesson hangs on.
>>> Only the two Hellos. The key share travels in the very first message, so by the end of the ServerHello both sides can compute the shared secret, and TLS 1.3 starts using it immediately. In TLS 1.3 the Certificate travels inside the encrypted part of the handshake, so a packet capture cannot read it, and neither can it read the Finished messages. A capture of this lab therefore shows exactly two readable handshake messages, ClientHello and ServerHello. Everything else, including the certificate, appears as encrypted records. If you picked the first answer, that answer was correct for years, and it is what most of the material on the internet still shows. If you picked none, the first two messages have to be readable: the two sides have no shared secret yet, so there is nothing to encrypt with.
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.
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.
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.
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.
>>> It ran and it failed, and you never saw it. openssl does check this certificate against the trusted authorities on the machine, and the check fails, because the certificate signed itself. The complaint (depth=0 CN = server.lab followed by verify error:num=18:self-signed certificate) is written to standard error, and connect-tls discards standard error to keep the screen readable. The connection completes either way, because s_client warns rather than refuses. If you picked the third answer, the signature really does match, and that is exactly the problem. A signature only proves the holder of that key signed it, and here the key belongs to the very machine whose identity is in question. A browser would stop and ask you. If you picked no check at all, take the broader lesson. A quiet screen can mean nothing went wrong, or it can mean the complaints were sent somewhere you are not looking.
Now walk the whole screen in order, because every line of it is telling you something you will want on a real server:
depth=0 CN = server.lab followed by verify error:num=18:self-signed certificate) is written to standard error, and connect-tls discards standard error to keep the screen readable. The connection completes either way, because s_client warns rather than refuses.TLS_AES_256_GCM_SHA384 is a TLS 1.3 suite: AES-256 in GCM mode for the data, and SHA-384 as the hash used to derive the keys.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.
Somebody is sitting on this virtual cable with tshark running, exactly as you just were, for the whole session and not only the handshake.
>>> That you connected to 10.0.1.20 and roughly how much data moved, but not the content. This is what HTTPS actually buys you, stated precisely. The addresses are in the IP header of every packet and cannot be encrypted, because the network needs them to deliver anything at all. Packet sizes and timing are visible for the same reason. The content, and even most of the handshake, is closed. If you picked nothing at all, that overstates the promise in a way that matters: encryption hides what you said, never that you spoke or to whom. If you picked everything, the certificate would not help even if they could read it. A certificate holds a public key, which is public by design. The private key never leaves the server.
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.
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.
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:
-days 365. Outside that window, clients reject it.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.
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.