Forward secrecy: why yesterday stays private
Somebody records your encrypted traffic today and gets your server's private key a year from now. Forward secrecy is the property that keeps the recording unreadable anyway: the keys protecting each TLS conversation came from a key exchange whose secret values were erased, and the long-term key cannot recreate them [2]. RFC 4949 defines it, under the name perfect forward secrecy, as a property of key agreement: a compromise of long-term keying material does not compromise session keys derived earlier [4].
- Also called
- perfect forward secrecy, PFS [1]
- Protects
- traffic recorded before a long-term key was compromised [1]
- Comes from
- an ephemeral Diffie-Hellman key exchange, over a finite field (DHE) or elliptic curves (ECDHE), with the secret values erased after the handshake [1] [2]
- Undone by
- session ticket keys that are never rotated, and Diffie-Hellman keys reused across connections [1]
What it protects against
Forward secrecy defends against one attacker: the one who records encrypted conversations now and obtains the long-term keys later [1]. Without it, a single key decrypts the past. RFC 5246, the TLS 1.2 specification, says it plainly for RSA key exchange: compromise of the server's static RSA key results in a loss of confidentiality for all sessions protected under that key [3].
How would anybody get the key years later? RFC 9325, the IETF's TLS recommendations, calls such a compromise "not entirely implausible" and lists ways it happens [1]:
a server or client attacked by some other route, and the private key taken;
a device sold or decommissioned without being wiped;
private keys collected from badly protected backups;
social engineering against administrators;
a cryptographic breakthrough, or keys that were too short.
With forward secrecy, determining the session keys of an old conversation is not feasible even with those keys in hand. It also covers an attacker who already holds the long-term key but stays passive during the conversation [1].
How the separation works
A TLS connection asks two different questions. Is this really the server for this name? And which keys protect the next few minutes of traffic? Forward secrecy depends on answering them with different keys.
In an ephemeral Diffie-Hellman exchange, each side picks a private value for this connection only and sends the matching public value. Both compute the same shared secret from their own private value and the other's public one; an eavesdropper who sees only the public values cannot [1]. The certificate's key does a separate job: it signs, to prove the exchange came from the server [3]. Once the handshake ends, the private values and the derived secrets have to be erased. RFC 9846, the TLS 1.3 specification, states the property under exactly that condition: a long-term key compromised after the handshake does not compromise the session key, as long as the session key and everything that could recreate it has been erased [2].
Static RSA key transport has no such split. The client encrypts the pre-master secret, from which the session keys are derived, with the public key in the server's certificate [3]. The private key that proves identity is also the key that unlocks every recorded session.
Which of the two a connection uses is settled while the handshake agrees on keys, and in TLS 1.2 it is named by the cipher suites the two sides agree on.
TLS 1.2 and TLS 1.3
In TLS 1.2, forward secrecy is optional. RFC 5246 still defines RSA key transport and advises users who want forward secrecy to choose ephemeral Diffie-Hellman (DHE) suites [3]. RFC 9325 now requires implementations to support and prefer suites with forward secrecy and says they SHOULD NOT negotiate static RSA [1]; RFC 10015 goes further and deprecates RSA key exchange in TLS 1.2 [5]. How the two versions differ otherwise is on TLS 1.2 versus TLS 1.3.
TLS 1.3 removed static RSA and static Diffie-Hellman, and every public-key key exchange in it provides forward secrecy [2]. What remains is the pre-shared key used for resumption: it can be combined with a fresh key exchange or used alone, and used alone it gives up forward secrecy for the application data [2].
Where it quietly breaks: resumption and reused keys
A server can run an ECDHE key exchange on every full handshake and still lose forward secrecy through the shortcuts that make connections faster: resumption without a fresh key exchange, session tickets under a key that never changes, and reused Diffie-Hellman values.
Resumption in TLS 1.3
For TLS 1.3, RFC 9846 defines two modes for resuming with a pre-shared key: psk_dhe_ke, which adds a fresh key exchange, and psk_ke, which does not. The forward secrecy property is not satisfied in psk_ke [2]. RFC 9325 says servers SHOULD select psk_dhe_ke [1]. OpenSSL allows the PSK-only mode on resumption only when the SSL_OP_ALLOW_NO_DHE_KEX option is set, and its manual warns there is then no forward secrecy for the resumed session [6]. In our run with OpenSSL 3.0.13 defaults, the resumed connection still showed a fresh X25519 key [9].
Session tickets in TLS 1.2
A TLS 1.2 session ticket is the session state, encrypted by the server and handed to the client, so the server needs no per-client cache (RFC 5077) [10]. A stolen ticket-encryption key can decrypt the sessions it protected, which is why RFC 9325 requires ticket keys to be changed regularly, "e.g., once every week", and old ones destroyed. TLS 1.2 also never rolls the session key forward inside a session, so RFC 9325 adds that servers SHOULD NOT resume sessions that are too old [1].
nginx and Apache both enable session tickets by default [7] [8]. The Apache mod_ssl manual warns that using them without restarting the server at an appropriate frequency, for example daily, compromises perfect forward secrecy [8]. In nginx, a shared session cache generates and rotates ticket keys automatically since 1.23.2, unless keys are set with ssl_session_ticket_key; with key files, rotation is yours to do [7].
Reused Diffie-Hellman keys
Some implementations reuse an ephemeral Diffie-Hellman or ECDHE private value across connections to save work. RFC 9325 warns that if a value is reused for too long, sometimes only a few hours, an attacker who gets into the host can decrypt previous connections: the reuse negates forward secrecy [1]. RFC 9846 puts it the other way round: a secret meant to be ephemeral that is not erased becomes one more long-term key to protect [2].
What it does not cover
Forward secrecy protects one direction in time, the past [2]. It leaves these open:
The future. Whoever holds your key can impersonate your server to new visitors with an active attack. A full handshake still protects new connections from passive eavesdroppers, but not from impersonation [2]. RFC 9325 names certificate revocation as an important tool for recovering from such an attack [1].
An endpoint that was already compromised. The property covers a key compromised after the handshake. A server under somebody else's control during the conversation hands over the session keys with it [2].
What either side keeps. Unreadable traffic in transit says nothing about the copy in your database, your logs or the visitor's browser.
What travels before encryption starts. Everything after the key exchange phase is encrypted; the ClientHello is part of that phase and carries the requested hostname (SNI) and the protocols offered through ALPN [2].
How to check whether a server has it
Connect with openssl s_client and look for the line that names the server's temporary key. Against our OpenSSL 3.0.13 test server it read Server Temp Key: X25519, 253 bits over TLS 1.3 and over TLS 1.2 with ECDHE, and it was absent when the server was limited to RSA key transport [9]; in the example, example.com stands for your host, and the output lines are from that test server:
$ openssl s_client -connect example.com:443 -servername example.com -brief </dev/null 2>&1 | grep -E 'Protocol version|Temp Key'
Protocol version: TLSv1.3
Server Temp Key: X25519, 253 bitsThen test the case that loses it. Offer TLS 1.2 with RSA key transport only, -tls1_2 -cipher kRSA. If the connection completes, the server accepts a key exchange without forward secrecy. An OpenSSL 3.0.13 server with default settings accepted it in our run [9], so a working site can pass the first check and fail this one.
Resumption needs the full output, without -brief: save a session with -sess_out and reconnect with -sess_in, and the summary line starts with Reused; a Temp Key line in the resumed connection means a fresh key exchange ran [6] [9].
What to do about it
Forward secrecy is not a setting you switch on. It follows from what the server agrees to, so the work is in four places:
Enable TLS 1.3. Every public-key key exchange in it is forward-secret [2].
For TLS 1.2, allow only ECDHE key exchange, and confirm with the
kRSAtest above that RSA key transport is refused [1] [5] [9].Keep resumption forward-secret. With OpenSSL, leave
SSL_OP_ALLOW_NO_DHE_KEXunset [6].Rotate session ticket keys or turn tickets off:
ssl_session_tickets offin nginx,SSLSessionTickets offin Apache. In nginx, a shared session cache withoutssl_session_ticket_keyrotates the keys for you [7] [8].
Check what the server negotiates, not what its configuration file appears to say: the effective result combines the software's defaults with your settings, and the default server in our run accepted RSA key transport [9].
FAQ
Is forward secrecy something I turn on?
Do I still need it now?
Sources
RFC 9325 (BCP 195), Recommendations for Secure Use of TLS and DTLS: §3.4, §4.1, §7.3, §7.4, §7.5.
RFC 9846, TLS 1.3: §1.3, §2, §2.2, §4.2, §4.3.9, Appendix F.1.
RFC 5246, TLS 1.2: §7.4.7.1, Appendix F.1.1.2, F.1.1.3.
RFC 4949, Internet Security Glossary, Version 2: "perfect forward secrecy".
RFC 10015, Deprecating Obsolete Key Exchange Methods in (D)TLS 1.2: abstract, §4.
OpenSSL 3.0 manual: SSL_CTX_set_options (SSL_OP_ALLOW_NO_DHE_KEX), openssl-s_client (-brief, -sess_out, -sess_in).
nginx: ngx_http_ssl_module (ssl_session_cache, ssl_session_ticket_key, ssl_session_tickets).
Apache HTTP Server 2.4, mod_ssl: SSLSessionTickets.
Our runs: OpenSSL 3.0.13 on Linux, localhost, a www.example.com test certificate from our own lab root.
RFC 5077, TLS Session Resumption without Server-Side State: abstract.