DSDIGITAL SENTRY
Back to Blog
NetworkingMay 9, 202410 min read

The TLS Handshake in 2025 and Beyond: What the Defaults Are, What's Still a Footgun, and What to Check

TLS 1.3 is the default now and most of the footguns from the TLS 1.2 era are gone, but a few remain: certificate validation, downgrade attacks, and the long tail of services still on 1.0 and 1.1. What the protocol actually does, what the operational defaults are, and the small set of checks that catch most of the remaining problems.

Overview

TLS is the protocol that encrypts the internet. Every HTTPS connection, every API call, every email sent over SMTP with STARTTLS, every database connection that uses TLS, runs over a TLS handshake. The protocol has been through three major versions in widespread use today: TLS 1.2 (RFC 5246, 2008), TLS 1.3 (RFC 8446, 2018), and the older TLS 1.0 and 1.1 (RFC 2246 and 4346, 1999 and 2006) which are formally deprecated by IETF as of RFC 8996 in 2021.

Most of the operational pain around TLS is from the older versions. The cipher suite negotiation in TLS 1.2 was a combinatorial mess (the client and server had to agree on a key exchange, a cipher, a MAC algorithm, and a hash, with dozens of combinations, many of them weak). TLS 1.3 replaced that negotiation with a much smaller set of AEAD ciphers (AES-GCM, ChaCha20-Poly1305) and a single key exchange (the finite-field or x25519 Diffie-Hellman, optionally signed with ECDSA or Ed25519). The result is a handshake that is both faster and harder to misconfigure.

The footguns that remain in 2025 are: certificate validation, downgrade attacks (rare but real), and the long tail of services that have not been updated to TLS 1.3 or that still support older ciphers for backward compatibility. The TLS 1.3 handshake is roughly 300 lines of code, the TLS 1.2 handshake is closer to 1,500, and the difference is mostly removed attack surface.

How it works

A TLS 1.3 handshake has one round trip. The client sends a ClientHello: the version it supports, a random nonce, a session ID, the cipher suites it supports (TLS 1.3 limits this to a small set of AEAD ciphers), the key-share it has generated, and the server name indication (SNI) for the host it is trying to reach. The server responds with a ServerHello: a selected version, a selected cipher, a session ID, its own key share, its certificate, a CertificateVerify signature proving it has the private key for the certificate, and a Finished message that authenticates the entire handshake. The client verifies the certificate chain, verifies the CertificateVerify signature, and sends its own Finished. From there, both sides have derived the same session keys, and the encrypted application data flows.

The key exchange is the interesting part. The client generates a fresh Diffie-Hellman key pair and sends the public half in the ClientHello. The server does the same and sends its half in the ServerHello. Each side combines its own private key with the other side's public key and gets the same shared secret, which neither side has transmitted. This is forward secrecy: even if the server's long-term private key is later compromised, an attacker who recorded the handshake cannot decrypt the session because the Diffie-Hellman keys were ephemeral and not derived from the long-term key.

Certificate validation is the part where most of the operational problems are. The client receives a server certificate and must verify three things: that the certificate was issued by a Certificate Authority the client trusts, that the certificate was issued for the hostname the client is trying to reach (the SNI), and that the certificate was not revoked. The chain of trust is anchored in a small set of root CAs that the client trusts implicitly (the operating system or browser root store). A certificate that does not chain to a trusted root, or that was issued for a different hostname, is rejected.

In practice

The default TLS configuration on a modern web server is TLS 1.3 with TLS 1.2 fallback, AES-128-GCM and AES-256-GCM as the cipher suites, and the X.509 certificate chain anchored in a public CA (Let's Encrypt, DigiCert, Sectigo, or one of the other large public CAs). The client side is whatever the user's browser or application supports, which on any modern client is TLS 1.3. This is the boring case, and it works.

The interesting cases are: services that are pinned to old TLS versions for backward compatibility (often internal services, SCADA systems, medical devices, or industrial control systems that have not been updated since the early 2010s), services that use self-signed certificates (every internal PKI, every Kubernetes cluster, every dev environment), and services that use certificate pinning (mobile apps that ship a known-good certificate for the API endpoint to prevent MITM). Each of these is a place where the default validation is overridden, and each is a place where a misconfiguration can be hard to detect.

Certificate transparency is the public-record mechanism that catches misissuance. Every public CA is required to log every certificate it issues to a public, append-only, cryptographically-verifiable log. A monitor (a CT log watcher) can detect when a certificate is issued for a domain the organization owns and was not requested by the organization. The major CT monitors (Facebook's CT monitor, Google's pilot, Let's Encrypt's Oak, Sectigo's crt.sh) are public and free. Running a CT monitor for your domains is a no-cost, low-effort control that catches misissuance before it can be exploited.

Common mistakes

Disabling certificate validation for testing and forgetting to re-enable it. The classic is an environment variable like NODE_TLS_REJECT_UNAUTHORIZED=0 or a curl flag like -k that gets committed to a script and runs in production. The fix is a lint rule in CI that fails the build if it finds a string like 'rejectUnauthorized: false' or 'verify=False' in the source tree.

Using self-signed certificates in production because the renewal process is too painful. Self-signed certificates are appropriate for internal PKI and for services that have a separate trust bootstrap (like certificate pinning). They are not appropriate for public-facing services because the user has no way to know whether the certificate is the real one or a MITM. The operational fix is to automate the renewal with Let's Encrypt or an equivalent; the manual renewal is the pain, not the certificate.

Allowing old TLS versions and old ciphers for backward compatibility. A service that still allows TLS 1.0 is exposing its users to BEAST and POODLE-class attacks. A service that allows RC4 is exposing its users to biases in the cipher that have been exploited in practice. The operational fix is to know what your real client base supports, drop support for everything below TLS 1.2, and prefer TLS 1.3-only if you can.

Confusing certificate pinning with certificate authentication. Pinning is a strong control for a specific service (the mobile app knows the API endpoint's certificate will be this specific certificate, and refuses to connect to anything else), but it has an operational cost: the certificate has to be rotated, and a pin that does not roll over is a self-inflicted outage. The right operational model is to pin the issuing CA, not the leaf certificate, and to have a backup pin in case the rotation is botched.

Defensive guidance

Configure servers to prefer TLS 1.3 with TLS 1.2 fallback. Disable TLS 1.0 and 1.1. Use a small set of AEAD cipher suites (AES-GCM, ChaCha20-Poly1305) and a single key-exchange group (X25519 is the modern default; P-256 is the fallback). Most modern server software (nginx 1.27+, Apache 2.4.58+, IIS 10+, Caddy 2+) ships with a sensible default; the right move is to use that default and not hand-craft a cipher list that you cannot defend.

Automate certificate renewal. Let's Encrypt certificates expire after 90 days by design; the operational answer is to automate the renewal (certbot with systemd, acme.sh with a cron, or the cloud-provider equivalent). The operational answer to "I forgot to renew the certificate" is to make renewal automatic. The same applies to internal PKI.

Run a certificate transparency monitor for your domains. The major CT log watchers are free and public. A certificate issued for your domain that you did not request is a meaningful security event, and the CT log will know about it before the certificate is used in an attack.

Use HSTS (HTTP Strict Transport Security) on public-facing services. HSTS is a header that tells the client to refuse to connect over plaintext HTTP, even if the user types the URL without the https:// prefix. It is a one-line configuration on the server and a one-time operational change on the client (the browser remembers the HSTS directive for the duration you specify). HSTS closes the SSL stripping attack class. Preload HSTS for high-value domains (the browser vendors maintain a preload list).

For internal services, treat the certificate as an identity assertion, not a network control. A service that authenticates itself with mTLS, with a CA that you control and that is trusted only by your infrastructure, gives you a strong identity assertion that does not depend on DNS or on the network path. Internal mTLS is the right default for any new internal service; HTTP basic auth, bearer tokens, and IP allowlists are weaker.

Have a question about security, tech, or my articles?

Ask Hermes, my AI assistant.

Chat with Hermes

Related articles