This guide installs an SSL certificate on Nginx with a secure, modern configuration, then covers renewal. It works for Let's Encrypt and commercial certificates alike.
Quickest path: Let's Encrypt with Certbot
If you don't have a certificate yet, Certbot can get one and configure Nginx for you:
sudo apt install certbot python3-certbot-nginx # Debian/Ubuntu; use dnf on RHEL/Fedora
sudo certbot --nginx -d example.com -d www.example.com
Certbot edits your server block and sets up automatic renewal. See Renew Let's Encrypt with Certbot to make sure renewal keeps working. The manual steps below are for commercial certificates, or if you prefer to manage the configuration yourself.
1. Put the certificate files in place
You need two files:
- The full chain: your certificate followed by the intermediate certificate(s). With a commercial certificate, combine them:
cat your_domain.crt ca_bundle.crt > fullchain.crt. Missing the intermediate is the classic mistake; see Fix an incomplete certificate chain. - The private key, readable only by root:
sudo mkdir -p /etc/nginx/ssl
sudo cp fullchain.crt /etc/nginx/ssl/example.com.crt
sudo cp example.com.key /etc/nginx/ssl/example.com.key
sudo chmod 600 /etc/nginx/ssl/example.com.key
2. Configure the server blocks
# Send all plain-HTTP visitors to HTTPS
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;
http2 on; # Nginx 1.25.1+; on older versions use "listen 443 ssl http2;"
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt; # full chain
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Ask browsers to use HTTPS only, for a year (add once HTTPS works everywhere on the domain)
add_header Strict-Transport-Security "max-age=31536000" always;
root /var/www/example.com;
}
server_name must match every name on the certificate you want to serve. If a name is missing from the certificate, visitors get a name-mismatch warning.
For a cipher list matched to your Nginx and OpenSSL versions, use the Mozilla SSL Configuration Generator with the Intermediate profile.
3. Test and reload
Always test before reloading; a typo would otherwise take the site down:
sudo nginx -t
sudo systemctl reload nginx
4. Check the result
Run the SSL checker on your domain. You should see a complete, trusted chain, a hostname match and only TLS 1.2 and 1.3 accepted: grade A.
Renewing
For a renewed certificate, replace the two files (keep the same paths), then test and reload again:
sudo nginx -t && sudo systemctl reload nginx
Nginx keeps serving the old certificate until it reloads, so the reload is essential. With Certbot, add a deploy hook so it happens automatically after each renewal (details).
About OCSP stapling
ssl_stapling on; makes Nginx include the issuer's revocation status in the handshake. It only works for certificates whose issuer runs an OCSP service. Let's Encrypt no longer does, so leave stapling off for Let's Encrypt certificates (Nginx logs warnings otherwise).
Troubleshooting
nginx -tfails with "cannot load certificate": wrong path or permissions, or the files are in the wrong format (they must be PEM text starting with-----BEGIN).- "key values mismatch": the key doesn't belong to the certificate. Check that the outputs of
openssl x509 -noout -modulus -in cert.crt | openssl md5andopenssl rsa -noout -modulus -in key.key | openssl md5match (for RSA keys). - Works in the browser, fails in apps: the intermediate is missing. See Fix an incomplete certificate chain.
CertHalo checks your Nginx sites on a schedule and emails you before the certificate expires. Monitor 3 certificates free.