Skip to content

Networking & APIs· 3 of 13

TLS and Encryption in Transit

Anyone can send your certificate. Only one signature proves it is you, and it says nothing about sizes, replays or what happens after the load balancer.

intermediate44 min readReviewed 22 October 2026

Your bank’s certificate is public. Anyone on the café Wi-Fi can download it and send it to you, byte for byte, when you try to connect. So what exactly stops them from being the bank? And if they cannot be the bank, how can they still tell which page of a clinic’s website you read, or copy your encrypted payment request and have the bank’s servers run it twice, without breaking a single cipher?

“TLS encrypts, certificates prove identity” is true and explains none of that. This page is about what the handshake really proves, what it leaves in the clear, which keys can undo it years later, why revocation barely works, and what breaks as certificate lifetimes fall to 47 days.

TLS (Transport Layer Security) protects one byte stream between two endpoints. It is built from two kinds of cryptography, and the rest of the page leans on the difference:

  • Symmetric (shared-key) encryption uses one key both to lock and to unlock. AES is the common one, and every modern CPU has instructions for it, so it is fast. But both sides must already hold the same key, and two strangers on the internet do not.
  • Public-key cryptography gives each party a key pair: a private key it keeps secret and a public key it gives to anyone. What one key does, only the other can undo or check. Anyone can encrypt to the bank’s public key and only the bank can decrypt; only the bank can sign with its private key, and anyone can check the signature with the public one. No shared secret is needed, but the operations are far slower and work only on small inputs.

So TLS uses public-key operations only during the handshake, to agree on a fresh symmetric key and to prove who is on the other end, and then encrypts the data with that symmetric key. A certificate ties a public key to a name: a small public document that says “this public key belongs to bank.example.in”, signed by a certificate authority (CA). The CA’s own certificate is signed in turn, up to a root CA whose certificate ships inside your operating system or browser, in its trust store.

Stated precisely, after a successful handshake the client knows this:

The bytes I receive were sent by someone holding the private key of a certificate that names the host I asked for and chains to a root in my trust store, and nobody has changed, reordered, dropped or replayed them within this connection.

Every qualifier in that sentence is a place where real systems break, and each has a section below:

  • “Someone holding the private key”: the certificate is public; the key is the secret. A stolen key makes the thief the bank until the certificate expires, because revocation does not really work.
  • “The host I asked for”: only if the client checks the name. verify=False drops this, and with it everything else.
  • “A root in my trust store”: about 120 roots, any of which may sign for any name, plus any root your employer installed on your laptop.
  • “Within this connection”: a copy of the first flight, sent on a new connection, is outside the promise. That is the 0-RTT replay.
  • What is not in the sentence at all: who you talk to, when and how much. Sizes and timing are visible. So is the site name, unless Encrypted Client Hello is in use.

Once the handshake has produced a symmetric key, the data travels in records of up to 16 KB. Encryption alone hides bytes but does not stop someone flipping them: a changed ciphertext decrypts to changed plaintext, and the receiver cannot tell. So each record also carries a tag, or MAC (message authentication code): a 16-byte value computed from the key and the encrypted bytes. Change one bit and the tag no longer matches, and without the key nobody can compute one that does. A cipher that encrypts and tags in one operation is an AEAD (authenticated encryption with associated data, the associated data being the record header, which is checked but left readable). TLS 1.3 allows only AEAD ciphers; in practice AES-GCM or ChaCha20-Poly1305.

The last ingredient is a nonce, a number used only once per key and mixed into each record so that two identical records encrypt differently. TLS takes it from a record counter that both sides keep and never send. Swap records 1 and 2 on the wire, and the receiver opens the first to arrive with nonce 1, but it was sealed with nonce 2: the tag fails and the connection is torn down. Dropped and repeated records fail the same way. The record layer is the part of TLS that rarely goes wrong. The handshake that creates its keys is where the interesting failures are.

Two strangers can agree on a symmetric key in public with Diffie-Hellman key exchange. It rests on one piece of arithmetic. Write x mod p for the remainder after dividing x by p: 27 mod 23 = 4. Raising a number to a power and then taking the remainder is fast, even for numbers hundreds of digits long. Going back, from the remainder to the exponent, has no known shortcut at those sizes.

Both sides agree in public on a prime p and a base g. Each picks a private random exponent and sends g to that power mod p, its key share. Each then raises the other’s share to its own exponent. Both arrive at the same number, because (ga)b = (gb)a, and that number is the shared secret. With numbers small enough to check:

Diffie-Hellman key agreement with small numbersEveryone, including eavesdroppers, knows p = 23 and g = 5. The client picks a secret 6 and sends 5 to the power 6 mod 23, which is 8. The bank picks a secret 15 and sends 5 to the power 15 mod 23, which is 19. The client computes 19 to the power 6 mod 23 and gets 2; the bank computes 8 to the power 15 mod 23 and also gets 2. The wire carried 23, 5, 8 and 19, never 2, and never the secrets.public: p = 23, g = 5Clientthe wireBanksecret a = 6secret b = 155⁶ mod 23 = 8A = 8 →← B = 195¹⁵ mod 23 = 1919⁶ mod 23 = 28¹⁵ mod 23 = 2never sentnever sentThe wire saw 23, 5, 8 and 19. Getting 2from those needs a secret. With 255-bitnumbers, nobody can work one out.
Each side keeps its exponent, sends 5 to that power mod 23, and raises the other's share to its own exponent. Both reach 2, while the wire carried only 23, 5, 8 and 19.

That defeats anyone who only listens. It does nothing against someone who answers. Here is the attack the hotspot owner, Mallory, runs:

Step throughA key exchange with a man in the middle
A key exchange with a man in the middleSequence between You, Mallory, Bank, in 6 steps.YouMalloryBank1your share2her share 13bank's share4her share 25two keys6pay ₹900
  1. Step 1 of 7your share

    You send your key share towards the bank. Every packet passes through the hotspot Mallory runs, so she keeps it.

  2. Step 2 of 7her share 1

    She sends the bank a share she generated herself. A share is a large random-looking number; nothing in it says who made it.

  3. Step 3 of 7bank's share

    The bank replies with its share, believing it is talking to you.

  4. Step 4 of 7her share 2

    Mallory keeps the bank's share and sends you a second share of her own.

  5. Step 5 of 7two keys

    You share a key with Mallory; the bank shares a different key with Mallory. Neither of you shares a key with the other.

  6. Step 6 of 7pay ₹900

    Your encrypted "pay ₹100" arrives at Mallory. She decrypts it, rewrites it, encrypts it again with the bank-side key and computes a fresh, valid integrity tag.

  7. Step 7 of 7The whole flow

    Every message was encrypted and every tag was valid. Diffie-Hellman kept the key secret from listeners, but nothing told either side who sent the key shares.

The mitm_dh example runs this with real 2048-bit Diffie-Hellman numbers and an encrypt-then-MAC channel:

python mitm_dh.py (node mitm-dh.mjs prints the same, with other random keys)
Key fingerprints (first 8 bytes of each key):
Alice: 5c0e3a752bcbe3c0 Mallory's copy: 5c0e3a752bcbe3c0
Bank: 6619d571a8c95184 Mallory's copy: 6619d571a8c95184
Alice and the bank hold different keys, and Mallory holds both.
Alice sends an encrypted, integrity-protected payment.
Mallory reads: {"to": "ravi@okbank", "amount_inr": 100}
Bank receives: {"to": "mallory@ok", "amount_inr": 900} (tag valid)

Alice and the bank hold different keys. Comparing fingerprints over a channel Mallory cannot touch would expose her, which is what SSH asks you to do on first connection. TLS needs the same check to happen automatically, for a billion clients and servers that have never met.

The fix needs two more building blocks.

A hash function turns any input into a short, fixed-size fingerprint. SHA-256, the one TLS usually uses, gives 32 bytes. The same input always gives the same output, changing one character changes the output completely, and nobody can construct a second input with the same hash. The SHA-256 of pay ₹100 starts 14695baf; that of pay ₹900 starts 044dcdf1. A hash has no key, so anyone can compute one, which is what lets two sides compare.

A digital signature is a value that only the private key can produce for a given hash, and that anyone holding the public key can check. Here it is with toy RSA numbers: the public key is n = 33 and e = 3, the private key is d = 7. To sign a message whose hash is 4, the signer computes 47 mod 33 = 16 and sends 16 with the message. The verifier computes 163 mod 33 = 4, hashes the message it received itself, and accepts only if both give 4. If Mallory changes the message so its hash becomes 5, the signature still opens to 4 and the check fails. The signature that opens to 5 is 14, and computing it needs d. Real signatures use 2048-bit RSA or elliptic-curve keys, where d cannot be worked out from the public key.

The server signs the conversation. After sending its certificate, the server takes a hash of every handshake message so far, the transcript, with both random values and both key shares included, and signs that hash with the private key that matches the certificate. This signature is the CertificateVerify message. Both sides then send Finished, a MAC over the same transcript using keys derived from the shared secret.

What the CertificateVerify signature coversFour handshake messages stacked on the left: ClientHello with the client's random value, key share A and the server name; ServerHello with the server's random value and key share B; EncryptedExtensions; and the Certificate chain for shop.example.in. All four feed one transcript hash. The server signs that hash with the private key that matches the certificate: that signature is CertificateVerify. A note on the right: if Mallory swaps key share A, the hash changes, and the signature no longer verifies, because she cannot sign without the private key. At the bottom, the Finished messages: a MAC over the same transcript, in both directions.everything the server signsClientHellorandom, key share A, SNIServerHellorandom, key share BEncryptedExtensionsALPN choice, extrasCertificatechain for shop.example.inMallory swapskey share A:the hashchanges, andshe has noprivate keyto re-signtranscript hashCertificateVerifysigned with the certificate's private keythen Finished: a MAC over it, both ways
The server's signature covers a hash of the whole handshake so far, including the client's key share. Swap any byte and the hash changes; only the holder of the private key can produce a new signature.

Now replay Mallory’s attack. If she forwards your key share to the bank unchanged, the bank’s signature is valid, but the key is shared between you and the bank, and she cannot read anything. If she swaps in her own share, the bank signs a transcript containing her share; you check the signature against a transcript containing yours, and the hashes differ. To fix that, she would have to sign your transcript herself, with the bank’s private key. Sending the bank’s certificate costs her nothing. Producing that signature is the one thing she cannot do.

That is the whole of server authentication in TLS 1.3: a certificate that the client’s trust store vouches for, plus a fresh signature from its key over this particular handshake. Everything else on this page is about what that sentence does not cover.

What the handshake proves, message by message

Section titled “What the handshake proves, message by message”

Two terms first. A group is a named, standard choice of Diffie-Hellman parameters, the real-size version of p = 23 and g = 5. X25519 is the common one, an elliptic-curve form of the same idea with 32-byte shares. And TLS never uses the shared secret directly as a key. It feeds the secret and the transcript hash through a hash-based function (HKDF) that derives a separate key for each direction and each stage, so changing any input changes every key.

Step throughA full TLS 1.3 handshake
A full TLS 1.3 handshakeSequence between Client, Server, in 8 steps.ClientServer1ClientHello2ServerHello3EncryptedExt4Certificate5CertVerify6Finished7verify all8Finished + GET
  1. Step 1 of 9ClientHello

    A fresh 32-byte random value, supported versions and ciphers, the site name (SNI), the application protocols (ALPN), and key shares already computed for the groups it guesses the server supports, such as X25519MLKEM768 and X25519.

  2. Step 2 of 9ServerHello

    Its own random value and key share for one chosen group. Both sides now compute handshake keys; everything after this message is encrypted.

  3. Step 3 of 9EncryptedExt

    The rest of the server's choices, such as h2 or http/1.1. TLS 1.2 sent these in the clear.

  4. Step 4 of 9Certificate

    The chain: the server's certificate and its intermediates, without the root. Encrypted in TLS 1.3, readable by anyone in TLS 1.2.

  5. Step 5 of 9CertVerify

    A signature over the transcript hash with the certificate's private key. This is the step a man in the middle cannot fake.

  6. Step 6 of 9Finished

    A MAC over the transcript with keys only the two real endpoints can derive.

  7. Step 7 of 9verify all

    The client builds the chain to a trusted root, checks the name, the dates, the signature and the Finished MAC. Any failure ends the connection here.

  8. Step 8 of 9Finished + GET

    Its own Finished and, in the same flight, the first encrypted request. One round trip after the ClientHello.

  9. Step 9 of 9The whole flow

    The key shares give a fresh secret; the certificate says which key belongs to the name; CertificateVerify proves that key's holder took part in this exact handshake; Finished proves nobody altered any message.

Each way of attacking the handshake meets one specific defence:

Attack What stops it
Swap a key share CertificateVerify covers both shares; the signature fails
Replay a recorded handshake from yesterday The client’s random value is new, so yesterday’s signature covers a different transcript
Edit the ClientHello to remove strong ciphers or force TLS 1.2 Finished covers the ClientHello; a TLS 1.3 server pushed to 1.2 also writes a fixed marker into the last 8 bytes of its random value, which TLS 1.3 clients check
Present a certificate for the name from a CA you made Chain building fails: your root is not in the client’s store
Present a certificate from a CA the client does trust Nothing in TLS. Certificate Transparency makes it visible afterwards

The last row is the honest limit. The client authenticates “a key some trusted CA vouched for”, not “the bank”. In 2011 attackers who broke into the Dutch CA DigiNotar issued a *.google.com certificate that intercepted Gmail for around 300,000 IP addresses, almost all in Iran (Fox-IT interim report). Since then, Certificate Transparency (RFC 6962) requires CAs to put every certificate into public append-only logs, and Chrome refuses certificates without proof of logging from at least two log operators (Chrome CT policy). Mis-issuance still works for a while, but it can no longer stay secret: search crt.sh for your own domains. A CAA record in your DNS zone narrows the field before issuance: it names the CAs allowed to issue for your domain, and every public CA must check it (RFC 8659). It stops an honest CA from being tricked into issuing for you; it cannot stop a compromised one that ignores it.

A corporate inspection proxy uses the same limit on purpose. It issues certificates on the fly from a company root installed on managed laptops, and from the browser’s point of view every check passes. TLS worked perfectly; the endpoint you authenticated was the proxy.

Notice also what the server learned about the client: nothing. In ordinary TLS the client is anonymous until the application asks for a password or token, and that token can be replayed from anywhere. Mutual TLS, later on this page, fixes that between services.

Forward secrecy, and the keys that quietly undo it

Section titled “Forward secrecy, and the keys that quietly undo it”

Forward secrecy means that stealing a server’s long-term private key next year does not decrypt traffic recorded today. It comes from the word ephemeral: both sides pick fresh Diffie-Hellman secrets per handshake and delete them afterwards, so the secret that protected a recording no longer exists anywhere. In the toy exchange, once both sides forget 6 and 15, the only way from a recording of 8 and 19 back to 2 is the hard reverse step, for the bank as much as for anyone. The certificate’s key only signs; it never encrypts the session secret. TLS 1.2’s older RSA key exchange did the opposite: the client encrypted the session secret to the server’s certificate key, so one stolen key decrypts every recording ever made. TLS 1.3 removed it.

The part teams miss is session resumption. A full handshake costs a key exchange, a signature and a chain check; a client that returns a minute later would rather reuse a secret from its last connection. The server could keep those secrets in a table, but in a cluster the next connection may land on any server. So the server hands the secret to the client, locked: it encrypts it with a symmetric key of its own, the session ticket encryption key (STEK), and sends the result as a session ticket. The client cannot read the ticket; it only stores it. On Monday the client receives ticket T. On Tuesday it puts T in its ClientHello, server 7 of 10 decrypts T with the same STEK, recovers the secret, and both sides continue from it without the certificate or signature.

  • In TLS 1.2, the ticket contains the connection’s master secret, the one value from which every key of that connection was derived. Anyone who records traffic and later steals the STEK can decrypt the ticket and then the whole connection, including the original full handshake. Many servers created one STEK at startup and kept it for months, so in practice forward secrecy lasted as long as the process (Langley, 2013).
  • In TLS 1.3, the ticket holds a resumption secret derived one way from the old session, and resumption normally mixes in a fresh Diffie-Hellman exchange (the psk_dhe_ke mode browsers use). A stolen STEK then decrypts neither the old connection nor the resumed one. It does decrypt any 0-RTT early data, which is encrypted with the ticket’s key alone.

So the STEK is a long-term key in disguise. Rotate it every few hours to a day, distribute it to the cluster as a secret, and keep old ones only as long as tickets live (at most 7 days in TLS 1.3).

Harvest now, decrypt later is the same problem at a larger scale. A large enough quantum computer could work back from an X25519 share to its secret, so traffic recorded today could be opened then. The fix is a hybrid key exchange, X25519MLKEM768, which runs X25519 and ML-KEM side by side. ML-KEM agrees on a secret using different mathematics (lattice problems) that no known quantum algorithm breaks. Both secrets go into key derivation, so an attacker must break both, and the session is safe if either holds (RFC 10024, August 2026). The cost is size: the client’s share grows from 32 to 1,216 bytes, so the ClientHello no longer fits in one packet, and middleboxes that assumed it would broke when browsers turned it on (tldr.fail). As of February 2026, over 60% of clients reaching Cloudflare offered it, but only about 10% of origin servers supported it (Cloudflare).

Encryption hides content, not shape. Here is what someone on the path learns from one visit, with no keys at all:

What an observer sees in one HTTPS visit over TLS 1.3Seven rows. Seen: the IP addresses and ports, always; the DNS lookup, unless DNS over HTTPS or TLS is used; the server name in SNI, unless Encrypted Client Hello is used; the ALPN offer and key shares in the ClientHello; and the size and timing of every record, always. Hidden: the certificate chain, which TLS 1.2 sent in the clear; and the path, headers, cookies and body.one visit, watched from the café Wi-FiIP addresses and portsalwaysseenThe DNS lookupunless DNS over HTTPS or TLSseenServer name (SNI)unless Encrypted Client HelloseenALPN offer, key sharesin the ClientHelloseenCertificate chainTLS 1.2 sent it in the clearhiddenPath, headers, cookiesand the bodyhiddenSize and timingof every record, alwaysseen
An on-path observer learns the addresses, usually the DNS lookup and the site name, the handshake offers, and the size and timing of every record. TLS 1.3 hides the certificate, the path, headers and body.

The SNI (server name indication) is in the ClientHello because the server needs it to pick a certificate before any keys exist. Encrypted Client Hello (ECH, RFC 9849, March 2026) uses public-key encryption to hide it. The hosting provider publishes an ECH public key in its DNS records. The client encrypts the real ClientHello, with the real name, to that key and wraps it inside an outer ClientHello whose SNI names only the provider: the observer reads ech.provider.example, and only the provider’s private key opens the inner clinic.example.in. It hides the site only when many sites share that provider’s addresses. A site on its own IP address gains nothing: the address already names it.

Sizes leak more than names. Look at the one field TLS cannot hide:

Anatomy of a TLS 1.3 recordA record is a 5-byte header in the clear, saying "application data", a version number kept for old middleboxes, and the length; then the encrypted payload, holding up to 16 kilobytes of your data and the real content type; then a 16-byte tag. The tag is computed over the header and the payload with a nonce made from the record's sequence number, so a changed, reordered, replayed or missing record fails to decrypt. An observer sees only the lengths and timing.one record, as it crosses the networkheaderencrypted payloadtag5 bytes,readableyour data, up to 16 KB,plus the real content type16 bytesAn observer sees• that it is TLS• each record's length• when records flow• the IP addressesThe tag catches• any changed byte• a reordered record• a replayed record• a dropped record
Every record starts with a 5-byte header in the clear, including its length. In TLS 1.3 with AES-GCM the length is the plaintext plus 17 bytes: one for the hidden content type and 16 for the tag.

The observer example puts a relay with no keys between a client and a server, the position of every Wi-Fi router, and reads only the plaintext parts:

observer.py (excerpt)
# runnable version: examples/tls/python/observer.py
def records(stream: bytes) -> list[tuple[int, int]]:
"""Split raw TLS bytes into (content type, length) using only the 5-byte plain headers."""
out, i = [], 0
while i + 5 <= len(stream):
length = int.from_bytes(stream[i + 3 : i + 5], "big")
out.append((stream[i], length))
i += 5 + length
return out
def server_name(client_hello: bytes) -> str:
"""Walk the plaintext ClientHello to the server_name extension (type 0)."""
i = 5 + 4 + 2 + 32 # record header, handshake header, version, random
i += 1 + client_hello[i] # session id
i += 2 + int.from_bytes(client_hello[i : i + 2], "big") # cipher suites
i += 1 + client_hello[i] # compression methods
end = i + 2 + int.from_bytes(client_hello[i : i + 2], "big")
i += 2
while i < end:
kind, size = int.from_bytes(client_hello[i : i + 2], "big"), int.from_bytes(client_hello[i + 2 : i + 4], "big")
if kind == 0: # list length (2), name type (1), name length (2), name
return client_hello[i + 9 : i + 9 + int.from_bytes(client_hello[i + 7 : i + 9], "big")].decode()
i += 4 + size
return "(none)"
python observer.py (node observer.mjs prints the same, with lower-case true/false)
TLS 1.3 GET /dental
server name (SNI), in the clear: clinic.example.in
CA name readable on the wire: False
request record: 41 bytes
response record: 1,217 bytes, for a 1,200-byte page
TLS 1.3 GET /hiv-test
server name (SNI), in the clear: clinic.example.in
CA name readable on the wire: False
request record: 43 bytes
response record: 5,417 bytes, for a 5,400-byte page
TLS 1.2 GET /dental
server name (SNI), in the clear: clinic.example.in
CA name readable on the wire: True
request record: 48 bytes
response record: 1,224 bytes, for a 1,200-byte page
TLS 1.2 GET /hiv-test
server name (SNI), in the clear: clinic.example.in
CA name readable on the wire: True
request record: 50 bytes
response record: 5,424 bytes, for a 5,400-byte page

Read the numbers. The response is exactly the page plus 17 bytes in TLS 1.3 and plus 24 in TLS 1.2 (an 8-byte explicit nonce and the tag). Even the request gives away its path length: /hiv-test is two characters longer than /dental, and its record is two bytes longer. TLS 1.2 also showed the observer the whole certificate chain; TLS 1.3 encrypts it.

Real pages are many requests with sizes that vary a little per visit, and that turns out not to help much. In 2014, researchers identified individual pages within ten large HTTPS sites, including health and finance sites, with about 89% accuracy from traffic shape alone; their best defence cut it to 27% for 9% more traffic (Miller et al., PETS 2014). Here is the mechanism on a toy site with twelve pages. The observer measured each page’s typical size in advance and, for every visit, names the page whose size is closest to what it saw:

SimulationNaming encrypted pages by their size

What to watch for. With no padding and 3% variation per visit, the observer names the page about 8 times in 10, against 1 in 12 for a blind guess, without decrypting a byte. Pad to 16 KB buckets and it falls to about 1 in 4, at the cost of about a quarter more bytes. Set the variation to 0 and size alone names every visit.

With the defaults the observer names the page in about 82% of visits, against 8% for a blind guess. Padding every response up to a multiple of 16 KB drops it to 27%, at the cost of 23% more bytes; 32 KB buckets get to 21% for 52% more. Padding works by making pages share a size, so it only helps when the buckets are wider than the gaps between real page sizes, and you pay for it in bandwidth on every response. TLS 1.3 records can carry padding, but no common server adds it for you: it is an application decision.

A full handshake costs one round trip (one RTT) before the first request can leave. On a resumed connection TLS 1.3 can make that zero, hence 0-RTT: the client already holds a secret from its ticket, so it sends its first request inside the very first flight, as early data, encrypted with a key derived from that secret. A user 200 ms away gets the answer 200 ms sooner. The price follows from the table above: a normal handshake is protected against replay because the server’s fresh random value enters the keys. Early data is sent before the server has said anything, so its keys depend only on the client’s flight. Anyone can copy that flight and send it again on a new connection, and it decrypts correctly every time.

The specification says plainly that TLS gives early data no protection against replay between connections, and lists what servers can do (RFC 8446, section 8): use each ticket once, or remember every ClientHello seen recently, plus a freshness check that rejects a ClientHello whose ticket age is too far from what the server expects, on the order of ten seconds. Both memories have to live somewhere, and in a cluster that is the whole problem. Watch an attacker copy every POST /transfer four times and send the copies through the same load balancer:

SimulationReplaying 0-RTT early data into a cluster

What to watch for. With a replay cache inside each server (defence 1), a copy is caught only when it lands on the server that already saw it, so with 8 servers about 7 copies in 10 still run. Drag servers to 1 and every copy is caught; drag it to 16 and only about 1 in 7 is. A shared cache (defence 2) catches every copy but adds a lookup to every handshake; refusing POST in early data (defence 3) catches every copy and gives up 0-RTT for every POST.

With a replay cache inside each server, a copy is caught only if it reaches the server that saw the original. With 8 servers and 4 copies per request, 570 of 800 copies ran in a five-minute run with the default seed: about 71%, close to the 72% you get from counting how many distinct servers five random picks reach. Per-server memory protects one server, and a cluster of one. A shared cache catches every copy, but now every 0-RTT handshake waits on a lookup in a shared store, and when that store is unreachable the only safe choice is to refuse early data. The cheapest complete answer is the third one: do not accept anything in early data that is unsafe to run twice.

That is what production systems do. As of October 2026, Cloudflare’s 0-RTT is off by default, accepts only GET, HEAD and OPTIONS in early data, and adds an Early-Data: 1 header so the origin knows (Cloudflare docs). An origin that sees that header on a request it cannot replay answers 425 Too Early, and the client resends after the full handshake (RFC 8470). Remember also that early data has no forward secrecy against a stolen STEK, from the previous section.

Chain building, and why “works in my browser” proves nothing

Section titled “Chain building, and why “works in my browser” proves nothing”

A certificate is a short structured record in a format called X.509. The fields that matter here:

  • Subject Alternative Name (SAN): the names it is valid for, such as bank.example.in and *.bank.example.in.
  • Public key: the key whose private half must sign CertificateVerify.
  • Issuer: the CA that signed it.
  • Validity: not before and not after, for example 1 October to 17 November 2026.
  • Serial number: unique per CA, used to revoke it.
  • Extensions that limit the key: CA:TRUE or CA:FALSE (may it sign other certificates?) and the allowed use, such as server authentication.
  • The issuer’s signature over a hash of all the fields above.

The last field makes the certificate a signed statement: the issuer vouches that this key belongs to these names until that date. Add one name and the hash changes, so the signature fails. Checking that signature needs the issuer’s public key, which sits in the issuer’s own certificate, which is signed by its issuer. A root signs its own certificate, so its signature proves nothing; it is trusted only because it is already in the trust store.

A server sends its own certificate (the leaf) and the intermediate CA certificates that signed it. The client must build a path from the leaf to a root already in its trust store, and then check every link.

A chain of trust from a server certificate to a root on your deviceThree certificates stacked. At the top, the leaf for example.com, signed by an intermediate certificate authority. In the middle, the intermediate, signed by a root. At the bottom, the root, which signs itself and sits inside your device's trust store, a list of about 120 roots. The server sends the leaf and the intermediate; the root is already on your device. The client checks each signature on the way up and trusts the leaf only if the chain ends at a root it already holds.example.comleaf, 90 dayssigned byIssuing CAintermediate, yearssigned byRoot CAsigns itselfyour device's trust storeabout 120 rootsserversendsThe client checks each signature upwards.
The server sends the leaf and the intermediate. The root is already on the device. The client checks each signature upwards and trusts the leaf only if the path ends at a root it holds.

For every certificate on the path the client checks the signature against the issuer’s public key, the validity dates against its own clock, that issuers are marked as CAs and the leaf is allowed for server authentication, and finally that the leaf’s Subject Alternative Names include the host it asked for. As of September 2026 Mozilla’s store, which curl and most Linux distributions use, held 121 roots trusted for TLS (curl’s extract).

The classic failure is a server that sends only its leaf. Clients then disagree, because each fills the gap differently:

Client Missing intermediate
Chrome Downloads it from the URL in the leaf’s Authority Information Access field
Firefox Already has it: it preloads every intermediate disclosed to the CA database (Mozilla, 2020)
Any browser that visited another site with the same intermediate Often has it cached
Python, Node, Go, Java, curl Fail: “unable to get local issuer certificate”

So the site opens in the browser, the developer’s API call fails, and the bug is reported against the wrong side. The tls_checks example in Build it yourself reproduces it, and shows that the error for a missing intermediate is identical to the error for an impostor whose CA nobody trusts. That is why turning verification off to “fix” it is so dangerous: it fixes the impostor case too.

Paths can also have more than one answer. CAs cross-sign: an intermediate may be signed by both a new root and an old one, so old devices that lack the new root still find a path. When the old root expires, a client that picks the expired path fails even though a valid one exists. That is what happened to older OpenSSL versions when Let’s Encrypt’s old root expired in September 2021 (Let’s Encrypt). Check chains with the clients you actually ship, not with a browser.

If a private key leaks, the CA can revoke the certificate before it expires. The client then has to find out, and both ways of asking have failed in practice:

  • A CRL (certificate revocation list) is a signed list of revoked serial numbers. Lists get large, and clients download them rarely.
  • OCSP asks the CA about one certificate at connection time. It adds a network round trip, tells the CA every site you visit, and fails often enough that browsers proceed when there is no answer. That soft-fail behaviour is the fatal part:
Step throughOCSP soft-fail against the attacker it was meant to stop
OCSP soft-fail against the attacker it was meant to stopSequence between Browser, Attacker, CA (OCSP), in 4 steps.BrowserAttackerCA (OCSP)1stolen cert2revoked?3drop it4soft-fail
  1. Step 1 of 5stolen cert

    The attacker intercepts your connection and presents the bank's real certificate, signing with the stolen private key. The CA revoked that certificate yesterday. Every signature checks out.

  2. Step 2 of 5revoked?

    The browser asks the CA. The request crosses the same network the attacker controls.

  3. Step 3 of 5drop it

    The attacker drops the OCSP request.

  4. Step 4 of 5soft-fail

    No answer could mean the CA is down, which happens, so the browser carries on as if the certificate were fine.

  5. Step 5 of 5The whole flow

    Soft-fail revocation stops only attackers who cannot block traffic, and an attacker using a stolen key on your path can always block traffic. Hard-fail would turn every CA outage into an outage for every site it serves.

OCSP stapling lets the server fetch a signed OCSP answer and send it in the handshake, fixing privacy and latency, and Must-Staple marks a certificate as unusable without a stapled answer. Few sites used either. Let’s Encrypt, the largest CA, stopped accepting Must-Staple requests in May 2025 and shut its OCSP service down on 6 August 2025, keeping only CRLs (Let’s Encrypt).

Browsers moved the work to themselves. Chrome ships CRLSets, a curated list of revocations pushed with browser updates, aimed at emergencies rather than every revoked certificate. Firefox ships CRLite, a compressed encoding of every revocation of every certificate in the Transparency logs, updated twice a day and checked locally; it is on for desktop users since Firefox 137, and Firefox 142 stopped making OCSP queries for ordinary domain-validated certificates (Mozilla, August 2025). Your back-end code is in a worse position: Python’s ssl, Node’s tls and most HTTP clients check no revocation at all by default.

So for most clients, the only revocation that always works is expiry. A stolen key is usable for the rest of the certificate’s life. With 398-day certificates that was up to 13 months; with 47-day certificates it is under 7 weeks. That is the main reason lifetimes are being cut.

Lifetimes falling to 47 days, and what breaks

Section titled “Lifetimes falling to 47 days, and what breaks”

In April 2025 the CA/Browser Forum, where CAs and browsers set the rules for public certificates, passed a schedule that cuts the maximum lifetime in steps (ballot SC-081v3):

How long a public TLS certificate may liveBars of certificate lifetimes. The industry maximum was 398 days until March 2026, then 200 days, 100 days from March 2027, and 47 days from March 2029. Let's Encrypt issues 90 days by default, plans 45 days by February 2028, and offers 6-day certificates.Maximum (CA/B Forum)Let's Encrypt (LE)until Mar 2026398 daysfrom Mar 2026200 daysfrom Mar 2027100 daysfrom Mar 202947 daysLE default90 daysLE, Feb 202845 daysLE short-lived6 daysas of October 2026
The maximum lifetime of a public certificate falls from 398 days to 47 by March 2029. Let's Encrypt issues 90 days by default and plans 45 by 2028.

The same ballot shortens how long a CA may reuse a past domain validation (proof that you control the name) to 10 days by 2029. Automated renewal runs on ACME (RFC 8555), the protocol certbot and similar clients speak to a CA: the CA hands out a random token, and the client proves control of the name by serving it at an HTTP path, in a DNS record or in a TLS response. In practice, nearly every renewal must now pass a fresh challenge, so that path must work every time, not once a year. As of October 2026, Let’s Encrypt issues 90-day certificates by default, moves to 64 days on 10 February 2027 and 45 days in February 2028, and recommends renewing at about two-thirds of the lifetime, ideally when the CA’s ARI (ACME Renewal Information) endpoint says so (Let’s Encrypt). It also stopped sending expiry reminder emails in June 2025.

Renewing at two-thirds means a third of the lifetime is left when renewal starts. That third is not spare time for the automation. It is the time humans have to notice and fix broken automation. Automation breaks for dull reasons: a firewall change blocks the challenge, a DNS API token expires, a server is rebuilt without the renewal job, the proxy is never reloaded. Here are six certificates over two years, with those breakages:

SimulationAutomated renewal at two-thirds of the lifetime, when the automation breaks

What to watch for. Each red cross is the automation breaking. With 47-day certificates renewed when 16 days are left and an alert at 14 days left, a break shows up as a short amber band and a human fixes it before expiry. Set the alert to 0: the same breaks turn red, because nothing tells a human until users do. Then drag the lifetime to 6 days: the renewal window is 2 days, so a 3-day fix lands after expiry, alert or not.

With the defaults, 47-day certificates, about one breakage per certificate per year, an alert at 14 days left and three days to fix, the run issues 139 certificates, the automation breaks 9 times, and no certificate expires. Each break is a short amber band: renewal overdue, alert, fix, renewed with days to spare. Set the alert to 0 and the same 9 breaks cause 27 days with an expired certificate, because nothing tells a human until users do. Drag the lifetime to 6 days: the window is 2 days, a 3-day fix arrives after expiry, and the run has 12 days of outage with the alert on; it takes a 1-day fix to get back to zero. Short lifetimes shrink the damage of a stolen key, and the price is that humans have to answer faster.

What breaks first when lifetimes fall is not ACME. It is every place a certificate was copied by hand: uploaded to a CDN or an appliance, pasted into a partner’s allow-list, pinned in a mobile app, imported into a Java keystore. At 398 days that was one chore a year per place; at 47 days it is at least eight. Find those places now, and make each one either automated or fetch its certificate from somewhere that is.

In mutual TLS (mTLS) the server adds a CertificateRequest to its flight, and the client answers with its own certificate and its own CertificateVerify signature over the transcript. Same mechanism, both directions: now the server knows which workload is calling, not only that someone holds a token. Inside a service mesh such as Istio or Linkerd, a proxy next to each workload does this for you, with an identity written as a URI in the certificate, by the SPIFFE convention: spiffe://cluster.local/ns/shop/sa/orders. As of October 2026, Istio issues workload certificates for 24 hours by default, from a key generated inside the pod (Istio security).

Four ways a mesh’s mTLS is weaker than it looks:

  1. Authenticated is not authorized. A valid certificate says who is calling. “Any workload with a certificate from our CA may call anything” is a policy, and usually the wrong one. The mtls example in Build it yourself refuses a perfectly authenticated caller.
  2. Permissive mode accepts plaintext. Istio’s PERMISSIVE mode, the default, accepts both mTLS and plain text so services can migrate one at a time. Until a namespace is STRICT, an attacker inside the network can skip mTLS entirely by speaking plain HTTP.
  3. The application trusts a header. The sidecar ends mTLS and talks plain HTTP to the app on localhost, passing the caller’s identity in a header such as X-Forwarded-Client-Cert. If the app’s port is reachable around the sidecar, anyone can send that header. Envoy strips a client-supplied one by default (Envoy docs); make sure the app listens only on localhost.
  4. The mesh CA becomes a hard dependency. With 24-hour certificates, a CA outage longer than the remaining lifetime is an outage of every service that cannot renew, at once. Monitor the CA like the database, not like a tool.

A request may cross a CDN, a load balancer, an API gateway and a sidecar before reaching your code, and each can decrypt it. Decrypting at a proxy is TLS termination, and where it happens decides who can read your traffic.

Where TLS ends inside a systemFour rows, each a client, a load balancer and an app. Terminate at the edge: TLS from the client to the balancer, then plain HTTP to the app, readable by anyone inside. Re-encrypt: TLS to the balancer, then a new TLS connection to the app. Passthrough: one TLS connection from the client straight to the app; the balancer only forwards encrypted bytes. Mesh with mutual TLS: TLS to the edge, then mutual TLS between services, where both sides present certificates.Terminate at the edgeclientbalancerappTLSplainsimple; anyone inside can read itRe-encryptclientbalancerappTLSTLSthe balancer can read and routePassthroughclientbalancerappTLSsame TLSbalancer only forwards bytesMesh with mutual TLSclientbalancerappTLSmTLSencrypted, and both ends identified
TLS can end at the edge, be re-encrypted to the service, pass through untouched, or be replaced by mutual TLS between services. Only the first leaves plain text inside your network.
Edge only Re-encrypt Passthrough Mesh mTLS
Plain text inside the network yes only inside the proxy no no
Proxy can route on path and headers yes yes no, only on SNI yes
Private keys live on the edge edge and services services only edge, plus a short-lived key per workload
Callers identified by certificate no no no yes
Typical use legacy most web apps behind a managed balancer databases, end-to-end rules many services, zero-trust networks

“Re-encrypt” only helps if the proxy verifies the service’s certificate; otherwise it is verify=False on the back half. CDNs make this choice explicit. Cloudflare’s Flexible mode is HTTPS to the visitor and plain HTTP to your origin; Full encrypts to the origin without checking its certificate; only Full (strict) verifies it (Cloudflare SSL modes). A Flexible-mode site whose origin redirects HTTP to HTTPS loops until the browser gives up, and teams “fix” that by removing the redirect.

Cloud providers encrypt some links for you; AWS encrypts traffic between its zones and regions and, on some instance types, between instances. Its documentation still leaves TLS to you (AWS EC2 data protection), because a cable that is encrypted does not stop a compromised machine on the same network, and it identifies nobody.

Two programs that run against a private CA built by make-certs.sh: a root, an intermediate, a good server certificate, an expired one, one for the wrong name, an impostor’s from a CA nobody trusts, and two client certificates. All files are in examples/tls/.

Step 1: what a client checks, and what verify=False removes

Section titled “Step 1: what a client checks, and what verify=False removes”

A local server presents each certificate in turn. The client trusts only our root and sends a password, first with the default checks, then with the “fix” people paste when they see CERTIFICATE_VERIFY_FAILED:

tls_checks.py (excerpt)
# runnable version: examples/tls/python/tls_checks.py
def main() -> None:
# The right client: default checks on, trusting only the CA we chose.
strict = ssl.create_default_context(cafile=CERTS / "root.pem")
print("Client with verification on (the default):")
for label, chain, key in CASES:
try:
result = connect(serve_once(chain, key), strict)
except ssl.SSLCertVerificationError as err:
result = f"REFUSED {err.verify_message}"
print(f" {label:<27} {result}")
# The "fix" people paste from the internet when they see CERTIFICATE_VERIFY_FAILED.
lax = ssl.create_default_context()
lax.check_hostname = False
lax.verify_mode = ssl.CERT_NONE
print("\nClient with verify=False (CERT_NONE, no hostname check):")
for label, chain, key in CASES:
print(f" {label:<27} {connect(serve_once(chain, key), lax)}")
python tls_checks.py
Client with verification on (the default):
good: leaf + intermediate OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'
missing intermediate REFUSED unable to get local issuer certificate
expired REFUSED certificate has expired
wrong name REFUSED Hostname mismatch, certificate is not valid for 'localhost'.
impostor's own CA REFUSED unable to get local issuer certificate
Client with verify=False (CERT_NONE, no hostname check):
good: leaf + intermediate OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'
missing intermediate OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'
expired OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'
wrong name OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'
impostor's own CA OK TLSv1.3 TLS_AES_256_GCM_SHA384 -> 'server got: password=hunter2'

Read the last line. With verification off, the impostor received the password over TLS 1.3 with AES-256: a modern protocol and a strong cipher, encrypting all the way to the attacker. That is Mallory’s attack again, with real TLS. Node prints the same outcomes with its own error codes (UNABLE_TO_VERIFY_LEAF_SIGNATURE for both the missing intermediate and the impostor). When you see that error, run openssl s_client -connect host:443 -servername host -showcerts and count the certificates the server sends.

Step 2: mutual TLS with authorization by identity

Section titled “Step 2: mutual TLS with authorization by identity”

A payments service that requires a client certificate, reads the caller’s SPIFFE ID from it, and checks what that caller may do:

mtls.py (excerpt)
# runnable version: examples/tls/python/mtls.py
ALLOWED = {
"spiffe://shop.internal/orders": {"POST /charge"},
"spiffe://shop.internal/reports": {"GET /summary"},
}
def caller_identity(cert: dict) -> str | None:
"""The URI in the certificate's Subject Alternative Name, e.g. spiffe://shop.internal/orders."""
return next((value for kind, value in cert.get("subjectAltName", ()) if kind == "URI"), None)
def payments_server() -> int:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(CERTS / "server-fullchain.pem", CERTS / "server.key")
context.verify_mode = ssl.CERT_REQUIRED # this one line is what makes it *mutual* TLS
context.load_verify_locations(CERTS / "root.pem") # client certs must chain to our root
python mtls.py (node mtls.mjs: the first line reads ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED)
no cert POST /charge -> refused: TLSV13_ALERT_CERTIFICATE_REQUIRED
orders POST /charge -> 200 OK for spiffe://shop.internal/orders
reports GET /summary -> 200 OK for spiffe://shop.internal/reports
reports POST /charge -> 403 Forbidden for spiffe://shop.internal/reports

Without a certificate the handshake fails, and the client only learns it on its first read: in TLS 1.3 the client finishes its side before the server has checked the client’s certificate, so a connection that “succeeded” can still be refused one step later. The last line is the one that matters: reports holds a valid certificate and is still refused a charge.

An expired certificate on a monitoring or security tool pages nobody, because nothing user-facing breaks. It fails silently and open. Put internal tooling on the same served-certificate monitor as the public site.

  • Renewed on disk, old one in memory. ACME clients write files; nginx, HAProxy, Java services and many SDKs read certificates only at startup. Reload after renewal, and check what is served.
  • verify=False in a codebase is an incident, not a code smell. It almost always arrived as a fix for a missing intermediate or a corporate proxy root. Fix the chain, or add that one root to that one client’s trust bundle.
  • Containers carry their own trust stores. A slim image without CA certificates, or with a stale bundle, fails every outbound call after a CA changes its chain. Treat the CA bundle as a dependency you update.
  • Infrastructure code picks old policies. As of October 2026, an AWS Application Load Balancer created in the console defaults to a TLS 1.2 and 1.3 policy with post-quantum key exchange, while one created through the CLI or CloudFormation without a policy still gets ELBSecurityPolicy-2016-08, which allows TLS 1.0 (AWS docs). Always name the policy.
  • Identical expiry everywhere is one point of failure. The Azure outage above hit every region at once because every region held the same certificate. Stagger renewals.

What TLS costs. One round trip for a new connection in TLS 1.3 and little CPU: when Gmail moved everyone to HTTPS in 2010 it cost under 1% of front-end CPU (Langley, Modadugu and Chang). The real cost is operations: keys to protect, certificates to renew, deploy and monitor, ticket keys to rotate, and a class of outage, expiry, that plain text never has.

What it does not solve. The endpoints: after decryption, a compromised machine reads everything. Data at rest. Authorization. Trustworthiness: phishing sites have valid certificates. Metadata: addresses, timing and sizes, and the site name unless ECH applies.

When not to reach for it alone. Inside one machine over a Unix socket it adds little. To protect a whole network link, including protocols that cannot speak TLS, a WireGuard or IPsec tunnel may be the better layer, alongside TLS rather than instead of it. Never build your own replacement: every broken scheme on this page was once someone’s careful design.

  1. “How does the client know it is talking to the real server?” Chain to a trusted root, name match, dates, and above all the CertificateVerify signature over this handshake’s transcript, which includes both key shares. Follow-up: “So can a man in the middle never win?” Yes, with a certificate from a CA the client trusts: a compromised CA or an installed corporate root. Certificate Transparency makes the first visible.
  2. “What does forward secrecy protect, and what breaks it?” Recorded traffic, against later theft of the certificate key, thanks to ephemeral key exchange. Broken by RSA key exchange, by long-lived session ticket keys in TLS 1.2, and for 0-RTT data by ticket keys in TLS 1.3.
  3. “Would you turn on 0-RTT for this API?” Only for requests safe to run twice, because early data can be replayed on new connections, and per-server anti-replay does not protect a cluster. Mention 425 Too Early and idempotency keys.
  4. “Where would you terminate TLS?” Edge versus re-encrypt versus passthrough versus mesh mTLS, trading the ability to route on paths against plain text inside, and who renews which certificates.
1. Mallory relays your ClientHello to the bank unchanged and relays the bank’s whole reply back to you unchanged. What can she read?

Nothing. The key was agreed between your share and the bank’s share, and she knows neither private value. To read anything she must substitute a share, and then the bank’s CertificateVerify covers a transcript that does not match yours.

2. An attacker records traffic to a TLS 1.2 server that uses ECDHE and session tickets, and a year later steals the ticket key the server has used since it booted. What can they decrypt?

Every recorded connection whose session ticket they captured, including the full handshake that issued the ticket, because a TLS 1.2 ticket contains the master secret encrypted under that key. Ephemeral key exchange protected against theft of the certificate key, not the ticket key. Rotate ticket keys often.

3. Your Python service fails with “unable to get local issuer certificate” calling a partner API that opens fine in Chrome. What is the likely cause, and why is verify=False the wrong fix?

The partner’s server sends only its leaf; Chrome fetches the intermediate through AIA or has it cached, Python does not. Ask them to send the full chain, or add their intermediate to your trust bundle for that host meanwhile. The same error appears for an impostor’s certificate, so turning verification off accepts the impostor too.

4. You run 12 servers with a ClientHello replay cache in each, and accept POST in 0-RTT. An attacker sends 3 copies of a captured transfer. Roughly how many run?

The original plus three copies land on random servers. A copy is rejected only if it lands on a server that already saw that ClientHello. With 12 servers, most copies land somewhere new: four random picks reach about 3.5 distinct servers, so about 2.5 of the 3 copies run. The fix is a shared cache or, better, no POST in early data.

5. Your renewal automation breaks and is fixed within 5 days of an alert. With 47-day certificates renewed at two-thirds of their lifetime, what is the latest an alert can fire and still avoid an outage? What changes at 6 days?

Renewal starts with about 16 days left, so the alert must fire with more than 5 days left; 14 days leaves plenty of slack. With 6-day certificates renewal starts with 2 days left, so a 5-day fix cannot avoid expiry: the team must fix renewals within hours, or the lifetime is too short for them.

  • The server proves itself with CertificateVerify, a signature over the handshake transcript including both key shares. The certificate alone proves nothing; anyone can send it.
  • TLS authenticates whatever key a trusted CA vouched for: compromised CAs and installed roots defeat it, and Certificate Transparency makes the first visible.
  • Forward secrecy comes from ephemeral key exchange and is undone by RSA key exchange and by long-lived session ticket keys. Rotate them.
  • Encrypted traffic still shows addresses, the site name (without ECH), sizes and timing; sizes alone can name pages unless responses are padded.
  • 0-RTT early data can be replayed on new connections; per-server caches do not protect a cluster. Accept only requests safe to repeat.
  • Revocation mostly does not work; short lifetimes are the real fix, falling to 47 days by 2029. The renewal window is your human response time. Automate everything, alert on overdue renewals, monitor what is served.
  • Know where TLS ends. Re-encrypt with verification, or use mTLS, and remember that mTLS authenticates; your code still authorizes.
  • HTTP/1.1 → HTTP/2 → HTTP/3coming soon

    How HTTP/2 and HTTP/3 use the connection TLS sets up, and why QUIC builds TLS 1.3 into the transport.

    Networking & APIs
  • Reverse proxiescoming soon

    Where termination happens in practice, and how to configure re-encryption with verification.

    Traffic & edge
  • Reverse Proxy vs Load Balancer vs API Gateway

    Which boxes in front of your services decrypt traffic, and what each can read and decide.

    Traffic & edge
  • CDNs and edge cachingcoming soon

    CDNs terminate TLS near users; how the edge-to-origin leg should be protected.

    Traffic & edge
  • Encryption at rest and key managementcoming soon

    The other half: protecting data once it arrives, and the key management behind it.

    Security
  • Secrets managementcoming soon

    Private keys and ticket keys are secrets: where to keep them and how to rotate them.

    Security
  • JWTs and key rotationcoming soon

    The same signature and rotation problems, applied to tokens.

    Security

Specs & official docs

Engineering blogs, papers & incidents

Explainers

Edit page

Last updated:

Senior Dev Notes

System design, explained like a mentor would.

Written by Uday. Every page is researched, reviewed by a human, and every code example was run before it was published.