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.

Updated August 19, 2026 Verified against OpenSSL 3.5.7

Step 1 — build the chain file #

Leaf first, then intermediates
cat certificate.crt intermediate.crt > /etc/ssl/certs/fullchain.crt

Step 2 — the configuration #

/etc/nginx/sites-available/example.com
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

PlaceholderWhat to put there
ssl_certificateThe full chain — leaf plus intermediates. Not the bare certificate.
ssl_certificate_keyThe private key, chmod 600 and owned by root.
ssl_trusted_certificateIntermediates only, for OCSP stapling. Different file from the one above.
http2 onReplaces 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:

Chain complete and validating?
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)
Is the intermediate actually being sent?
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_certificate served a different purpose and an old ssl_certificate_chain was 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 -t checks 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 600 and 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