Crux Read real openssl s_client output, an Nginx ticket-key config, an Early-Data handler, and a key_share trace, then pick the diagnosis and highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
TLS problems are diagnosed in handshake traces, server configs, and request headers — not in textbook diagrams. Read each artifact, predict the behaviour, and choose the fix a senior engineer would make first.
Goal
Practise the loop you run in every TLS incident: read the openssl output or config, locate which handshake invariant is violated, and reach for the highest-leverage fix.
Snippet 1 — openssl s_client chain verification
% openssl s_client -connect api.example.com:443 -tls1_3depth=0 CN = api.example.comverify error:num=20:unable to get local issuer certificateverify error:num=21:unable to verify the first certificate---Certificate chain 0 s:CN = api.example.com i:CN = R3 Issuing CA---New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384Verify return code: 21 (unable to verify the first certificate)
Quiz
Completed
The handshake completes and a cipher is negotiated, yet verify return code is 21. What is misconfigured?
Heads-up Expiry produces error 10 (certificate has expired), not 20/21. Error 20/21 means the issuer chain is incomplete — the intermediate is missing from what the server sent.
Heads-up It is a standard TLS 1.3 suite and the handshake negotiated it successfully. The keys derived fine; the failure is purely chain-building, a separate step from the key exchange.
Heads-up openssl s_client sends SNI by default and the returned CN matches the host. The chain is for the right host — it is simply missing the intermediate the client needs to reach a root.
Snippet 2 — Nginx session-ticket key config
server { listen 443 ssl; ssl_protocols TLSv1.3; ssl_session_ticket_key /etc/nginx/ticket.key; # generated once at install ssl_early_data on; # ...}
Quiz
Completed
This config has been running unchanged for a year. What is the most serious security defect, given ssl_early_data is on?
Heads-up 0-RTT early data is acceptable for idempotent routes with replay defenses. The deeper defect here is the never-rotated, disk-persisted STEK, which undermines resumption forward secrecy regardless of early data.
Heads-up Restricting to TLS 1.3 is a hardening choice, not a security defect. The security problem is the static, disk-stored ticket key.
Heads-up The path being absolute is irrelevant to TLS security. The real problem is that the key is static and on disk — STEKs must rotate and stay in memory.
Snippet 3 — an Early-Data request handler
app.post("/transfer", (req, res) => { // money movement if (req.headers["early-data"] === "1") { // request arrived in TLS 0-RTT early data } doTransfer(req.body); res.send("ok");});
Quiz
Completed
The Early-Data header is read but the handler still calls doTransfer regardless. What goes wrong under a replay, and what is the correct fix?
Heads-up Reading the header changes nothing on its own; the code still executes the mutation. You must reject with 425 Too Early on mutating routes when Early-Data is 1, forcing a post-handshake retry.
Heads-up Early data carries the full request including the body; it is processed normally. The danger is precisely that it processes — and can be replayed to process twice.
Heads-up TLS delivers early data to the application; it does not know your route is non-idempotent. Enforcing idempotency is an application-layer responsibility via 425 Too Early.
The client offered x25519 and the hybrid X25519MLKEM768, yet the server sent HelloRetryRequest asking for secp256r1. What happened and what is the cost?
Heads-up HRR is purely about key_share group negotiation, not authentication. It is sent when none of the client's offered groups match what the server will use.
Heads-up X25519MLKEM768 is a valid hybrid named group; the server simply does not support it. The handshake continues via HRR with a group both sides accept, costing one RTT, not aborting.
Heads-up HRR stays within TLS 1.3 and only renegotiates the key_share group. It does not change the protocol version; downgrade attempts would break the Finished transcript check.
Recap
Every TLS incident is read in artifacts: an openssl verify code 20/21 means an incomplete chain — send the intermediates; a static disk-stored ssl_session_ticket_key destroys resumption forward secrecy — rotate STEKs hourly in memory; an Early-Data handler that mutates without returning 425 Too Early is replayable — gate mutating routes on the Early-Data header; and a HelloRetryRequest naming a different group means your offered key_shares missed — offer x25519 plus P-256 up front to avoid the extra RTT. Read the artifact, find the violated invariant, fix the cause.