How to Enable SSL/TLS on Nginx

How to Enable SSL/TLS on Nginx

How to Enable SSL/TLS on Nginx

There’s no excuse anymore for running a production site without HTTPS. Between free certificate authorities, automated renewal tools, and browsers actively flagging plain HTTP sites as “Not Secure,” enabling SSL/TLS on Nginx has gone from a paid, manual chore to something I can set up in under ten minutes, fully automated. In this guide, I’ll walk through the whole process using Let’s Encrypt and Certbot — the free, widely trusted route I use for the vast majority of the sites I manage — along with the manual certificate path for cases where you’re using a paid CA instead.

SSL vs TLS — A Quick Clarification

I still hear these terms used interchangeably, so it’s worth a quick note: SSL (Secure Sockets Layer) is the older protocol, now deprecated and insecure. TLS (Transport Layer Security) is its modern successor. When people say “SSL certificate” today, they almost always mean a certificate used for TLS. Nginx directives still use the ssl_ prefix for historical reasons even though the actual protocol in use is TLS.

Requirements

Method 1: Let’s Encrypt with Certbot (Recommended)

This is the approach I use for nearly every site — it’s free, automated, and widely trusted by all major browsers.

Step 1: Install Certbot

On Ubuntu/Debian:

sudo apt update
sudo apt install certbot python3-certbot-nginx

On RHEL/CentOS/Fedora:

sudo dnf install certbot python3-certbot-nginx

Step 2: Confirm Your Site Is Reachable

Before requesting a certificate, Let’s Encrypt needs to verify domain ownership by reaching your server over HTTP. I make sure a basic server block already exists and DNS is pointed correctly:

dig +short example.com

This should return your server’s public IP address.

Step 3: Run Certbot with the Nginx Plugin

sudo certbot --nginx -d example.com -d www.example.com

Certbot automatically:

  1. Verifies domain ownership via an HTTP challenge
  2. Obtains the certificate from Let’s Encrypt
  3. Edits the Nginx configuration to add the ssl_certificate and ssl_certificate_key directives
  4. Optionally sets up the HTTP-to-HTTPS redirect (Certbot will prompt: “Redirect HTTP traffic to HTTPS?” — I always choose yes)

After it finishes, Certbot shows a confirmation with the certificate’s expiration date, typically 90 days out.

Step 4: Verify the Configuration

Certbot modifies the server block automatically, producing something like:

server {
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    }

    if ($host = example.com) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name example.com www.example.com;
    return 404;
}

I always add http2 on; manually if Certbot doesn’t include it, since HTTP/2 isn’t enabled by default in every Certbot version:

listen 443 ssl;
http2 on;

Step 5: Set Up Automatic Renewal

Let’s Encrypt certificates are valid for 90 days, so renewal has to be automated — I never want to be manually renewing certs every three months. Certbot installs a systemd timer or cron job automatically, but I always verify it:

sudo systemctl status certbot.timer

I test the renewal process without actually renewing (dry run):

sudo certbot renew --dry-run

If that completes without errors, automatic renewal is properly configured, and Certbot will silently renew certificates as they approach expiration, reloading Nginx afterward.

Method 2: Manually Installing a Certificate from a Paid CA

If I’ve purchased a certificate from a commercial CA (DigiCert, Sectigo, etc.) instead of using Let’s Encrypt, the process is a bit more manual.

Step 1: Generate a Private Key and CSR

sudo openssl req -new -newkey rsa:2048 -nodes \
  -keyout /etc/nginx/ssl/example.com.key \
  -out /etc/nginx/ssl/example.com.csr

I submit the resulting .csr file to the CA through their portal, and they issue a certificate (and usually an intermediate/chain certificate).

Step 2: Combine Certificate and Chain

Most CAs require the server certificate and intermediate chain combined into one file:

cat example_com.crt intermediate.crt > /etc/nginx/ssl/example.com.chained.crt

Step 3: Configure Nginx

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/example.com.chained.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    root /var/www/example.com;
    index index.html;
}

Hardening the SSL/TLS Configuration

Once a certificate is installed, whether from Let’s Encrypt or a paid CA, I always harden the protocol and cipher settings rather than leaving Nginx defaults in place:

ssl_protocols TLSv1.2 TLSv1.3;
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;

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

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;

I explicitly exclude TLSv1.0 and TLSv1.1 since both are deprecated and considered insecure by every modern security standard, including PCI-DSS compliance requirements.

Complete Example Configuration

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    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_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    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;

    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

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

    root /var/www/example.com;
    index index.html;

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

Testing Your SSL/TLS Setup

sudo nginx -t
sudo systemctl reload nginx
curl -Iv https://example.com/ 2>&1 | grep -E "SSL|subject|issuer"

For a thorough external check, I run the domain through Qualys SSL Labs’ SSL Server Test, which grades the configuration (A+ is the target) and flags any weak protocols, ciphers, or missing headers.

Troubleshooting Common Issues

Certbot fails the HTTP-01 challenge. This almost always means port 80 isn’t reachable from the internet — check the firewall/security group, and confirm DNS actually resolves to the correct server.

sudo ufw allow 80
sudo ufw allow 443

nginx -t fails after Certbot edits the config. Rare, but happens if a previous manual edit conflicts with Certbot’s automatic changes. I review the diff and resolve duplicate server_name or listen directives.

Certificate renews but site still shows the old, expired certificate. Certbot’s renewal hook should reload Nginx automatically, but I verify:

sudo certbot renew --dry-run
sudo systemctl reload nginx

If Nginx isn’t reloading after renewal, I check /etc/letsencrypt/renewal/example.com.conf for the correct [[webroot_map]] and deploy hook settings.

Mixed content warnings after enabling HTTPS. Same root cause as with redirects — hardcoded http:// references in HTML/CSS/JS need to be updated to protocol-relative or https:// URLs.

Security Considerations

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;

Performance Tips

Real-World Use Cases

Best Practices

Wrapping Up

Enabling SSL/TLS on Nginx today is mostly a solved problem thanks to Let’s Encrypt and Certbot — what used to be an expensive, manual process is now a single command that also sets up automatic renewal. The part that still takes real judgment is the hardening afterward: choosing the right protocols, ciphers, and headers, and actually verifying renewal works before you need it. Get through those steps once, and HTTPS becomes something you never have to think about again.

Exit mobile version