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

Protocol OverviewRFC 8446 · PROPOSED STANDARD · August 2018Client HelloRFC 8446 · PROPOSED STANDARD · August 2018CertificateRFC 8446 · PROPOSED STANDARD · August 20180-RTT and Anti-ReplayRFC 8446 · PROPOSED STANDARD · August 2018

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.

ClientSends: ClientHello + key sharePassive observerServerwatching
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
ClienthostSends: ClientHello + key share
Passive observerfirewall
Serverhost
ClientServerlinkup
Passive observerClientlinkstandby · watching
1 / 5

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.

Content type 22 is genuine here because nothing is protected yet. The version says TLS 1.2 and is frozen at that value; the real version is inside, in the supported_versions extension.

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

Terminate TLS 1.3 with a chain that satisfies a client which will not help you.

nginx 1.25 · Linux, BSDdraft

  1. 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

  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.

  3. 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

  4. # ssl_early_data on;   # only with a handler that ignores replayed requests

    Left 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

  5. 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.

  1. A browser reports a certificate error for the wrong site entirely, on a server hosting several names.

    Narrow it down

    1. Check whether the client sent a server name indication in the ClientHello.
    2. Confirm which certificate the server selected.
    3. 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 2018
  2. A service works in a browser and fails from a script or container with an unknown-authority error.

    Narrow it down

    1. Fetch the chain the server actually sends and count the certificates.
    2. Check whether the intermediate is included or only the leaf.
    3. 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 2018
  3. Everything fails at once on a date nobody deployed anything.

    Narrow it down

    1. Check the certificate expiry.
    2. Check the intermediate expiry too, which is the one nobody monitors.
    3. 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.

  4. Handshakes to one server are consistently a round trip slower than to others, with no errors anywhere.

    Narrow it down

    1. Look for a HelloRetryRequest in the exchange.
    2. Compare the group in the client’s key share against the group the server selects.
    3. 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 2018
  5. An action occurs twice with no duplicate request in the application logs upstream of the terminator.

    Narrow it down

    1. Check whether early data is enabled on the terminator.
    2. Establish whether the duplicated request travelled as early data.
    3. 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.

ClientServerIssues: NewSessionTicketOn-path attacker
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
Clienthost
ServerhostIssues: NewSessionTicket
On-path attackerfirewall
ClientServerlinkup
On-path attackerServerlinkstandby
1 / 6

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.

ServerBrowserContainer / scriptWorksUnknown authority
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
Serverhost
Browserhost
Container / scripthost
ServerBrowserlinkup
ServerContainer / scriptlinkup
1 / 5

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.

Check yourself

  • A server hosts several names on one address and returns the wrong certificate. Why?
  • A site loads in a browser and fails from curl in a container with "unable to get local issuer certificate". What is wrong?
  • What is encrypted in a TLS 1.3 handshake that was not in 1.2?
  • Why does an embedded device without a real-time clock reject every certificate on boot?
  • A capture reports TLS 1.2 for a connection you know is 1.3. Why?
  • Why is early data restricted to requests that are safe to repeat?
  • What does the change_cipher_spec record do in TLS 1.3?
  • What does a valid certificate actually establish?