Install a TLS certificate on nginx
nginx takes exactly two certificate directives, `ssl_certificate` and `ssl_certificate_key`, and there is **no separate directive for the chain**. The intermediates must be concatenated into the same file as your server certificate, leaf first — `cat certificate.crt intermediate.crt > fullchain.crt`. Pointing `ssl_certificate` at the bare server certificate is the single most common nginx TLS mistake: the config tests clean, the site loads in your browser, and it fails for curl, mobile apps and monitoring.
Step 1 — build the chain file #
cat certificate.crt intermediate.crt > /etc/ssl/certs/fullchain.crt Step 2 — the configuration #
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/fullchain.crt;
ssl_certificate_key /etc/ssl/private/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling — saves clients a round trip to the CA.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/chain.crt;
add_header Strict-Transport-Security "max-age=63072000" always;
root /var/www/example.com;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
} Replace these values
| Placeholder | What to put there |
|---|---|
ssl_certificate | The full chain — leaf plus intermediates. Not the bare certificate. |
ssl_certificate_key | The private key, chmod 600 and owned by root. |
ssl_trusted_certificate | Intermediates only, for OCSP stapling. Different file from the one above. |
http2 on | Replaces the old listen ... http2 form, deprecated in nginx 1.25.1. |
Step 3 — permissions #
chmod 600 /etc/ssl/private/private.key
chown root:root /etc/ssl/private/private.key
chmod 644 /etc/ssl/certs/fullchain.crt Step 4 — test before reloading #
nginx -t Expected output
nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx Step 5 — verify from outside #
This is the check that matters, because it tests what clients actually receive:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep "Verify return code" Expected output
Verify return code: 0 (ok)
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -c "BEGIN CERTIFICATE" Expected output
2
A count of 1 means you skipped step 1.
Common nginx TLS failures #
| Symptom | Cause | Fix |
|---|---|---|
key values mismatch on reload |
Key and certificate are different pairs | Check the pair |
| Works in Chrome, fails in curl | Missing intermediate | Concatenate the chain |
PEM_read_bio_X509_AUX failure |
File is DER, not PEM | Convert it |
| nginx will not start, no error | Encrypted key waiting on a passphrase | Remove the passphrase |
| Wrong certificate served | SNI not configured, or wrong server_name |
Check server_name matches |
ssl_stapling ignored warning |
ssl_trusted_certificate missing or wrong |
Point it at intermediates only |
Serving several sites #
Each server block gets its own certificate, selected by SNI:
server {
listen 443 ssl;
server_name a.example.com;
ssl_certificate /etc/ssl/certs/a.fullchain.crt;
ssl_certificate_key /etc/ssl/private/a.key;
}
server {
listen 443 ssl;
server_name b.example.com;
ssl_certificate /etc/ssl/certs/b.fullchain.crt;
ssl_certificate_key /etc/ssl/private/b.key;
} The first block for a given listen address is the default when a client sends no SNI — which is
why testing without -servername returns a misleading certificate.
Starting from a PFX #
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out /etc/ssl/private/private.key
openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out /tmp/certificate.crt
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out /tmp/chain.crt
cat /tmp/certificate.crt /tmp/chain.crt > /etc/ssl/certs/fullchain.crt See convert PFX to PEM.
Frequently asked questions
- Why does nginx have no ssl_certificate_chain directive?
- It had one —
ssl_client_certificateserved a different purpose and an oldssl_certificate_chainwas removed long ago. nginx expects the chain inside the certificate file, which is why the concatenation step is mandatory rather than optional. - nginx -t passes but browsers still complain — why?
nginx -tchecks syntax and that the key matches the certificate. It does not check whether the chain is complete. Test from outside with openssl s_client instead.- Does the private key need to be readable by the nginx worker user?
- No. The master process reads it as root at startup and then drops privileges. Keep it
chmod 600and owned by root. - Can nginx use an encrypted private key?
- Yes, with
ssl_password_file. It stores the passphrase in plaintext beside the key, so the security benefit is minimal, but it does allow an unattended restart.
References
- nginx: configuring HTTPS servers — ssl_certificate and why the chain goes in the same file