TLS 1.3 is the current version of the protocol behind HTTPS. Compared with TLS 1.2 it completes the handshake in one round trip instead of two, removes every legacy cipher and key exchange, and always provides forward secrecy. In this tutorial you will configure Nginx and Apache on Ubuntu 24.04 to offer TLS 1.3 with TLS 1.2 as a fallback, choose ciphers and curves, set up session resumption, understand when 0-RTT is safe, and verify the result from the command line.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with a non-root user that has sudo privileges.
  • Nginx or Apache already serving your_domain over HTTPS with a valid certificate, for example from Let's Encrypt.
  • Port 443 open in your firewall.
  • A second machine (or the same server) with openssl, curl and nmap for testing. Install nmap with sudo apt install nmap.

What changes with TLS 1.3

TLS 1.3 has only five cipher suites, and every one of them uses authenticated encryption with ephemeral key exchange. OpenSSL enables three by default:

TLS 1.3 cipher suiteEnabled by default in OpenSSLNotes
TLS_AES_256_GCM_SHA384YesFast on CPUs with AES-NI
TLS_CHACHA20_POLY1305_SHA256YesFast on phones and CPUs without AES acceleration
TLS_AES_128_GCM_SHA256YesMandatory for every TLS 1.3 implementation
TLS_AES_128_CCM_SHA256NoMeant for constrained IoT devices
TLS_AES_128_CCM_8_SHA256NoShort authentication tag, avoid

Because the TLS 1.3 list contains nothing weak, leave it at its default. The ssl_ciphers (Nginx) and SSLCipherSuite (Apache) settings you will configure below only affect TLS 1.2.

Other practical differences:

  • 1-RTT handshake. The client sends its key share in the first message, saving one round trip on every new connection. On a 100 ms path, that is 100 ms less before the first byte.
  • Encrypted certificate. The server certificate is sent encrypted, so passive observers see less.
  • Resumption through tickets. Sessions are resumed with pre-shared keys; the old session ID mechanism is gone from the protocol.
  • Optional 0-RTT. A resuming client can send data in its first packet, at the cost of replay risk.

Step 1 - Checking your OpenSSL and web server versions

TLS 1.3 needs OpenSSL 1.1.1 or newer. Ubuntu 24.04 ships OpenSSL 3.0, and both Nginx and Apache are built against it. Confirm:

openssl version
nginx -V 2>&1 | grep -o 'built with OpenSSL [0-9.]*'
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)
built with OpenSSL 3.0.13

For Apache, apache2 -v prints the server version (2.4.58 on Ubuntu 24.04); its mod_ssl uses the same system OpenSSL library.

Check what your server negotiates today, before changing anything:

echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | grep -E '^(New|    Protocol)'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
    Protocol  : TLSv1.3

If you already see TLSv1.3, the protocol works and the remaining steps are about tightening the rest of the configuration.

Step 2 - Configuring TLS 1.3 in Nginx

Ubuntu's default /etc/nginx/nginx.conf still enables TLS 1.0 and 1.1 in the http block. Protocol versions are negotiated before Nginx knows which virtual host the client wants, so set them globally in this file rather than per site. Open it:

sudo nano /etc/nginx/nginx.conf

Find the SSL Settings section inside the http block and replace its directives with:

	##
	# SSL Settings
	##

	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers off;
	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_ecdh_curve X25519:prime256v1:secp384r1;

	ssl_session_cache shared:SSL:10m;
	ssl_session_timeout 1d;
	ssl_session_tickets off;

Why these values:

  • ssl_protocols TLSv1.2 TLSv1.3 drops TLS 1.0 and 1.1, which every current browser has already disabled.
  • ssl_ciphers lists only ECDHE key exchange with AEAD ciphers for TLS 1.2 clients, following Mozilla's "intermediate" profile.
  • ssl_prefer_server_ciphers off lets the client choose, since every cipher on the list is strong and clients know best whether they have AES hardware.
  • ssl_ecdh_curve prefers X25519, then the NIST curves for older clients.
  • ssl_session_cache stores sessions in 10 MB of shared memory (about 40,000 sessions). With ssl_session_tickets off, OpenSSL still resumes TLS 1.3 sessions, but keeps the state on the server instead of encrypting it into tickets with a key Nginx never rotates on its own.

Then, in your site's server block, make sure HTTP/2 and HSTS are enabled:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your_domain;

    ssl_certificate     /etc/letsencrypt/live/your_domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000" always;

    # ... the rest of your configuration
}

On Nginx 1.25.1 and newer, write listen 443 ssl; and add a separate http2 on; line; the form above is the correct one for Nginx 1.24 in Ubuntu 24.04. Start with a short HSTS max-age (such as 300) if you are not yet sure every subdomain supports HTTPS.

Test and reload:

sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 3 - Configuring TLS 1.3 in Apache

Apache's defaults live in /etc/apache2/mods-available/ssl.conf. Instead of editing that file, which package upgrades may replace, put your settings in a separate configuration file that loads after it:

sudo nano /etc/apache2/conf-available/tls-hardening.conf
SSLProtocol             -all +TLSv1.2 +TLSv1.3
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
SSLHonorCipherOrder     off
SSLSessionTickets       off
SSLOpenSSLConfCmd       Curves X25519:prime256v1:secp384r1

As with Nginx, SSLCipherSuite without a protocol prefix only applies to TLS 1.2, and the TLS 1.3 suites stay at OpenSSL's defaults. Apache's session cache (SSLSessionCache with shmcb) is already configured in Ubuntu's ssl.conf.

Enable the file, plus HTTP/2 and the headers module for HSTS:

sudo a2enconf tls-hardening
sudo a2enmod http2 headers

In your HTTPS virtual host, allow HTTP/2 and send HSTS:

sudo nano /etc/apache2/sites-available/your_domain-le-ssl.conf
<VirtualHost *:443>
    ServerName your_domain
    Protocols h2 http/1.1

    Header always set Strict-Transport-Security "max-age=63072000"

    # ... certificate directives and the rest of your configuration
</VirtualHost>

The file name depends on how the site was created; Certbot names it *-le-ssl.conf. If the virtual host includes /etc/letsencrypt/options-ssl-apache.conf, those settings take precedence for that site, just as in Nginx.

Ubuntu's Apache uses the prefork MPM by default when PHP is installed through libapache2-mod-php, and HTTP/2 does not work with prefork. If apache2ctl -M | grep mpm shows mpm_prefork_module, HTTP/2 is ignored but TLS 1.3 still works.

Check the syntax and reload:

sudo apache2ctl configtest
sudo systemctl reload apache2
Syntax OK

Step 4 - Verifying protocols and ciphers

Test that TLS 1.3 and TLS 1.2 both work by forcing each version from the client:

echo | openssl s_client -connect your_domain:443 -servername your_domain -tls1_3 2>/dev/null | grep '^New'
echo | openssl s_client -connect your_domain:443 -servername your_domain -tls1_2 2>/dev/null | grep '^New'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256

The TLS 1.2 cipher will be ECDHE-ECDSA-... if your certificate uses an ECDSA key, which is the default for new Certbot certificates.

A modern OpenSSL client refuses to speak TLS 1.0 or 1.1 itself, so it cannot prove the server rejects them. Use nmap, which lists every protocol and cipher the server accepts:

nmap --script ssl-enum-ciphers -p 443 your_domain
PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers:
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (x25519) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (x25519) - A
|     compressors:
|       NULL
|     cipher preference: client
|   TLSv1.3:
|     ciphers:
|       TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
|       TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
|       TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A
|     cipher preference: client
|_  least strength: A

Only TLSv1.2 and TLSv1.3 should appear, with every cipher graded A. Finally, confirm HTTP/2 and HSTS:

curl -sI https://your_domain/ | grep -iE '^(HTTP|strict-transport)'
HTTP/2 200
strict-transport-security: max-age=63072000

Step 5 - Measuring the handshake

curl can report how long the TCP connection and the TLS handshake took. time_appconnect is the moment TLS finished, so the difference between the two values is the handshake itself:

curl -so /dev/null -w 'tcp: %{time_connect}s  tls: %{time_appconnect}s  ttfb: %{time_starttransfer}s\n' --tlsv1.3 https://your_domain/
curl -so /dev/null -w 'tcp: %{time_connect}s  tls: %{time_appconnect}s  ttfb: %{time_starttransfer}s\n' --tls-max 1.2 https://your_domain/
tcp: 0.042s  tls: 0.089s  ttfb: 0.131s
tcp: 0.041s  tls: 0.129s  ttfb: 0.171s

Run each command a few times from a machine far from the server. With TLS 1.3, the handshake should take roughly one round trip (about the same as the TCP connect time), against two for TLS 1.2.

About 0-RTT (early data)

TLS 1.3 lets a client that is resuming a session send its first request together with the handshake, removing the last round trip. The catch is that early data can be replayed by an attacker who captures it: the server may process the same request twice. This is harmless for a GET of a static page and dangerous for anything that changes state.

  • Nginx supports it with ssl_early_data on;, which is off by default. If you enable it, pass proxy_set_header Early-Data $ssl_early_data; to your application and have it answer 425 Too Early to non-idempotent requests that arrive as early data.
  • Apache mod_ssl does not support 0-RTT.

For most sites, the gain is small next to the risk. Leave it off unless you have measured a need and your application handles the Early-Data header.

Post-quantum key exchange

Browsers now offer the hybrid X25519MLKEM768 key exchange, which protects recorded traffic against future quantum computers. Supporting it on the server requires OpenSSL 3.5 or newer; the OpenSSL 3.0 in Ubuntu 24.04 does not include it, and the configuration above simply negotiates X25519 with those clients. When your distribution moves to OpenSSL 3.5, you can add X25519MLKEM768 at the front of ssl_ecdh_curve (Nginx) or the Curves list (Apache).

Troubleshooting

nmap still shows TLSv1.0 or TLSv1.1. Another file still enables them. Search the configuration with sudo grep -rn 'ssl_protocols' /etc/nginx/ or sudo grep -rn 'SSLProtocol' /etc/apache2/. In Nginx, the default server for the IP address and port decides the protocol list, so fix it there.

s_client -tls1_3 fails with alert protocol version. The server does not offer TLS 1.3. Check that the ssl_protocols or SSLProtocol line you edited is the one actually in effect, and reload the service.

Some old clients can no longer connect. Devices that only speak TLS 1.0/1.1 (Android 4.3 and older, very old Java versions) are excluded by design. Update or replace them rather than reopening obsolete protocols for everyone.

Conclusion

Your Nginx or Apache server on Ubuntu 24.04 now offers TLS 1.3 with a strong TLS 1.2 fallback, uses modern curves, resumes sessions efficiently, and you have verified protocols, ciphers and handshake times from the command line. As next steps, automate certificate renewal with ACME if you have not already, run a periodic nmap check against your public endpoints, and revisit the post-quantum settings once OpenSSL 3.5 reaches your servers.