SSL/TLS Configuration

A complete HTTPS server block, redirecting HTTP to HTTPS, and certificate basics with Let's Encrypt/Certbot.

Certificate basics, briefly

HTTPS relies on a TLS certificate — a file that proves a server genuinely controls the domain it claims to, signed by a Certificate Authority (CA) that browsers already trust. A certificate comes paired with a private key, which must stay secret on the server and is never sent anywhere; the certificate itself is public and is exactly what the server presents to a connecting browser. Let's Encrypt is a free, widely-used CA, and Certbot is the standard tool for requesting a certificate from it and installing it into Nginx automatically:

Bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot's Nginx plugin does three things in one command: requests a certificate from Let's Encrypt, writes the certificate and key to disk, and edits the relevant server block to reference them — the manual server block configuration in this page is exactly what that plugin generates for you. Let's Encrypt certificates expire every 90 days by design (a deliberately short lifetime that limits the damage of a leaked or forgotten key), so Certbot also installs a scheduled renewal job; sudo certbot renew --dry-run verifies that renewal will actually work before you need to rely on it.

A complete HTTPS server block

Nginx
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    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;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root /var/www/mysite/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
  • listen 443 ssl; — accepts HTTPS connections on the standard TLS port. listen [::]:443 ssl; is the same thing over IPv6.
  • ssl_certificate / ssl_certificate_key — paths to the certificate (fullchain.pem, which includes the CA's intermediate certificates the browser needs to build a full trust chain) and the private key. Both must be present and match each other, or Nginx refuses to start.
  • ssl_protocols TLSv1.2 TLSv1.3; — restricts which TLS versions are accepted. Older versions (TLS 1.0, 1.1, and SSL entirely) have known weaknesses and should never be enabled on a modern server; TLS 1.2 and 1.3 are the current secure baseline.
  • ssl_ciphers / ssl_prefer_server_ciphers — which encryption cipher suites are allowed, and whether the server's preferred order (rather than the client's) wins when negotiating one. HIGH:!aNULL:!MD5 is a conservative baseline excluding null and MD5-based ciphers; modern setups often defer to Mozilla's regularly-updated SSL configuration generator rather than hand-picking a cipher list.

Redirecting HTTP to HTTPS

A production site should never leave port 80 serving real content over plain HTTP — every http:// request should be redirected to the equivalent https:// URL:

Nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    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;

    root /var/www/mysite/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Two separate server blocks share the same server_name but listen on different ports — the port-80 block's entire job is a 301 Moved Permanently redirect (return 301 https://$host$request_uri;), preserving the requested host and path exactly, just switching the scheme. 301 (rather than a temporary 302) tells browsers and search engines this redirect is permanent, so they cache it and stop requesting the plain HTTP version at all on future visits.

HSTS: telling browsers to skip HTTP entirely

Even with a redirect in place, the very first request a browser ever makes to a domain still goes out over plain HTTP before being redirected — a brief window an attacker on the same network could intercept. HTTP Strict Transport Security (HSTS) closes this by telling the browser, via a response header, to never attempt a plain HTTP connection to this domain again for a specified duration:

Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Placed inside the HTTPS server block, this tells any browser that successfully loads the site once to rewrite every future request to that domain (and its subdomains, via includeSubDomains) as HTTPS internally, without ever making the initial insecure request at all — max-age=31536000 is one year, in seconds.

Common mistakes

  • Serving real content over both port 80 and 443 instead of redirecting 80 to 443 — this leaves users free to stay on an unencrypted connection indefinitely if they happen to type or follow an http:// link.
  • Enabling TLSv1 or TLSv1.1 for "compatibility" — both have known cryptographic weaknesses and are disabled by default in current browsers anyway, so keeping them enabled server-side adds risk without adding real compatibility.
  • Letting a Let's Encrypt certificate expire by not verifying the renewal job actually works (certbot renew --dry-run) — a site silently serving an expired certificate presents visitors with a frightening full-page browser warning instead of a normal page load.
  • Adding an HSTS header with a long max-age before confirming HTTPS is fully and permanently working — once a browser has cached that header, it refuses to fall back to HTTP for that domain until the max-age expires, even if HTTPS breaks in the meantime.