TLS: Trust On The Wire
You already covered the ops side of this in HTTPS, TLS & CDNs — certificates as things you get from Let’s Encrypt and let Cloudflare terminate. This module is the other half: what the handshake actually does at runtime, what it protects (and what it provably does not), and how to read a TLS failure when curl throws one at you at 2am.
The Goal
By the end of this module you can:
- State the two jobs of TLS — encryption and identity — and say which one everyone forgets
- Walk the handshake from hello to session key, and explain why it starts asymmetric and switches to symmetric
- Read a certificate as a claim: this public key belongs to this domain, vouched by a chain you can name
- Decode the four classic TLS errors and say who must fix each one
- Defend plain HTTP on localhost and say exactly what changes at deploy
- Explain why a JWT without TLS is a password written on a postcard
The Lesson
The two jobs of TLS
TLS does two things, and most people remember only the first:
- Encryption — nobody between you and the server can read the bytes.
- Identity — you are talking to the server that actually owns the certificate for that domain name.
The second job is the one that earns its keep. Encryption without identity is worthless: you could be having a perfectly private conversation with the attacker. The padlock in your browser means exactly this and no more: “you are talking privately to whoever owns this certificate.” It does NOT mean the site is honest, safe, or competent — phishing sites have valid padlocks too, because a certificate asserts domain ownership, not virtue.
Burn that in, because both halves of the mistake are common: trusting a padlocked scam site, and (worse, in your job) writing code that keeps the encryption but skips the identity check. You will meet that exact sin in Build This as a curl flag.
The handshake, conceptually
First, place it. TLS is a layer with a fixed position in the request you traced in module 01:
flowchart LR
A["DNS lookup"] --> B["TCP handshake"]
B --> C["TLS handshake"]
C --> D["HTTP request and response"]
After TCP’s three packets (module 02) and before the first byte of HTTP (module 04), client and server run the TLS handshake:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: hello with supported TLS versions and ciphers
S->>C: chosen cipher plus certificate
Note over C: verify certificate chain against trusted CAs
C->>S: key exchange completes
Note over C,S: both now hold the same session key
C->>S: encrypted HTTP from here on
In words: the client says hello and lists what it can speak. The server picks a cipher and presents its certificate. The client verifies that certificate against the Certificate Authorities its OS already trusts — this is the identity job happening. Then a key exchange establishes a shared session key that only these two parties know, and from that moment everything — your request line, your headers, your Authorization: Bearer, the response — travels symmetrically encrypted with that key.
Why two kinds of cryptography? Asymmetric crypto (public/private key pairs) solves the impossible-sounding problem of agreeing on a secret over a wire an attacker can read — but it is computationally expensive, hundreds of times slower than symmetric crypto. Symmetric crypto (one shared key) is extremely fast but needs both sides to already have the key. So TLS uses each for what it is good at: asymmetric for the brief handshake that establishes the secret, symmetric for the gigabytes that follow. That is the entire performance story in one trade-off, and it is a standard interview question.
The handshake costs round trips on top of TCP’s — which is exactly why HTTP/3 collapsed the transport and TLS handshakes together, as the version table in module 04 noted.
One version note, because you will see it in Build This: the line TLSv1.3 in curl output names the protocol version. Two versions matter today:
| Version | Status | What changed |
|---|---|---|
| TLS 1.2 | Still everywhere, still fine | The long-reigning default |
| TLS 1.3 | The modern standard | Handshake cut to fewer round trips, old weak ciphers removed outright |
Anything older (TLS 1.0/1.1, or anything called SSL) is deprecated — if a 2am error mentions an ancient version, the fix is “upgrade the server config,” not “make the client accept it.”
What a certificate actually asserts
A certificate is a small signed file making one precise claim: “this public key belongs to this domain name.” Nothing about the company being real, the site being safe, or the code being good. Public key, domain name, validity dates, signature. That is the claim.
Who vouches for it? A chain:
| Link | What it is | Who signed it |
|---|---|---|
| Leaf | The site’s certificate — example.com’s public key | An intermediate CA |
| Intermediate | A CA’s working certificate | The root CA |
| Root | The anchor — preinstalled in your OS and browser trust store | Itself (self-signed, trusted by distribution) |
Your machine ships with a few dozen trusted roots. Verification walks the chain: the leaf is signed by an intermediate you can check, the intermediate is signed by a root you already trust. If every signature holds, the dates are valid, and the domain name matches the one you asked for, identity is established. Any link failing — that is your TLS error, and there is a table of those coming.
What TLS does NOT hide
Honesty paragraph, because the padlock gets oversold. TLS encrypts the content of your connection, not its existence. The domain you connected to leaks in the clear via SNI — the client names the host at the start of the handshake so a server hosting many sites knows which certificate to present — so your ISP and any middlebox can see you talked to example.com, just not what you said. Packet sizes and timing leak too: an observer watching encrypted traffic can often infer which page of a known site you loaded, purely from the rhythm and volume of bytes. And DNS lookups (module 03) travel before TLS even starts. TLS makes the conversation private; it does not make you invisible.
What that means for an observer on the path:
| Hidden by TLS | Still visible |
|---|---|
| The path and query string you requested | The domain you connected to (SNI, and the earlier DNS lookup) |
Every header, including Authorization | The server’s IP and port |
| Request and response bodies | How many bytes moved, and when |
| Status codes and cookies | That a TLS session exists at all |
TLS errors, decoded
Four failures cover almost everything you will ever see. The crucial column is the last one — TLS errors are almost always the server operator’s to fix, and “make the client ignore it” is never the fix:
| Error | What it means | Who must fix it |
|---|---|---|
| Expired certificate | The validity dates have passed — renewal was missed | Server operator: renew (or fix the automation that should have) |
| Hostname mismatch | The cert is valid but for a different name than the one you dialled | Server operator: issue a cert covering that name — or you dialled the wrong host |
| Self-signed certificate | No chain to any trusted root — it vouches only for itself | Server operator: get a CA-signed cert (free via Let’s Encrypt) |
| Incomplete chain | Server sent the leaf but not the intermediate, so the client cannot walk to a root | Server operator: configure the full chain bundle |
Note what is happening in each row: the identity job is failing, not the encryption. The math would encrypt fine in every case — the client just cannot prove who it is encrypting to, so it correctly refuses. Clients that override the refusal are choosing encryption-to-possibly-an-attacker, which is the worthless combination from the top of this lesson.
Localhost honesty
Your splitease-api runs on http://localhost:8080 — no TLS — and that is fine. Traffic to localhost never leaves your machine; there is no wire for an attacker to sit on. Setting up local certificates buys you nothing today and costs you real friction. Browsers agree: they treat http://localhost as a secure context, so even APIs that demand HTTPS elsewhere work in local development.
What changes at deploy: in week 9 you deploy to a platform (Railway/Render), and the platform terminates TLS for you — the padlock, the certificate, the renewals all happen at their edge, and your Spring app keeps speaking plain HTTP behind it, exactly like the CDN termination picture from the web-infrastructure module. You will likely never hand-configure a certificate into Spring Boot itself, and that is the industry norm, not a shortcut.
The rule to carry: plain HTTP on localhost, TLS on every wire that crosses a machine boundary, terminated by the platform.
The JWT connection
Here is where this module touches code you wrote. Your security module sends Authorization: Bearer <token> on every request — picture the JwtFlowViz lanes, the token crossing from client to filter. Now ask: what does that crossing look like on the wire?
Over plain HTTP, that header is readable text — module 04 proved headers are just lines. Every middlebox, proxy, coffee-shop router, and ISP on the path can read the token, and whoever reads it is you until it expires; a JWT is a bearer credential, possession equals identity. Over TLS, the entire HTTP message — request line, headers, token, body — is inside the encrypted channel; observers see only which host you contacted (SNI, as confessed above) and how many bytes moved.
So TLS is not an optional polish layer on your auth design — it is the assumption your whole token scheme silently stands on. JWT without TLS is authentication theatre.
Check The Concept
How This Shows Up At Work
- The expired-cert outage. A certificate someone renewed manually two years ago expires on a Saturday; every client starts refusing connections at the same second. It is the most common self-inflicted outage in the industry, and the on-call who recognizes “expired, server side, renew it” from the error string ends the incident in minutes.
- Code review. A PR disables certificate verification “temporarily” to unblock a staging integration (
-k, orverify=false, or a trust-all manager in Java). You block it: that line keeps encryption and deletes identity, and temporary flags like this are how production talks to attackers. The right fix is on the server. - The interview pair. “Walk me through what happens when you hit an HTTPS URL” expects DNS → TCP → TLS handshake → HTTP in order, and the follow-up “why symmetric after asymmetric?” expects the performance trade-off in one sentence. You now have both.
- The deploy-day mismatch. First deploy behind a new domain, and clients throw hostname mismatch — the platform cert covers
yourapp.onrailway.appbut you dialled the bare custom domain you have not attached yet. Error table, row two: cert coverage, server side, five-minute fix once you can name it. - The office-laptop mystery. On a corporate machine, every site’s certificate chain ends at the company’s own root instead of DigiCert’s. That is a TLS-inspecting proxy: IT installed their root into the laptop’s trust store, so the proxy can re-sign and read everything. The chain model explains in one look what “my requests work at home but fail in the office” usually means.
Build This
Reminder from module 04: in PowerShell type curl.exe, not curl. One useful check first — run curl.exe -V and look at the first line: Windows’ bundled curl says Schannel (Windows’ TLS library), other builds say OpenSSL. The handshake is identical; the verbose wording differs.
- Watch a real handshake:
curl.exe -v https://example.com/
Read the lines before any > appears — that is TLS happening after TCP and before HTTP. Depending on your build you will see lines like:
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
* subject: CN=example.com
* start date: Jan 15 00:00:00 2026 GMT
* expire date: Jan 15 23:59:59 2027 GMT
* issuer: C=US; O=DigiCert Inc; CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
* SSL certificate verify ok.
Annotate it: the negotiated TLS version and cipher (the hello agreed on these), the subject (the domain the cert vouches for — the identity claim), the dates (row one of the error table waits at the expire date), the issuer (the intermediate that signed the leaf), and verify ok (your machine walked the chain to a trusted root and every signature held). The Schannel build prints less detail here — if yours does, do this same reading in step 2 instead.
-
See the full chain in the browser: open
https://example.com, click the padlock → connection is secure → certificate. Walk the chain display top to bottom: root (preinstalled in Windows), intermediate (signed by the root), leaf (signed by the intermediate, namingexample.comand its public key). That picture is the trust model — three links, two signatures, one preinstalled anchor. -
Isolate the layers like an on-call engineer. Before blaming TLS, prove the layers under it:
Test-NetConnection -ComputerName expired.badssl.com -Port 443
Expect TcpTestSucceeded : True. Read what that proves: DNS resolved, the host is up, the port is open, TCP connects — every layer below TLS is healthy, and yet the next step will fail. This is the discrimination that ends arguments at 2am: “the network is fine, the certificate is the problem.” A TCP success plus a TLS failure points at exactly one layer.
- Break it on purpose — expired cert. badssl.com is a free site built precisely to serve broken TLS safely:
curl.exe -v https://expired.badssl.com/
Expect failure — Schannel says something like:
curl: (60) schannel: CertGetCertificateChain trust error CERT_TRUST_IS_NOT_TIME_VALID
(OpenSSL builds: SSL certificate problem: certificate has expired.) Decode it against the table: NOT TIME VALID = the dates failed = expired = server operator must renew. Note what curl did: it refused to send even one byte of HTTP. No request line ever crossed.
- Break it again — hostname mismatch:
curl.exe -v https://wrong.host.badssl.com/
Schannel: SEC_E_WRONG_PRINCIPAL — “wrong principal” is Windows for “this certificate vouches for a different name than the one you dialled.” (OpenSSL builds name the mismatch explicitly.) Table, row two: valid cert, wrong coverage, server side.
- Now commit the sin, once, on a safe target — override verification:
curl.exe -vk https://expired.badssl.com/
It “works”: handshake completes, HTML comes back. Say out loud what -k did — it kept the encryption and skipped the identity check, the worthless combination. On badssl, a teaching site, this is harmless. In production code — a deploy script, a Java TrustManager that trusts everything, verify=false in a client — it means your system will happily hold private conversations with whoever intercepts it. -k is a debugging probe (“is the only problem the certificate?”), never a fix, and it must never appear in committed code.
Where to Practice
| Resource | What to do there | How long |
|---|---|---|
| badssl.com | Visit the homepage in a browser and click through 5 or 6 broken subdomains (self-signed, untrusted-root, expired) — read each browser error against the table | 20 min |
| Cloudflare Learning Center (cloudflare.com/learning) | Read “What happens in a TLS handshake?” and check each step against the sequence diagram above | 20 min |
| curl docs (curl.se) | Read the -k / --insecure section of the manual — note how curl’s own docs warn about it | 10 min |
| MDN HTTP docs (developer.mozilla.org) | Read “Transport Layer Security” under HTTP security — skim for the parts this module skipped | 15 min |
Check Yourself
- TLS has two jobs. Name both, and say which one the padlock icon is widely misread as exceeding.
- Walk the handshake in five steps, from hello to encrypted HTTP.
- Why asymmetric crypto for the handshake but symmetric for the data?
- What single claim does a certificate make, and who vouches for it — name the three links.
- An observer can see something about your TLS connection. What leaks, and via what?
- Expired cert vs hostname mismatch vs self-signed: one line each on who fixes it.
- Why is plain HTTP acceptable for
splitease-apion localhost, and what handles TLS after week 9’s deploy? - Finish the sentence and justify it: “A JWT sent over plain HTTP is…”
Answers
- Encryption (nobody on the path reads the bytes) and identity (you reached the certificate’s owner). The padlock is misread as a trustworthiness badge — it only proves private conversation with that domain’s certificate holder.
- Client hello with supported versions/ciphers → server picks a cipher and sends its certificate → client verifies the chain against trusted CAs → key exchange establishes a shared session key → all further traffic symmetrically encrypted with it.
- Asymmetric solves key agreement over a readable wire but is slow; symmetric is fast but needs a shared key. Use the slow tool once to create the key, the fast tool for everything after.
- “This public key belongs to this domain name.” Vouched by the chain: leaf (the site) signed by an intermediate CA, signed by a root CA preinstalled in your OS trust store.
- The destination domain leaks via SNI in the clear at handshake start; packet sizes and timing leak usage patterns; DNS lookups happen before TLS at all. Content stays hidden, destination and rhythm do not.
- Expired: dates passed — server operator renews. Hostname mismatch: cert covers a different name — operator extends coverage (or you dialled the wrong host). Self-signed: no chain to a trusted root — operator gets a CA-signed cert. All server side; never “fixed” by disabling client checks.
- Localhost traffic never leaves the machine, so there is no wire to protect. At deploy, the platform (Railway/Render) terminates TLS at its edge and the Spring app speaks plain HTTP behind it.
- …readable by every middlebox on the path — and a JWT is a bearer credential, so whoever reads it is you until expiry. TLS is the assumption the whole token scheme stands on.
Explain it out loud: Explain to the empty chair why curl -k made the expired-badssl request “work,” what was silently lost, and why that flag in a production deploy script could one day mean your API sends customer tokens to an attacker — walking through the handshake step that -k deletes. If the identity half goes fuzzy mid-sentence, re-read the two-jobs section.
Still Unclear?
Explain the TLS handshake to me using a sealed-envelope courier analogy, but
make the identity verification step the hero of the story, not the encryption.
Then quiz me: give me 4 TLS error messages and make me say who must fix each
one before you confirm.
I ran curl -v against an HTTPS site and here is everything before the first
> line: [paste]. Walk me through each line — version, cipher, subject, issuer,
dates, verify result — asking me what each means before you explain it.
Why is asymmetric cryptography too slow to encrypt all traffic, in concrete
terms? Give me rough relative costs, then make me explain back why TLS still
needs the asymmetric step at all and what breaks if you skip it.
Why AI Can’t Do This For You
When TLS breaks in production it breaks as an opaque one-liner — SEC_E_WRONG_PRINCIPAL at 2am, a partner saying “your endpoint stopped working” after a cert rotation — and the model cannot see your wire. Decoding which link of which chain failed, on whose side, while traffic is down, is pattern recognition you build by reading real handshakes and real failures — which is exactly what badssl just gave you, in a place where failing was free.
Worse, AI will cheerfully generate the sin: ask a model for “Java HTTP client code that handles SSL errors” and trust-all TrustManager boilerplate appears in seconds, compiling cleanly and deleting the identity half of TLS. Only an engineer who knows what verification proves will recognize that snippet as a vulnerability instead of a fix — and that judgement is precisely what this module installed.
Module done? Add it to today’s tracker