HTTPS and TLS done right on Linux servers
A good TLS setup in 2026 is short: allow only TLS 1.2 and 1.3, use the published “intermediate” cipher list, renew certificates automatically, and test from outside. Most problems come from old copied configs, HSTS turned on too fast, OCSP stapling that no longer has anything to staple, and services never restarted after an OpenSSL update.
Where the recommended config lives
For years admins copied their settings from Mozilla’s SSL Configuration Generator and its “Server Side TLS” guidelines. Both have moved. The generator at ssl-config.mozilla.org now sends you to the TLSRef Configurator, and the Mozilla wiki page points to the guidelines at docs.tlsref.org. The content is the same lineage; the current guideline is version 6.0, which removed the old “Old” profile.
Two profiles remain:
- Intermediate (recommended)
- TLS 1.2 and 1.3. Works with nearly every client from the last five or more years, back to Chrome 49, Firefox 31.3, Safari 9, Android 4.4.2, IE 11 on Windows 10 and Java 8u161. Use this for public websites.
- Modern
- TLS 1.3 only. Oldest clients are Chrome 70, Firefox 63, Safari 12.1, Android 10, Java 11 and OpenSSL 1.1.1. Good for APIs and internal services where you control the clients.
In both profiles every cipher is forward secret and authenticated, and the client chooses the cipher (ssl_prefer_server_ciphers off). The guideline recommends ECDSA P-256 certificates, or RSA 2048 if you need RSA. Certbot has created ECDSA P-256 keys by default since version 2.0.
nginx: intermediate config
This is the configurator’s intermediate output for current nginx with OpenSSL 3.5 or later, with Let’s Encrypt paths filled in. Put the ssl_* lines in the http block so every site gets them:
# /etc/nginx/conf.d/tls.conf (inside http {})
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
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;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
# one site
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}
# send plain HTTP to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
return 308 https://$host$request_uri;
}
Check your library with openssl version. X25519MLKEM768 is a post-quantum hybrid key exchange and needs OpenSSL 3.5 or later. On older OpenSSL, the configurator leaves it out; use ssl_ecdh_curve X25519:prime256v1:secp384r1;. The http2 on; directive needs nginx 1.25.1 or later. Let’s Encrypt follows the redirect, so HTTP-01 renewals keep working if the HTTPS site serves /.well-known/acme-challenge/ from the webroot Certbot uses. Since nginx 1.23.2 the shared session cache also rotates session ticket keys for you, so you no longer need ssl_session_tickets off.
Apache: intermediate config
# mod_ssl, server-wide (e.g. /etc/apache2/conf-available/tls.conf)
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1:secp384r1
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
SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
SSLHonorCipherOrder off
SSLSessionTickets off
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Protocols h2 http/1.1
</VirtualHost>
Set SSLProtocol yourself. Apache’s built-in default is all -SSLv3, which still names TLS 1.0 and 1.1. Apache turns session tickets on by default, and its manual warns that tickets weaken forward secrecy unless the server restarts often, so the configurator turns them off. As with nginx, drop X25519MLKEM768 on OpenSSL older than 3.5. On Debian and Ubuntu enable the pieces with sudo a2enmod ssl http2 headers and sudo a2enconf tls.
Test, then apply. A reload is enough for config and certificate changes:
sudo nginx -t && sudo systemctl reload nginx
sudo apachectl configtest && sudo systemctl reload apache2 # httpd on RHEL family
HSTS, with care
HTTP Strict Transport Security tells browsers to use only HTTPS for your domain for max-age seconds. It stops downgrade attacks, but you cannot take it back: a browser that has seen the header refuses plain HTTP until the time runs out. With includeSubDomains that covers every subdomain, including old intranet or printer names that have no certificate.
Roll it out in steps, as hstspreload.org advises, and wait the full max-age at each step while you watch for broken pages:
max-age=300(5 minutes)max-age=604800(1 week)max-age=2592000(1 month)max-age=63072000(2 years, the TLSRef value), addingincludeSubDomainsonly when every subdomain works on HTTPS
# nginx, in each HTTPS server block
add_header Strict-Transport-Security "max-age=300" always;
# Apache, in the :443 VirtualHost (mod_headers)
Header always set Strict-Transport-Security "max-age=300"
In nginx, a location block with its own add_header drops the headers inherited from the server block, so HSTS can silently vanish on some paths. Repeat it there, or use add_header_inherit merge; on nginx 1.29.3 and later. Only add preload and submit to the browser preload list if you are sure: that list needs at least one year, includeSubDomains, and removal takes months.
OCSP stapling after Let’s Encrypt
OCSP stapling lets the server attach a fresh “not revoked” answer from the CA. Let’s Encrypt has left OCSP behind: it removed OCSP URLs from new certificates on May 7, 2025, rejected Must-Staple requests from that date, and switched its OCSP responders off on August 6, 2025. It now publishes revocation only through CRLs.
So for Let’s Encrypt certificates, stapling has nothing to fetch. Remove ssl_stapling, ssl_stapling_verify and the related resolver lines from nginx, and SSLUseStapling from Apache (it is off by default). Do not use Certbot’s --must-staple or --staple-ocsp options with Let’s Encrypt. Certificates from other CAs may still carry an OCSP URL; check before you decide:
openssl x509 -in /path/to/cert.pem -noout -ocsp_uri # empty output = no OCSP
Automatic renewal and shorter lifetimes
Let’s Encrypt certificates still last 90 days by default (the “classic” profile), but that is shrinking:
| From | Classic profile lifetime | Authorization reuse |
|---|---|---|
| Now | 90 days | 30 days |
| February 10, 2027 | 64 days | 10 days |
| February 16, 2028 | 45 days | 7 hours |
The opt-in tlsserver profile has issued 45-day certificates since May 13, 2026, and a shortlived profile gives about 6 days. Industry rules cap all public certificates at 100 days from March 15, 2027 and 47 days from March 15, 2029.
What to do now:
- Run Certbot 4.1 or later. Since 4.0 it renews when a third of the lifetime is left (not at a fixed 30 days), and since 4.1 it checks ACME Renewal Information (ARI, RFC 9773), so the CA can ask for early renewal, for example before a mass revocation.
- Remove any cron job with a fixed “every 60 days” schedule. Run
certbot renewtwice a day and let it decide. - Try the shorter profile early:
sudo certbot renew --cert-name example.com --preferred-profile tlsserver --force-renewal(once; Certbot saves the profile for later renewals). - Add a deploy hook that reloads every service using the certificate, and monitor expiry from outside. Details are on the expired certificate page.
Test from outside
Quick check with OpenSSL (use the real host name so SNI picks the right site):
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
# should FAIL: old protocol refused
openssl s_client -connect example.com:443 -servername example.com -tls1_1 </dev/null
# mail server with STARTTLS
openssl s_client -connect mail.example.com:587 -starttls smtp -brief </dev/null
-brief prints the protocol, cipher and verification result. A failed TLS 1.1 test proves little on its own, because a recent OpenSSL client may refuse TLS 1.1 itself. The free testssl.sh tool checks properly, because it does not depend on what the local OpenSSL allows:
git clone --depth 1 https://github.com/testssl/testssl.sh.git
cd testssl.sh
./testssl.sh example.com # full report
./testssl.sh -p -U example.com # protocols + known vulnerabilities only
./testssl.sh --starttls smtp mail.example.com:25
./testssl.sh --jsonfile result.json example.com # for scripts and records
It also runs as a container: docker run --rm -it ghcr.io/testssl/testssl.sh example.com. Run it after every config change, and on a schedule, so drift shows up.
Restart after OpenSSL updates
A new certificate needs only a reload. A new OpenSSL library needs a restart: running nginx, Apache, PHP-FPM, Postfix, Dovecot and sshd keep the old libssl in memory until they restart, so a patched server can still serve the vulnerable code. After an OpenSSL update, run sudo needrestart (Debian/Ubuntu) or sudo dnf needs-restarting -s (RHEL family) and restart what it lists. This briefly drops connections. See restarting after an update and OpenSSL for supported branches.
Sources
- Server-Side TLS (guideline 6.0), TLSRef
- TLS Configurator, TLSRef
- SSL Configuration Generator (moved notice), Mozilla
- Security/Server Side TLS (moved notice), Mozilla wiki
- Module ngx_http_ssl_module, nginx.org
- Module ngx_http_headers_module, nginx.org
- Apache Module mod_ssl, Apache Software Foundation
- HSTS Preload List Submission, hstspreload.org
- Ending OCSP Support in 2025, Let’s Encrypt
- OCSP Service Has Reached End of Life, Let’s Encrypt
- Decreasing Certificate Lifetimes to 45 Days, Let’s Encrypt
- Profiles, Let’s Encrypt
- Certificate Lifetime Rationale and Plans, Let’s Encrypt
- RFC 9773: ACME Renewal Information (ARI) Extension, RFC Editor
- Certbot User Guide, EFF
- Certbot change log, Certbot project
- testssl.sh
- openssl-s_client, OpenSSL documentation