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
- A registered domain name pointed at your server’s public IP (an A record in DNS)
- Nginx installed and running, listening on port 80
- Root or sudo access
- Port 80 and 443 open in your firewall/security group
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:
- Verifies domain ownership via an HTTP challenge
- Obtains the certificate from Let’s Encrypt
- Edits the Nginx configuration to add the
ssl_certificateandssl_certificate_keydirectives - 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
- Never disable certificate verification anywhere in your stack “temporarily” — these settings have a way of becoming permanent.
- Rotate and renew certificates well before expiration; Certbot’s default renewal window (30 days before expiry) provides comfortable buffer, but I still monitor certificate expiration with an external uptime/SSL monitoring tool as a backup.
- Enable OCSP stapling to speed up and improve the privacy of certificate revocation checks:
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
- Keep Nginx and OpenSSL updated — TLS implementation vulnerabilities do surface periodically, and patching promptly matters.
Performance Tips
- Enable session resumption (
ssl_session_cache) to avoid full handshakes on repeat visits. - Use ECDSA certificates alongside or instead of RSA where your CA supports it — ECDSA handshakes are computationally cheaper.
- Pair TLS with HTTP/2 (covered in a separate article) for compounding performance gains.
- OCSP stapling reduces client-side latency by avoiding a separate revocation-check round trip.
Real-World Use Cases
- Standard requirement for any production website, e-commerce store, or API handling user data.
- Compliance requirements (PCI-DSS, HIPAA, GDPR) that mandate encryption in transit.
- SEO — search engines factor HTTPS into ranking signals and mark HTTP sites as insecure in search results.
- Enabling modern web platform features (service workers, geolocation, secure cookies) that browsers restrict to HTTPS-only contexts.
Best Practices
- Use Let’s Encrypt with Certbot for automatic issuance and renewal unless you have a specific business reason for a paid CA (extended validation, organizational branding, or a specific compliance requirement).
- Always harden
ssl_protocolsandssl_ciphersbeyond Nginx’s defaults. - Set up and actually test automatic renewal — don’t assume the cron job works without a dry run.
- Add HSTS only once you’re confident the HTTPS setup is fully stable across all subdomains.
- Regularly test your live configuration with Qualys SSL Labs and aim for an A or A+ grade.
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.