Perfect Forward Secrecy (PFS) — also called Forward Secrecy (FS) — is a TLS property that ensures each session uses a unique ephemeral key that is never stored. Even if an attacker captures encrypted traffic today and later obtains your server's private key, they cannot decrypt those past sessions. This guide shows how to configure PFS on Nginx and Apache.
What Is Perfect Forward Secrecy?
In a standard TLS handshake without PFS, the session key is derived in a way that is tied to the server's long-term private key. If that private key is stolen — through a server breach, legal compulsion, or future cryptanalysis — an attacker who recorded past traffic can retroactively decrypt it.
PFS prevents this by using ephemeral key exchange algorithms — specifically ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) or DHE (Diffie-Hellman Ephemeral). Each TLS session generates a new, temporary key pair that is discarded after the session ends. The session key is never derivable from the long-term private key alone.
How PFS Works
With ECDHE or DHE, the key exchange works like this:
- During the TLS handshake, the server generates a temporary (ephemeral) key pair.
- The client and server exchange public keys and compute a shared secret independently — the Diffie-Hellman exchange.
- This shared secret is used to derive the session encryption keys.
- After the session ends, the ephemeral private key is discarded. It cannot be recovered.
Because the session key is never written to disk and is not derivable from the long-term certificate private key, compromise of the certificate does not expose past sessions.
Prerequisites
- Root or sudo access to your web server.
- OpenSSL 1.0.1 or later (run
openssl versionto check). - A valid TLS certificate already installed.
- Configuration file backups made before making changes.
Step 1 — Generate DH Parameters (for DHE ciphers)
DHE cipher suites require a set of pre-generated Diffie-Hellman parameters. ECDHE does not need this file, but generating it future-proofs your configuration.
# Generate 2048-bit DH parameters (recommended minimum)
sudo openssl dhparam -out /etc/ssl/dhparam.pem 2048
# This command takes several minutes to complete — that is expected
# Verify the file was created:
ls -lh /etc/ssl/dhparam.pemStep 2 — Configure Nginx for PFS
Edit your Nginx server block (typically in /etc/nginx/sites-available/ or /etc/nginx/conf.d/). The key changes are the ssl_ciphers and ssl_dhparam directives:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# TLS protocols — disable old, insecure versions
ssl_protocols TLSv1.2 TLSv1.3;
# PFS-enabling cipher suites (ECDHE and DHE only)
# ECDHE ciphers are preferred; DHE is included for broader compatibility
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
# Prefer server cipher order
ssl_prefer_server_ciphers on;
# DH parameters for DHE ciphers
ssl_dhparam /etc/ssl/dhparam.pem;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# OCSP stapling (improves TLS handshake speed)
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS (optional but recommended)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}# Test configuration for syntax errors
sudo nginx -t
# If the test passes, reload (no downtime)
sudo systemctl reload nginxStep 3 — Configure Apache for PFS
Edit your Apache SSL virtual host configuration. Ensure mod_ssl is enabled (sudo a2enmod ssl on Debian/Ubuntu).
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile /etc/ssl/private/example.com.key
SSLCertificateChainFile /etc/ssl/certs/example.com.chain.crt
# Disable old, insecure protocols
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
# PFS-enabling cipher suites
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# Prefer server cipher order
SSLHonorCipherOrder on
# DH parameters for DHE ciphers
SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparam.pem"
# HSTS (optional but recommended)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost># Test configuration for syntax errors
sudo apache2ctl configtest
# If the test passes, restart Apache
sudo systemctl restart apache2mod_ssl and mod_headers are both enabled. On Debian/Ubuntu: sudo a2enmod ssl headers. If Apache fails to start, check sudo tail -50 /var/log/apache2/error.log for the specific error.Step 4 — Verify PFS Is Working
After reloading your server, confirm that PFS cipher suites are being negotiated:
# Connect with OpenSSL and check the negotiated cipher
openssl s_client -connect example.com:443 -tls1_2 2>/dev/null | grep "Cipher"
# A PFS-enabled connection will show ECDHE or DHE in the cipher name, e.g.:
# Cipher : ECDHE-RSA-AES256-GCM-SHA384
# Verify no non-PFS ciphers are accepted (RSA key exchange only — no ECDHE/DHE)
# This should return "no peer certificate available" or a handshake failure:
openssl s_client -connect example.com:443 -cipher "AES256-SHA" 2>&1 | grep -E "Cipher|alert"
# Check which TLS versions are supported
openssl s_client -connect example.com:443 -tls1 2>&1 | grep "ssl handshake failure"
openssl s_client -connect example.com:443 -tls1_1 2>&1 | grep "ssl handshake failure"The cipher name in the output must start with ECDHE or DHE — these indicate PFS is active. Cipher names starting with AES256- or RSA- without the ECDHE/DHE prefix indicate non-PFS key exchange.
Step 5 — Run an SSL Labs Test
The most thorough way to verify your configuration is to use SSL Labs SSL Server Test. It grades your TLS configuration and specifically reports:
- Forward Secrecy — shows "Yes" when PFS cipher suites are supported for all clients.
- Protocol Support — confirms TLS 1.2 and 1.3 are active and older protocols are disabled.
- Cipher Suites — lists every cipher suite your server accepts, with PFS status for each.
- Overall Grade — aim for A or A+.
You can also check your SSL configuration with the ShowDNS SSL Checker to verify certificate validity and basic TLS settings.
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Server won't start after config change | Syntax error in config or missing DH params file | Run nginx -t or apache2ctl configtest and check the error log |
| SSL Labs shows "Forward Secrecy: No" | Non-ECDHE/DHE ciphers are still in the cipher list or preferred | Remove all RSA key-exchange ciphers from ssl_ciphers / SSLCipherSuite |
| Some older clients can't connect | Strict cipher list excludes legacy cipher suites | This is expected — browsers that don't support ECDHE or DHE are outdated |
| High CPU usage after enabling DHE | DHE is more CPU-intensive than ECDHE | Move ECDHE ciphers to the top of the list; ECDHE is faster and equally secure |
Frequently Asked Questions
Does enabling PFS affect performance?
ECDHE has negligible performance impact compared to RSA key exchange. DHE is slower due to larger key sizes, but ECDHE handles the same role more efficiently. On modern hardware with TLS 1.3, PFS has essentially no measurable overhead for end users.
Do I need to generate new DH parameters regularly?
For ECDHE cipher suites, no DH parameters file is needed. For DHE, regenerating the DH parameters periodically (e.g. annually) is a good practice but is not strictly required for security. The dhparam.pem file does not contain secrets.
Is TLS 1.3 automatically PFS?
Yes. TLS 1.3 removed all non-PFS cipher suites entirely — every TLS 1.3 handshake uses ECDHE. If your server and client both support TLS 1.3, PFS is guaranteed for those connections regardless of your TLS 1.2 cipher configuration.
Should I disable TLS 1.2 entirely and use only TLS 1.3?
TLS 1.3 offers the strongest security, but some older clients do not support it yet. A configuration that enables both TLS 1.2 (with PFS-only ciphers) and TLS 1.3 provides strong security while maintaining broad compatibility. Only disable TLS 1.2 if your user base is known to use modern clients exclusively.