Transport Layer Security (TLS)
TLS 1.2 vs 1.3 handshake, cert chains, mTLS, the cost of a TLS terminator, why HTTPS is non-negotiable.
What it is#
Transport Layer Security (TLS) is the protocol that gives HTTP its s. It runs between TCP and the application layer; it gives the application three properties on the wire:
- Confidentiality — bytes between client and server are encrypted; a passive observer on the network sees ciphertext, not plaintext.
- Integrity — bytes are authenticated; an active attacker who modifies a byte in flight is detected.
- Server authentication — the client verifies it is talking to the server it intended, via a chain of trust rooted in a Certificate Authority.
Optionally, with mutual TLS (mTLS), the server also verifies the client — a second certificate, presented by the client, proves the client’s identity. This is the dominant pattern in zero-trust service meshes and high-stakes B2B APIs.
TLS is defined by RFC 8446 (TLS 1.3, the current version) and RFC 5246 (TLS 1.2, still widely deployed as a fallback). TLS 1.0 and 1.1 are deprecated and disabled in modern browsers and clients. SSL 3.0 and earlier are obsolete; the “SSL certificate” terminology persists but the underlying protocol is TLS.
When to use it#
Use TLS for:
- Every public-Internet API endpoint, without exception. HTTP-without-TLS is deprecated. Browsers mark it as “Not Secure”; HSTS preloading prevents browsers from ever talking plain HTTP to opted-in domains; major cloud platforms refuse to serve plaintext HTTP on production deployments.
- Every internal API call, unless you have an explicit threat model that excludes the local network. Even an “internal” data centre is one misconfigured switch from being on someone else’s network.
- Webhook delivery in both directions. Outgoing webhooks should require
https://callback URLs; incoming webhooks should rejecthttp://. - mTLS for service-to-service. Zero-trust service meshes (Istio, Linkerd, Consul Connect) assume the network is hostile and authenticate every call with mTLS. This is the standard pattern in 2026.
You do not skip TLS for:
- “It’s internal, the network is private.” It isn’t. The 2013 industry shift to encrypted internal traffic (post-Snowden) settled this.
- “Performance overhead.” TLS 1.3 has a 1-RTT handshake (0-RTT with session resumption). Symmetric encryption on modern CPUs is essentially free — AES-NI hardware acceleration is on every server-class CPU since 2010.
- “We terminate at the load balancer.” That covers the wire from caller to LB. The wire from LB to backend still needs TLS unless you can prove the network between them is trustworthy. Usually you cannot.
How it works#
TLS sits between TCP and the application:
Application (HTTP, gRPC, WebSocket, ...) │ ▼ TLS record protocol ← encryption + MAC │ ▼ TCP │ ▼ IPEach TLS “record” is encrypted and MAC’d before going to TCP; on the way up, each record is verified and decrypted before going to the application. The application sees a normal byte stream; TCP sees opaque ciphertext.
The TLS 1.3 handshake (1-RTT)#
TLS 1.3 cut the handshake from 2-RTT (TLS 1.2) to 1-RTT in the steady state, and 0-RTT with session resumption. The full handshake:
Client Server │ │ │ 1. ClientHello │ │ - supported_versions: [TLS 1.3] │ │ - cipher_suites: [TLS_AES_128_GCM_SHA256, ...] │ - key_share: client's ephemeral pubkey │ │ - server_name: api.example.com (SNI) │ │───────────────────────────────────────────►│ │ │ │ 2. ServerHello │ │ - selected_version: TLS 1.3 │ │ - selected_cipher_suite │ │ - key_share: server's ephemeral pubkey │ │ │ │ 3. {Certificate} ← encrypted from here │ - server's X.509 cert + chain │ │ │ │ 4. {CertificateVerify} │ │ - signature over handshake (proves the server holds the private key) │ │ │ 5. {Finished} │ │◄───────────────────────────────────────────│ │ │ │ 6. {Finished} │ │───────────────────────────────────────────►│ │ │ │ 7. HTTP request (already encrypted) │ │───────────────────────────────────────────►│ │ 8. HTTP response │ │◄───────────────────────────────────────────│Key shift from TLS 1.2: the cipher-suite list is much shorter (RC4, 3DES, CBC modes removed), the key-exchange is always (EC)DHE (forward secrecy mandatory), and the handshake itself is encrypted starting at step 3. RSA key-exchange is gone — every TLS 1.3 connection uses ephemeral keys, so a compromised long-term key cannot retroactively decrypt past sessions.
TLS 1.2 handshake (2-RTT, still common)#
TLS 1.2 is still widely deployed because many clients (older Android, legacy enterprise software) do not speak 1.3. Its handshake is similar but slower:
Client Server │ ClientHello │ │───────────────────────────────────────────►│ │ ServerHello │ │ Certificate │ │ ServerKeyExchange │ │ ServerHelloDone │ │◄───────────────────────────────────────────│ │ ClientKeyExchange │ │ ChangeCipherSpec │ │ Finished │ │───────────────────────────────────────────►│ │ ChangeCipherSpec │ │ Finished │ │◄───────────────────────────────────────────│ │ HTTP request │ │───────────────────────────────────────────►│Two round trips before the first byte of HTTP. On a mobile connection with 150ms RTT, that’s an extra 150ms of latency vs TLS 1.3 per cold connection — the kind of thing that adds up at scale.
Certificate chains#
A TLS certificate is signed by an issuer; that issuer is signed by another issuer; eventually the chain ends at a root certificate that the client trusts a priori. Three levels are typical:
Root CA (e.g. ISRG Root X1) │ trusted by the OS / browser │ self-signed ▼ Intermediate CA (e.g. Let's Encrypt R3) │ signed by the root │ what actually signs leaf certs day-to-day ▼ Leaf certificate (api.example.com) │ signed by an intermediate │ contains the server's public key + SAN listThe server sends the leaf and intermediate certificates in the handshake; the client already trusts the root, so it can verify the chain. The two foot-guns:
- Forgotten intermediate. The server only sends the leaf; the client cannot build a chain to a known root; the connection fails with “unable to verify” errors. Almost every “TLS works in my browser but not in curl” report is a missing intermediate.
- Expired certificate. Certificates have an expiry date (90 days for Let’s Encrypt, 1 year for paid CAs). Auto-renewal is mandatory; calendar-driven renewal is a known failure mode (the on-call who renewed it last year is on holiday this year).
The CA ecosystem: Let’s Encrypt is the dominant CA in 2026 for public-Internet certificates, free, 90-day validity, ACME-protocol automation. DigiCert / Sectigo / GoDaddy issue paid certificates with longer validity and EV (extended validation) options. Internal CAs (HashiCorp Vault PKI, AWS ACM Private CA, cert-manager) issue short-lived certificates inside a service mesh.
Cipher suites and forward secrecy#
A cipher suite names the algorithms used in a TLS session. In TLS 1.3 the format is simpler (only 5 suites are defined); in TLS 1.2 it is TLS_<key-exchange>_WITH_<cipher>_<mac> — for example TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.
Picks for 2026:
- TLS 1.3 default:
TLS_AES_128_GCM_SHA256(fast) orTLS_CHACHA20_POLY1305_SHA256(better on CPUs without AES-NI, common on mobile). - TLS 1.2 fallback:
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256orTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256. ECDHE for forward secrecy; AES-GCM for AEAD; no CBC, no RC4, no 3DES.
Forward secrecy (sometimes “perfect forward secrecy”, PFS) means each session uses an ephemeral key that is discarded when the session ends. An attacker who later steals the server’s long-term private key cannot decrypt past sessions. TLS 1.3 makes forward secrecy mandatory; TLS 1.2 has it whenever ECDHE is used (always pick ECDHE over RSA key-exchange in TLS 1.2).
Mutual TLS (mTLS)#
Standard TLS authenticates the server to the client. mTLS adds a step: the server requests a client certificate, the client sends one, the server verifies it. The client’s certificate identifies the client.
Server Client │ CertificateRequest │ │────────────────────────────────────►│ │ Certificate (client's) │ │ CertificateVerify │ │◄────────────────────────────────────│mTLS is the dominant service-to-service AuthN pattern in 2026:
- Service mesh sidecars (Envoy, Linkerd) issue short-lived client certificates to every workload and rotate them every few hours. SPIFFE / SPIRE is the open identity framework underneath.
- B2B APIs with high-stakes data flow (banking, healthcare) use mTLS as the primary authentication, often with a static API key as a secondary credential.
- VPN-replacement zero-trust products (Cloudflare Access, Tailscale) use mTLS to authenticate users to internal services.
mTLS shifts the AuthN burden onto certificate management. The certificate becomes a credential — its issuance, renewal, and revocation all matter.
TLS-enabled client example (Python, Go, Node)#
A realistic shape: a client calling a peer API over mTLS, with a client certificate, a client key, and a custom CA bundle for verifying the server. Same pattern in all three languages.
import requests
# Server CA bundle, client cert/key for mTLSresp = requests.get( "https://api.peer.example.com/v1/orders", cert=("/etc/svc/client.crt", "/etc/svc/client.key"), # client cert + key verify="/etc/svc/peer-ca.crt", # trust this CA for the server timeout=5,)resp.raise_for_status()data = resp.json()package main
import ( "crypto/tls" "crypto/x509" "io/ioutil" "net/http" "time")
func mtlsClient() (*http.Client, error) { cert, err := tls.LoadX509KeyPair("/etc/svc/client.crt", "/etc/svc/client.key") if err != nil { return nil, err }
caPEM, err := ioutil.ReadFile("/etc/svc/peer-ca.crt") if err != nil { return nil, err } pool := x509.NewCertPool() pool.AppendCertsFromPEM(caPEM)
tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: pool, MinVersion: tls.VersionTLS13, } return &http.Client{ Transport: &http.Transport{TLSClientConfig: tlsConfig}, Timeout: 5 * time.Second, }, nil}import https from "node:https";import fs from "node:fs";
const agent = new https.Agent({ cert: fs.readFileSync("/etc/svc/client.crt"), key: fs.readFileSync("/etc/svc/client.key"), ca: fs.readFileSync("/etc/svc/peer-ca.crt"), minVersion: "TLSv1.3",});
const resp = await fetch("https://api.peer.example.com/v1/orders", { // @ts-ignore - node-fetch / undici accept `dispatcher` or `agent` agent,});if (!resp.ok) throw new Error(`peer API returned ${resp.status}`);const data = await resp.json();The three examples do the same thing: load a client certificate and key, trust a specific CA bundle for the server’s certificate, refuse anything below TLS 1.3, time out after 5 seconds.
Where to terminate TLS#
A production API has three common termination patterns:
Internet │ │ TLS to LB ▼ Load balancer (TLS termination) │ │ Pattern A: plaintext on private network │ Pattern B: TLS re-encrypt to backend │ Pattern C: TCP passthrough (TLS terminates at backend) ▼ Application- Pattern A — terminate at LB, plaintext behind. Simplest, lowest CPU on the backend. Acceptable only if the network from LB to backend is verifiably private (same VPC, encrypted overlay, etc.). Increasingly out of fashion.
- Pattern B — terminate at LB, re-encrypt to backend. Two TLS handshakes per request; the LB has its own certificate, the backend has its own. mTLS often added between LB and backend for service authentication. Standard in zero-trust deployments.
- Pattern C — passthrough, terminate at backend. The LB does Layer-4 routing; TLS terminates at the application. The application sees the full handshake (useful for mTLS where the client’s identity must reach the application). Costs more on the backend.
The LB-as-TLS-terminator pattern is the dominant one for HTTP APIs. The cost: the LB needs CPU for handshakes (a beefy LB can do tens of thousands of handshakes per second), needs the certificate and private key, and is now a sensitive piece of infrastructure with its own attack surface.
HSTS — the “no fallback to HTTP” header#
HTTP Strict Transport Security (RFC 6797) is one header:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadWhen a browser sees this header on an HTTPS response, it remembers the domain and refuses to ever talk plain HTTP to it (or any subdomain, with includeSubDomains) for the next max-age seconds. The preload directive opts the domain into a list compiled into the browser itself — protection from the very first visit.
Set HSTS on every production HTTPS API. Without it, an attacker on the same Wi-Fi can downgrade a fresh visitor to HTTP and strip the TLS entirely.
Variants#
| Variant | What it does | When it fits |
|---|---|---|
| TLS 1.3 | 1-RTT handshake, mandatory forward secrecy, modern AEAD ciphers. | Every new deployment. Browsers and modern clients all support it. |
| TLS 1.2 + ECDHE | 2-RTT handshake, forward secrecy with the right cipher suites. | Required for backward compatibility with older clients. |
| mTLS | Bidirectional certificate authentication. | Service-to-service, B2B APIs, zero-trust meshes. |
| TLS with cert pinning | Client only trusts a specific certificate or public key. | Mobile apps where the threat model includes hostile CAs. |
| 0-RTT resumption | Repeat clients can send data on the first packet. | Latency-sensitive workloads; carries replay risk for non-idempotent requests. |
| QUIC + TLS 1.3 | TLS embedded into the QUIC transport (HTTP/3). | Modern HTTP stacks; the future-default substrate. |
Trade-offs#
What TLS gives you:
- Confidentiality and integrity on the wire — passive observers see nothing; active attackers are detected.
- Server authentication — the chain of trust binds the connection to the intended hostname.
- Forward secrecy — past sessions stay safe even if long-term keys leak (in TLS 1.3, always).
- A negotiation envelope — version, cipher, ALPN (which application protocol) all negotiated in the handshake.
What TLS costs you:
- Certificate management overhead — issuance, rotation, revocation, monitoring expiry.
- Handshake latency — 1-RTT on TLS 1.3, 2-RTT on TLS 1.2, on top of the TCP handshake. Mitigated with session resumption and 0-RTT.
- CPU — minor on modern CPUs with AES-NI; not zero. Matters at LB scale.
- Debugging complexity — encrypted traffic is opaque to packet captures unless you have the keys. Application-layer logging becomes more important.
TLS 1.3. 5 cipher suites. 1-RTT handshake. Mandatory forward secrecy. Encrypted handshake from step 3. Mandatory AEAD (no MAC-then-encrypt). The default for new deployments.
TLS 1.2. Hundreds of cipher suites (most insecure). 2-RTT handshake. Forward secrecy only with ECDHE. Plaintext handshake. The required fallback for older clients.
Common pitfalls#
- Forgetting the intermediate certificate. Browsers cache intermediates and can mask the bug; curl and library clients cannot. Always ship the full chain.
- Letting the certificate expire. Auto-renew with ACME / cert-manager; alert at
T-30 daysand again atT-7 days. Cert-expiry incidents still take down major sites annually. - Weak ciphers in TLS 1.2 config. Disable RC4, 3DES, CBC modes, and anything without forward secrecy. SSLLabs / testssl.sh will tell you what to remove.
- Terminating TLS at the LB and forgetting the backend leg. Plaintext from LB to backend is fine only if you can defend the claim “the network in between is private and trustworthy.” Increasingly, you cannot.
- Wildcard certificates with leaked private keys. A leaked wildcard (
*.example.com) compromises every subdomain. Prefer per-service certificates from an internal CA. - Pinning the wrong cert. Mobile cert pinning helps against hostile CAs but bricks the app if the cert rotates and the new one was not pre-deployed. Pin to public keys (durable) rather than full certificates (rotated).
- Mixing TLS termination patterns inconsistently. Some endpoints behind the LB use re-encrypt, others use plaintext — easy to miss in a config sprawl. Pick one pattern and apply it everywhere.
Related building blocks#
- API Security — An Overview — where TLS sits in the five-property model (confidentiality + integrity on the wire).
- Authentication vs Authorization — mTLS is an AuthN mechanism; TLS itself is a transport mechanism.
- OAuth 2 — The Authorization Framework — OAuth 2 assumes TLS everywhere; without it the flow is unsafe.
- HTTP — The Foundational Protocol for APIs — the layer that sits on top of TLS; the
sin HTTPS is this page. - API Security — A High-Level Recap — the full security checklist; TLS is one box, mandatory.