Skip to content
DevOps devops security 5 min read

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_ciphers and ssl_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=63072000 is two years in seconds.
  • includeSubDomains applies the rule to every subdomain.
  • preload lets you submit your domain to the browser preload list.
  • always makes Nginx send the header even on error responses.

When NOT to use HSTS: only enable includeSubDomains and preload once you are 100% certain every subdomain has working HTTPS. Once a browser sees preload, it refuses plain HTTP for that domain — undoing it can take weeks. Start without preload, confirm everything works, then add it.

Test your configuration

Never trust that hardening worked — measure it. Two tools matter.

ToolWhat it isWhen to use
SSL Labs (ssllabs.com/ssltest)Online scanner from QualysPublic sites; gives a graded A–F report you can share
testssl.shLocal command-line scannerInternal 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 301 and 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/includeSubDomains only after all subdomains use HTTPS.
  • Keep certificate renewal automated and test it with certbot renew --dry-run.
  • Scan with SSL Labs and testssl.sh after 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.
Last updated June 15, 2026
Was this helpful?