<!-- https://pkizerotohero.com/guides/install-certificate-nginx/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Install a TLS certificate in nginx: build fullchain.pem, set ssl_certificate and ssl_certificate_key, test, reload and verify the chain from outside.

Source: https://pkizerotohero.com/guides/install-certificate-nginx/ · Last reviewed: 2026-10-20

How-to guide · Install on a server

# How to install a certificate in nginx

**In one sentence:**

nginx serves your certificate and its full chain on port 443, with a key that matches, and you have proved it from the outside with OpenSSL and curl.

Task

[Install on a server](https://pkizerotohero.com/guides/#task-install)

Platforms

- Linux
- nginx

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Developer](https://pkizerotohero.com/start/#developer)

Level

Practitioner · 4 steps

## Before you start

- Your server certificate (the [leaf certificate](https://pkizerotohero.com/handbook/glossary/#leaf-certificate)) and the CA’s [intermediate certificate](https://pkizerotohero.com/handbook/glossary/#intermediate-certificate) or certificates, in PEM format.
- The [private key](https://pkizerotohero.com/handbook/glossary/#private-key) you created with the CSR. It never leaves the server; never paste it into a website.
- Root access, and port 443 open in the firewall.
- If you use Let’s Encrypt, [certbot](https://pkizerotohero.com/guides/certbot-lets-encrypt/) can write these files and the configuration for you.

Why it matters: nginx sends exactly the certificates you give it. Leave out the intermediate and most browsers still work, but curl on Linux, Java, Python, Node.js and many phones fail.

## Steps

### 1. Build the full chain file

nginx wants the leaf **first**, then the intermediates, in one file. The [nginx documentation](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate) says “the primary certificate comes first, then the intermediate certificates”. Leave the root out: clients must already have it. See [how to build the full chain file](https://pkizerotohero.com/guides/build-fullchain/).

```bash
sudo mkdir -p /etc/ssl/example
cat www.example.com.crt intermediate.crt | sudo tee /etc/ssl/example/fullchain.pem > /dev/null
sudo install -m 600 -o root -g root www.example.com.key /etc/ssl/example/privkey.pem
```

Check the order. Each certificate’s issuer must be the subject of the next one:

```bash
openssl crl2pkcs7 -nocrl -certfile /etc/ssl/example/fullchain.pem | openssl pkcs7 -print_certs -noout
```

```text
subject=CN = intranet.example.com
issuer=C = BE, O = Example Corp, CN = Example Corp Issuing CA 1

subject=C = BE, O = Example Corp, CN = Example Corp Issuing CA 1
issuer=C = BE, O = Example Corp, CN = Example Corp Root CA
```

### 2. Check that the key matches the certificate

The two hashes must be identical ([key and certificate match](https://pkizerotohero.com/guides/check-key-matches-certificate/)):

```bash
openssl x509 -in /etc/ssl/example/fullchain.pem -noout -pubkey | openssl sha256
sudo openssl pkey -in /etc/ssl/example/privkey.pem -pubout | openssl sha256
```

```text
SHA2-256(stdin)= 4831d810716492fe84b3e7885848156700dce2d6c52325f917f7dec47de8ba1c
SHA2-256(stdin)= 4831d810716492fe84b3e7885848156700dce2d6c52325f917f7dec47de8ba1c
```

`openssl x509` reads only the first certificate in the file, which is why the leaf must come first.

### 3. Configure the server block

On Debian and Ubuntu, create `/etc/nginx/sites-available/example.com` and link it into `sites-enabled`; on other systems, use a file in `/etc/nginx/conf.d/`:

```nginx
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com example.com;

    ssl_certificate     /etc/ssl/example/fullchain.pem;
    ssl_certificate_key /etc/ssl/example/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    root /var/www/example;
}

server {
    listen 80;
    listen [::]:80;
    server_name www.example.com example.com;
    return 301 https://$host$request_uri;
}
```

`ssl_protocols TLSv1.2 TLSv1.3` is the default since nginx 1.27.3, but older packages still enable TLS 1.0 and 1.1: Ubuntu 24.04’s `/etc/nginx/nginx.conf` contains `ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;`. Change that line too, so every server block agrees. For HTTP/2, add `http2 on;` (nginx 1.25.1 and later) or `listen 443 ssl http2;` on older versions. Ciphers, HSTS and the rest are in [How to configure TLS securely](https://pkizerotohero.com/guides/tls-configuration/).

To serve an ECDSA and an RSA certificate side by side, repeat the `ssl_certificate` and `ssl_certificate_key` pair; nginx has supported that since 1.11.0. A key with a passphrase needs `ssl_password_file`.

### 4. Test the configuration and reload

```bash
sudo nginx -t
```

```text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

```bash
sudo systemctl reload nginx
```

A reload loads the new files without dropping connections. nginx reads certificates only at start and reload, so every renewal needs one; certbot and similar clients do it with a deploy hook.

## Check it worked

Look at what the server actually sends, from another machine if you can:

```bash
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | grep -E '^ [0-9] s:|^   i:|Verify return code'
```

On our test server (nginx 1.24 on Ubuntu 24.04, private test CA, so we added `-CAfile corp-root.crt`):

```text
 0 s:CN = intranet.example.com
   i:C = BE, O = Example Corp, CN = Example Corp Issuing CA 1
 1 s:C = BE, O = Example Corp, CN = Example Corp Issuing CA 1
   i:C = BE, O = Example Corp, CN = Example Corp Root CA
Verify return code: 0 (ok)
```

Two certificates, leaf first, and code 0. Then a plain request:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/
```

Paste the `-showcerts` output into the chain checker to see the same verdict, explained:

*Interactive tool: Chain checker (runs in the browser at [/tools/chain/](https://pkizerotohero.com/tools/chain/))*

## If something goes wrong

- **`nginx -t` passes, but clients say [`unable to verify the first certificate`](https://pkizerotohero.com/errors/openssl-verify-error-21/) or [`unable to get local issuer certificate`](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/).** The intermediate is missing. `nginx -t` does not check the chain: with the leaf alone our test passed `nginx -t`, then `openssl s_client` showed one certificate and `Verify return code: 21 (unable to verify the first certificate)`, and curl failed. Rebuild `fullchain.pem` (step 1).
    
- **`key values mismatch`.** nginx refuses to start:
    
    ```text
    [emerg] SSL_CTX_use_PrivateKey("/etc/ssl/example/privkey.pem") failed (SSL: error:05800074:x509 certificate routines::key values mismatch)
    ```
    
    Either the key belongs to another certificate, or the chain file starts with the intermediate. We got the same message both ways. Check the order and the key (steps 1 and 2).
    
- **`cannot load certificate … BIO_new_file() failed`.** The path is wrong or the file is missing.
    
- **The browser shows the old certificate.** You did not reload, or another `server` block answers for that name. Run `sudo nginx -T | grep -E 'server_name|ssl_certificate'` to see the configuration nginx actually loaded.
    
- **[`NET::ERR_CERT_COMMON_NAME_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-common-name-invalid/).** The certificate’s Subject Alternative Name does not cover the name in the address bar. Check it with the [hostname matcher](https://pkizerotohero.com/tools/hostname/).
    
- **[`NET::ERR_CERT_DATE_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/).** The certificate has expired or is not yet valid, or the client’s clock is wrong.
    

Note

Protect the key

The key file should be readable by root only (mode 600). The nginx master process runs as root and reads it before handing work to the unprivileged workers. Never commit it to a repository or copy it into a container image.

## Why this works

During the [TLS handshake](https://pkizerotohero.com/handbook/glossary/#tls-handshake), nginx sends the certificates from `ssl_certificate` in file order, and signs the handshake with the matching private key to prove it holds that key. The client then builds a [certificate chain](https://pkizerotohero.com/handbook/glossary/#certificate-chain) from the leaf through the intermediate to a root in its own store. If the intermediate is missing, the client cannot complete the chain unless it fetches or caches the intermediate itself, which browsers often do and other clients do not. See [Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/), [The TLS 1.3 handshake](https://pkizerotohero.com/handbook/tls-handshake/) and [Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/).

## Go deeper

- [nginx: ngx\_http\_ssl\_module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html): every TLS directive, with defaults and versions.
- [nginx: configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html): chains, name-based HTTPS servers and SNI.
- [RFC 9846, section 4.5.1](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1): TLS 1.3’s rule that “the sender’s certificate MUST come in the first CertificateEntry”, and why the root may be left out.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/install-certificate-nginx/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 13: Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/)
- [Chapter 15: The TLS 1.3 handshake](https://pkizerotohero.com/handbook/tls-handshake/)
- [Chapter 8: Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/)

### Error pages

- [verify error:num=21:unable to verify the first certificate](https://pkizerotohero.com/errors/openssl-verify-error-21/)
- [SSL certificate problem: unable to get local issuer certificate](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/)
- [NET::ERR\_CERT\_COMMON\_NAME\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-common-name-invalid/)
- [NET::ERR\_CERT\_DATE\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/)
- [UNABLE\_TO\_VERIFY\_LEAF\_SIGNATURE](https://pkizerotohero.com/errors/node-unable-to-verify-leaf-signature/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Certificate decoder](https://pkizerotohero.com/tools/decoder/): Paste a PEM certificate and read every field in plain words.
- [Hostname matcher](https://pkizerotohero.com/tools/hostname/): Test whether a name matches a certificate's SANs and wildcards.

### Topics

- [TLS and HTTPS](https://pkizerotohero.com/topics/tls/)

## More guides: Install on a server

- [How to build the full chain file your server needs](https://pkizerotohero.com/guides/build-fullchain/)
- [How to install a certificate in Apache httpd](https://pkizerotohero.com/guides/install-certificate-apache/)
- [How to install a certificate in IIS](https://pkizerotohero.com/guides/install-certificate-iis/)
- [How to install a certificate in Tomcat and other Java servers](https://pkizerotohero.com/guides/install-certificate-tomcat/)
