# 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.

Source: https://certguru.io/platforms/nginx/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

## Step 1 — build the chain file

*Leaf first, then intermediates*

```bash
cat certificate.crt intermediate.crt > /etc/ssl/certs/fullchain.crt
```

> **This is the step people skip**
>
> nginx has no chain directive. If `ssl_certificate` points at a file containing only your server
>   certificate, clients cannot build a path to a trusted root. It works in your browser — which
>   caches intermediates from other sites — and fails for curl, mobile apps, and anything with a fresh
>   trust store. See
>   [unable to get local issuer certificate](/errors/unable-to-get-local-issuer-certificate/).

## Step 2 — the configuration

*/etc/nginx/sites-available/example.com*

```nginx
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;
}
```

| 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

```bash
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

```bash
nginx -t
```

Expected output:

```
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

```bash
systemctl reload nginx
```

> **Reload, do not restart**
>
> `reload` keeps existing connections alive while workers are replaced. `restart` drops them.

## Step 5 — verify from outside

This is the check that matters, because it tests what clients actually receive:

*Chain complete and validating?*

```bash
openssl s_client -connect example.com:443 -servername example.com /dev/null \
  | grep "Verify return code"
```

Expected output:

```
Verify return code: 0 (ok)
```

*Is the intermediate actually being sent?*

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts /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](/openssl/check-key-matches-certificate/) |
| Works in Chrome, fails in curl | Missing intermediate | Concatenate the chain |
| `PEM_read_bio_X509_AUX` failure | File is DER, not PEM | [Convert it](/convert/der-to-pem/) |
| nginx will not start, no error | Encrypted key waiting on a passphrase | [Remove the passphrase](/openssl/remove-passphrase-from-key/) |
| 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:

```nginx
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

```bash
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](/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

- [nginx: configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) — ssl_certificate and why the chain goes in the same file

## Related pages

- https://certguru.io/errors/unable-to-get-local-issuer-certificate/
- https://certguru.io/errors/key-values-mismatch/
- https://certguru.io/errors/wrong-version-number/
- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/platforms/certbot/
- https://certguru.io/platforms/apache/
- https://certguru.io/concepts/ocsp-vs-crl/
- https://certguru.io/convert/pfx-to-pem/
- https://certguru.io/platforms/haproxy/

--
CertGuru — https://certguru.io
