TLS & Encryption in Transit
When your app talks to a browser, the data travels across many networks you do not control. Without encryption, anyone in the middle (a coffee-shop Wi-Fi router, an internet provider, an attacker on the same network) can read passwords, session cookies, and personal data in plain text. TLS (Transport Layer Security, the modern name for what people still call SSL) fixes this by encrypting that traffic, so only the two ends can read it. This page is about doing TLS well on Ubuntu, not just turning it on.
First, terms. SSL is the old, broken protocol. TLS is its successor. People say “SSL certificate” out of habit, but every modern setup uses TLS. When this page says TLS, that is what you should be running.
Why “encryption in transit” matters
There are two kinds of encryption in security. At rest means data sitting on a disk is encrypted. In transit means data moving over the network is encrypted. TLS is the in-transit half. Even on an internal network you trust today, encrypting traffic protects you if that network is later breached, and it stops accidental leaks of secrets through logs and proxies.
The goal is not just “HTTPS works.” The goal is: HTTPS everywhere, with strong protocol versions and ciphers, enforced so nobody can downgrade to plain HTTP.
Get a certificate first
Before hardening, you need a certificate. The free, automated standard is Let’s Encrypt via Certbot. If you have not set this up yet, see the Domains/SSL pages in the main DevOps docs. The short version on Ubuntu:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Output:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
Deploying certificate
Successfully deployed certificate for example.com to /etc/nginx/sites-enabled/example.com
Congratulations! You have successfully enabled HTTPS on https://example.com
Certbot renews automatically via a systemd timer. Confirm it:
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
Enforce HTTPS everywhere
Having a certificate is not enough if users can still reach plain http://. You must redirect every HTTP request to HTTPS. Certbot’s Nginx plugin usually adds this, but verify it yourself. In /etc/nginx/sites-available/example.com:
# Plain HTTP server — only job is to redirect to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ... your location blocks ...
}
The return 301 sends a permanent redirect from HTTP to HTTPS. Reload Nginx after any change:
sudo nginx -t && sudo systemctl reload nginx
nginx -t tests the config for syntax errors before reloading, so a typo never takes your site down.
Choose strong protocol versions and ciphers
A cipher is the specific algorithm used to encrypt the connection. Old protocol versions (TLS 1.0 and 1.1) and weak ciphers have known attacks, so disable them. In 2026 the safe baseline is TLS 1.2 and TLS 1.3 only.
Create a shared snippet at /etc/nginx/snippets/ssl-hardening.conf:
# Allow only modern TLS versions
ssl_protocols TLSv1.2 TLSv1.3;
# Let the server pick the cipher order (TLS 1.2)
ssl_prefer_server_ciphers on;
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;
# Reuse TLS sessions for performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
Then include it in each HTTPS server block:
server {
listen 443 ssl http2;
server_name example.com;
include snippets/ssl-hardening.conf;
# ...
}
Gotcha: TLS 1.3 ignores
ssl_ciphersandssl_prefer_server_ciphers— it uses its own fixed, already-strong cipher list. So you only configure ciphers for TLS 1.2. Do not waste time trying to “tune” TLS 1.3 ciphers; the defaults are correct.
If you are unsure what to copy, generate a config tailored to your Nginx version using Mozilla’s SSL Configuration Generator (set profile to “Intermediate”). It produces a known-good ssl_protocols and ssl_ciphers line.
Turn on HSTS
HSTS (HTTP Strict Transport Security) is a header that tells the browser: “for the next N seconds, only ever connect to me over HTTPS, no matter what.” This blocks downgrade attacks where someone tricks a browser into using plain HTTP. Add it to your HTTPS server block:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
max-age=63072000is two years in seconds.includeSubDomainsapplies the rule to every subdomain.preloadlets you submit your domain to the browser preload list.alwaysmakes Nginx send the header even on error responses.
When NOT to use HSTS: only enable
includeSubDomainsandpreloadonce you are 100% certain every subdomain has working HTTPS. Once a browser seespreload, it refuses plain HTTP for that domain — undoing it can take weeks. Start withoutpreload, confirm everything works, then add it.
Test your configuration
Never trust that hardening worked — measure it. Two tools matter.
| Tool | What it is | When to use |
|---|---|---|
| SSL Labs (ssllabs.com/ssltest) | Online scanner from Qualys | Public sites; gives a graded A–F report you can share |
testssl.sh | Local command-line scanner | Internal hosts, CI pipelines, sites not reachable from the internet |
Run testssl.sh on Ubuntu:
sudo apt install -y testssl.sh
testssl.sh https://example.com
Output:
Testing protocols
SSLv2 not offered (OK)
SSLv3 not offered (OK)
TLS 1 not offered (OK)
TLS 1.1 not offered (OK)
TLS 1.2 offered (OK)
TLS 1.3 offered (OK): final
Testing vulnerabilities
Heartbleed not vulnerable (OK)
ROBOT not vulnerable (OK)
Overall Grade A+
Aim for an A or A+. If you see TLS 1.0/1.1 “offered” or any “VULNERABLE” line, fix it before going live. Re-run after every config change.
Best practices
- Redirect all HTTP to HTTPS with a
301and never serve real content on port 80. - Allow only TLS 1.2 and 1.3; disable SSLv3, TLS 1.0, and TLS 1.1 everywhere.
- Configure ciphers only for TLS 1.2 — leave TLS 1.3 defaults alone.
- Enable HSTS, but add
preload/includeSubDomainsonly after all subdomains use HTTPS. - Keep certificate renewal automated and test it with
certbot renew --dry-run. - Scan with SSL Labs and
testssl.shafter every change and target an A+ grade. - Lock down port 443 and 80 access at the firewall so only the web server listens, and apply OS security updates that patch OpenSSL promptly.