How to Configure SSL/TLS on Nginx: A Complete HTTPS Guide for VPS
HTTPS is no longer optional. Search engines penalize HTTP sites, browsers show security warnings, and users expect the padlock. But there’s a significant gap between a basic SSL certificate installation and a properly hardened TLS configuration that earns an A+ rating from security scanners.
This guide covers everything from installing a free Let’s Encrypt certificate to configuring TLS 1.3, strong cipher suites, HSTS, OCSP stapling, and security headers — a complete SSL/TLS hardening guide for Nginx on Ubuntu VPS.
SSL vs TLS: Terminology Note
SSL (Secure Sockets Layer) is the older protocol — technically deprecated and insecure. TLS (Transport Layer Security) is its modern replacement. Despite this, “SSL certificate” remains the common term for the certificate file itself, while the connection protocol should always be TLS 1.2 or 1.3 in 2025.
Part 1: Install a Free Let’s Encrypt Certificate
Prerequisites
- Ubuntu VPS with Nginx installed
- Domain name with DNS A record pointing to your VPS IP
- Port 80 open in your firewall
Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx -y
Issue a certificate for your domain
# Single domain
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Multiple separate domains at once
sudo certbot --nginx \
-d yourdomain.com -d www.yourdomain.com \
-d api.yourdomain.com \
-d admin.yourdomain.com
Certbot automatically:
- Verifies domain ownership via HTTP challenge
- Downloads and installs the certificate
- Updates your Nginx config with HTTPS settings
- Sets up HTTP → HTTPS redirect
- Configures automatic renewal via systemd timer
Test auto-renewal
sudo certbot renew --dry-run
# Check renewal timer is active
sudo systemctl status certbot.timer
Wildcard certificates (covers all subdomains)
# Requires DNS challenge — add TXT record to your DNS
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d "*.yourdomain.com" \
-d yourdomain.com
Part 2: Understanding the Certificate Files
After Certbot runs, certificates are stored in /etc/letsencrypt/live/yourdomain.com/:
| File | Contents | Use in Nginx |
|---|---|---|
fullchain.pem |
Your cert + intermediate CA chain | ssl_certificate |
privkey.pem |
Your private key | ssl_certificate_key |
cert.pem |
Your certificate only | OCSP stapling |
chain.pem |
Intermediate CA chain only | OCSP stapling |
Always use fullchain.pem for ssl_certificate — never just cert.pem.
Part 3: Production Nginx HTTPS Configuration
Certbot’s auto-generated config is basic. Here’s a hardened, production-ready configuration:
sudo nano /etc/nginx/sites-available/yourdomain.com
# ── HTTP → HTTPS redirect ─────────────────────────────────
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# ACME challenge for certificate renewal
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect everything else to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# ── HTTPS server ─────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # Enable HTTP/2
server_name yourdomain.com www.yourdomain.com;
root /var/www/yourdomain.com;
index index.php index.html;
# ── SSL Certificate ───────────────────────────────────
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# ── TLS Protocol Versions ─────────────────────────────
# Only allow TLS 1.2 and 1.3 (disable 1.0 and 1.1)
ssl_protocols TLSv1.2 TLSv1.3;
# ── Cipher Suites ─────────────────────────────────────
# Strong ciphers only — forward secrecy for all
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:DHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off; # Let client choose (better for TLS 1.3)
# ── DH Parameters (for DHE cipher suites) ────────────
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# ── Session Cache (performance) ───────────────────────
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # Disable for better forward secrecy
# ── OCSP Stapling (faster certificate validation) ─────
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# ── Security Headers ──────────────────────────────────
# HSTS — tell browsers to always use HTTPS (6 months)
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# XSS protection (legacy browsers)
add_header X-XSS-Protection "1; mode=block" always;
# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Security Policy (customize for your app)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# Hide Nginx version
server_tokens off;
# ── Your application config ───────────────────────────
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
location ~ /\.(ht|git|env) { deny all; }
}
Part 4: Generate Strong DH Parameters
sudo mkdir -p /etc/nginx/ssl
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# This takes 1–5 minutes
# For maximum security, use 4096 bits (takes 10–30 minutes)
sudo nginx -t && sudo systemctl reload nginx
Part 5: Test Your SSL Configuration
SSL Labs test (comprehensive)
Visit ssllabs.com/ssltest and enter your domain. With the configuration above, you should score A+.
Command-line tests
# Check which TLS versions are supported
openssl s_client -connect yourdomain.com:443 -tls1 # Should fail (TLS 1.0 disabled)
openssl s_client -connect yourdomain.com:443 -tls1_2 # Should succeed
openssl s_client -connect yourdomain.com:443 -tls1_3 # Should succeed
# Check cipher suite
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
# Check security headers
curl -I https://yourdomain.com | grep -i "strict-transport\|x-frame\|x-content"
Part 6: HSTS Preloading
HSTS (HTTP Strict Transport Security) tells browsers to never connect via HTTP — even before the first request. HSTS Preloading goes further: it submits your domain to a browser preload list, so users are protected even on their very first visit.
# Current HSTS header (already in our config):
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;
To add your domain to the browser preload list:
- Ensure HTTPS is working perfectly for all subdomains
- Visit hstspreload.org
- Submit your domain
- Wait for inclusion in Chrome/Firefox preload lists (weeks to months)
⚠️ Warning: HSTS preloading is difficult to undo. Only submit if you’re committed to HTTPS permanently for all subdomains.
Part 7: SSL for Multiple Domains (SNI)
Server Name Indication (SNI) allows one IP address to serve SSL certificates for multiple domains:
# Each domain gets its own server block with its own certificate
server {
listen 443 ssl http2;
server_name site1.com www.site1.com;
ssl_certificate /etc/letsencrypt/live/site1.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site1.com/privkey.pem;
# ... include ssl params from a shared file
}
server {
listen 443 ssl http2;
server_name site2.com www.site2.com;
ssl_certificate /etc/letsencrypt/live/site2.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site2.com/privkey.pem;
# ...
}
Shared SSL parameters snippet
sudo nano /etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
Include in each server block:
include snippets/ssl-params.conf;
Part 8: Certificate Monitoring and Renewal Alerts
nano ~/check-ssl-expiry.sh
#!/bin/bash
DOMAINS=("yourdomain.com" "api.yourdomain.com")
ALERT_DAYS=14
EMAIL="you@youremail.com"
for DOMAIN in "${DOMAINS[@]}"; do
EXPIRY=$(echo | openssl s_client -servername $DOMAIN \
-connect $DOMAIN:443 2>/dev/null | \
openssl x509 -noout -dates 2>/dev/null | \
grep "notAfter" | cut -d= -f2)
EXPIRY_DATE=$(date -d "$EXPIRY" +%s)
TODAY=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_DATE - $TODAY) / 86400 ))
if [ $DAYS_LEFT -lt $ALERT_DAYS ]; then
echo "⚠️ $DOMAIN certificate expires in $DAYS_LEFT days!" | \
mail -s "SSL Expiry Alert: $DOMAIN" $EMAIL
else
echo "✅ $DOMAIN: $DAYS_LEFT days remaining"
fi
done
chmod +x ~/check-ssl-expiry.sh
crontab -e
# 0 8 * * * /bin/bash /root/check-ssl-expiry.sh
Common SSL Issues and Fixes
Mixed content warnings
HTTP resources (images, scripts) on an HTTPS page trigger browser warnings. Fix by searching and replacing http://yourdomain.com with https:// in your database (use Better Search Replace plugin for WordPress) or adding this to Nginx:
add_header Content-Security-Policy "upgrade-insecure-requests;" always;
Certificate renewal fails
Certbot needs port 80 open for HTTP challenge. If you’ve blocked port 80, use DNS challenge instead or temporarily open the port during renewal:
sudo ufw allow 80/tcp
sudo certbot renew
sudo ufw delete allow 80/tcp
SSL Labs score is A, not A+
Common reasons for missing the A+:
- HSTS header missing or max-age too short (needs 6+ months)
- TLS 1.0 or 1.1 still enabled
- Weak DH parameters (use 2048-bit minimum)
Final Thoughts
A properly configured Nginx SSL setup isn’t just about the padlock — it’s about forward secrecy, modern cipher suites, OCSP stapling, and security headers that protect your users at every level. The configuration in this guide consistently earns A+ scores on SSL Labs and passes strict security header checks.