L7 · security · reviewed
TLS
Transport Layer Security 1.3
Authenticates a server and encrypts a byte stream in one round trip — and hides less than people assume, because the parts a middlebox used to read had to stay readable.
Presenter modeEmbed this figure
Why it exists
TCP delivers a byte stream reliably and says nothing about who is at the other end or who else can read it. TLS adds both, without the application having to know how.
TLS 1.3 is not 1.2 with better ciphers. The handshake is a different shape — one round trip rather than two, forward secrecy always, and everything after the ServerHello encrypted, including the certificate. Protocol OverviewRFC 8446 · PROPOSED STANDARD · August 2018
Two of its oddities exist because of middleboxes rather than cryptography: the record version field is frozen at the value 1.2 used, and a meaningless change_cipher_spec record is sent to make the exchange look familiar. Both were concessions to equipment that rejected anything unfamiliar. Record LayerRFC 8446 · PROPOSED STANDARD · August 2018
One round trip, and what stays visible
The client guesses the key exchange so the server can answer with a key immediately. Everything after that is encrypted — except the parts that could not be.
The ClientHello carries the versions and groups the client supports — and a key share for the group it expects the server to pick. Guessing saves a round trip. Client: Sends ClientHello + key share. Passive observer. Server.
- Link
- Blocking
- Packet in flight
- Discarded
- Emphasis
Text equivalent of this diagram
| Element | Kind | State |
|---|---|---|
| Client | host | Sends: ClientHello + key share |
| Passive observer | firewall | — |
| Server | host | — |
| Client — Server | link | up |
| Passive observer — Client | link | standby · watching |
The ClientHello carries the versions and groups the client supports — and a key share for the group it expects the server to pick. Guessing saves a round trip.
If the guess is wrong the server replies HelloRetryRequest and asks for a different group, and the handshake costs the round trip after all. A client guessing badly is a measurable latency problem.
What changed
- Client: Sends → ClientHello + key share
- ClientHello: Client → Server
How it works
The client guesses which key-exchange group the server will choose and sends a share for it in the ClientHello. A right guess means the server can answer with its own share and keys immediately; a wrong one costs a HelloRetryRequest and the round trip the guess was avoiding. Client HelloRFC 8446 · PROPOSED STANDARD · August 2018
Everything from the ServerHello onward is encrypted. What stays visible is what had to be sent before any key existed: the server name, the protocols offered by ALPN, and the supported versions. ExtensionsRFC 8446 · PROPOSED STANDARD · August 2018
Both sides finish by exchanging a hash over the whole transcript, which is what makes tampering with the earlier plaintext messages detectable rather than merely unlikely. The Transcript HashRFC 8446 · PROPOSED STANDARD · August 2018
Resumption trades a round trip for a ticket. Early data goes further and sends application data before the server has spoken — with guarantees the specification is explicit are weaker, because nothing in that data proves it is fresh. 0-RTT and Anti-ReplayRFC 8446 · PROPOSED STANDARD · August 2018
On the wire
Constructed examples, encoded from the field table below them — not captured traffic.
- TCP
- Usually port 443. TLS assumes an ordered reliable byte stream and provides none of its own, which is exactly the assumption QUIC declined to make. RFC 8446
- Record header
- Content type, a frozen legacy version, and the length of what follows. RFC 8446
- Fragment
- Up to 2^14 bytes. Once the handshake has produced keys, this is ciphertext with the real content type inside it. RFC 8446
Configure it
ssl_certificate /etc/ssl/site/fullchain.pem; ssl_certificate_key /etc/ssl/site/privkey.pem;The file must be the leaf followed by the intermediates, in that order, with the root omitted. `cert.pem` rather than `fullchain.pem` is the single most common cause of "works in a browser, fails everywhere else".
Common mistake: Pointing this at the leaf alone. Browsers fetch or cache the missing intermediate, so the mistake is invisible until a container, a mobile SDK or a monitoring check meets it.
RFC 8446 §4.4.2
ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off;For TLS 1.3 the client’s preference order is the sensible one, because its list reflects what its hardware does quickly. Forcing the server’s order is a habit from 1.2 that now costs performance.
server { listen 443 ssl default_server; ssl_reject_handshake on; }What a connection without SNI, or with an unknown name, should get. Rejecting is better than serving an unrelated site’s certificate and generating a security warning that looks like an attack.
RFC 8446 §4.4.2.2
# ssl_early_data on; # only with a handler that ignores replayed requestsLeft off deliberately. Early data can be replayed verbatim and is cryptographically valid, so enabling it is a statement about the application, not about the server.
RFC 8446 §8
ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/ssl/site/chain.pem;The server fetches its own revocation status and hands it to clients, so they need not contact the authority — which removes a third party from the path of every handshake.
Verify
openssl s_client -connect host:443 -servername host -showcerts- Exactly what the server sends, in order.
openssl s_client -connect host:443 -servername host -tls1_3- That 1.3 is negotiated at all.
openssl s_client -connect host:443 -noservername- What a client without SNI receives.
openssl x509 -enddate -noout -in chain.pem- The intermediate’s expiry — the one nobody monitors.
Caveats
- The certificate file must be leaf-then-intermediates; a leaf alone works in browsers and nowhere else.
- Server cipher preference is counterproductive for 1.3 — the client knows what its hardware does quickly.
- Early data is a decision about the application’s idempotency, not a performance setting.
When it breaks
Symptom first, because that is what you have when it happens.
A browser reports a certificate error for the wrong site entirely, on a server hosting several names.
Narrow it down
- Check whether the client sent a server name indication in the ClientHello.
- Confirm which certificate the server selected.
- Test with a client that omits SNI and compare.
Cause
The server chooses its certificate from the name in the ClientHello. Without SNI it has nothing to choose on and returns the default virtual host.
Fix
Ensure the client sends SNI, or give the name its own address. Very old clients cannot send it and cannot be served by name-based virtual hosting over TLS.
Server Certificate SelectionRFC 8446 · PROPOSED STANDARD · August 2018A service works in a browser and fails from a script or container with an unknown-authority error.
Narrow it down
- Fetch the chain the server actually sends and count the certificates.
- Check whether the intermediate is included or only the leaf.
- Compare the trust stores — a browser ships its own, a container often ships almost none.
Cause
The server is not sending the intermediate. Browsers paper over this by fetching the missing certificate or reusing a cached one; a minimal client cannot and correctly refuses.
Fix
Configure the full chain — leaf then intermediates, root omitted. The browser working is not evidence the chain is correct.
CertificateRFC 8446 · PROPOSED STANDARD · August 2018Everything fails at once on a date nobody deployed anything.
Narrow it down
- Check the certificate expiry.
- Check the intermediate expiry too, which is the one nobody monitors.
- Check the clock on the client if the certificate is valid.
Cause
Validity is a wall-clock window. Either the certificate lapsed, or a client whose clock is wrong believes it did — an embedded device that boots without a battery starts in 1970 and rejects everything.
Fix
Automate renewal and alert on the intermediate as well as the leaf. Fix the clock before the certificate on devices without a real-time clock.
Handshakes to one server are consistently a round trip slower than to others, with no errors anywhere.
Narrow it down
- Look for a HelloRetryRequest in the exchange.
- Compare the group in the client’s key share against the group the server selects.
- Check whether the client’s preferred group is one the server supports at all.
Cause
The client guessed the wrong key-exchange group. The server cannot use the share it was sent, so it asks for a different one and the round trip the guess was meant to save is spent anyway.
Fix
Align the client’s first-choice group with what the server prefers. It is a latency problem, not a correctness one, which is why it goes unnoticed.
Hello Retry RequestRFC 8446 · PROPOSED STANDARD · August 2018An action occurs twice with no duplicate request in the application logs upstream of the terminator.
Narrow it down
- Check whether early data is enabled on the terminator.
- Establish whether the duplicated request travelled as early data.
- Look at whether the anti-replay measures are actually in place across the whole fleet.
Cause
Early data carries no proof of freshness, so a captured first flight can be replayed verbatim and is cryptographically valid. Single-use tickets need state shared across every server, which a fleet behind a load balancer frequently does not have.
Fix
Restrict early data to requests that are safe to repeat, or disable it. Treating it as ordinary encrypted traffic is the mistake.
0-RTT and Anti-ReplayRFC 8446 · PROPOSED STANDARD · August 2018
Design notes
Test the chain with a client that will not help you. A browser caches intermediates and fetches missing ones, so it will load a site whose chain is incomplete — and every container, script and mobile SDK will correctly refuse.
Only enable early data for requests that are safe to repeat. An attacker can replay the first flight verbatim and the server cannot distinguish it, and the anti-replay measures bound the window rather than close it. 0-RTT and Anti-ReplayRFC 8446 · PROPOSED STANDARD · August 2018
Monitor the intermediate’s expiry as well as the leaf’s, and the clock on anything without a battery. An embedded device that boots in 1970 rejects every certificate as not yet valid, which reads as a certificate problem and is not.
Do not treat SNI as private. It is on the wire in 1.3 exactly as in 1.2, which is why network-based filtering by hostname still works and why Encrypted Client Hello exists.
Misconceptions
- “TLS 1.3 is TLS 1.2 with better ciphers.”
- The handshake is a different shape. One round trip instead of two, key exchange always forward-secret, and everything after the ServerHello encrypted — including the certificate, which is why passive observers can no longer see which one was served. Protocol OverviewRFC 8446 · PROPOSED STANDARD · August 2018
- “A valid certificate means you are talking to the right server.”
- It means someone proved control of that name to a certificate authority. If an authority is compromised or coerced, or the client trusts one it should not, the certificate is valid and the peer is not who you wanted. Certificate transparency and pinning exist because validity alone is not identity.
- “SNI is encrypted in TLS 1.3.”
- It is not. The ClientHello is sent before any keys exist, so the requested name is visible on the wire. Encrypted Client Hello addresses it and is a separate, still-deploying mechanism. ExtensionsRFC 8446 · PROPOSED STANDARD · August 2018
- “The version in the record header tells you which TLS version is in use.”
- It is frozen at 0x0303 — the value TLS 1.2 used — and the specification says it must be ignored for all purposes. The real version is negotiated in the supported_versions extension, which is why capture tools report a TLS 1.3 connection as 1.2. Record LayerRFC 8446 · PROPOSED STANDARD · August 2018
More walkthroughs
Zero round trips, and what it costsdesign-choice
A ticket from a previous session lets a client send data with its first packet. That data is replayable, and the specification says so plainly.
After a successful handshake the server issues a session ticket. It is a pre-shared key the client may present next time instead of doing the whole exchange again. Client. Server: Issues NewSessionTicket. On-path attacker.
- Link
- Blocking
- Packet in flight
- Discarded
- Emphasis
Text equivalent of this diagram
| Element | Kind | State |
|---|---|---|
| Client | host | — |
| Server | host | Issues: NewSessionTicket |
| On-path attacker | firewall | — |
| Client — Server | link | up |
| On-path attacker — Server | link | standby |
After a successful handshake the server issues a session ticket. It is a pre-shared key the client may present next time instead of doing the whole exchange again.
What changed
- Server: Issues → NewSessionTicket
- Emphasis on Client ↔ Server
It works in the browserfailure
A server sends only its own certificate. Browsers hide the problem, and everything else refuses correctly.
The site loads perfectly in a browser and fails from a container with "unable to get local issuer certificate". Nobody changed anything. Server. Browser. Container / script.
- Link
- Blocking
- Packet in flight
- Discarded
- Emphasis
Text equivalent of this diagram
| Element | Kind | State |
|---|---|---|
| Server | host | — |
| Browser | host | — |
| Container / script | host | — |
| Server — Browser | link | up |
| Server — Container / script | link | up |
The site loads perfectly in a browser and fails from a container with "unable to get local issuer certificate". Nobody changed anything.
What changed
- Emphasis: Works
- Emphasis: Unknown authority
Terms
- TLS termination
- Decrypting at the proxy rather than the backend. It is what makes routing, caching and header rewriting possible, and it is why the backend has to be told the client used HTTPS.
- SNI
- Server Name Indication: the hostname a client names in the TLS handshake, in the clear, so one address can present the right certificate for many names.
- Certificate chain
- The leaf plus the intermediates a server sends so a client can build a path to a root it already trusts. The root is omitted; sending only the leaf works in browsers, which fetch what is missing, and nowhere else.