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.
- Platforms
- Linux
- nginx
- Level
- Practitioner · 4 steps
On this page
Before you start
- Your server certificate (the leaf certificate) and the CA’s intermediate certificate or certificates, in PEM format.
- The 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 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 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.
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:
openssl crl2pkcs7 -nocrl -certfile /etc/ssl/example/fullchain.pem | openssl pkcs7 -print_certs -noout
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):
openssl x509 -in /etc/ssl/example/fullchain.pem -noout -pubkey | openssl sha256
sudo openssl pkey -in /etc/ssl/example/privkey.pem -pubout | openssl sha256
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/:
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.
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
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
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:
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):
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:
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:
If something goes wrong
-
nginx -tpasses, but clients sayunable to verify the first certificateorunable to get local issuer certificate. The intermediate is missing.nginx -tdoes not check the chain: with the leaf alone our test passednginx -t, thenopenssl s_clientshowed one certificate andVerify return code: 21 (unable to verify the first certificate), and curl failed. Rebuildfullchain.pem(step 1). -
key values mismatch. nginx refuses to start:[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
serverblock answers for that name. Runsudo nginx -T | grep -E 'server_name|ssl_certificate'to see the configuration nginx actually loaded. -
NET::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. -
NET::ERR_CERT_DATE_INVALID. The certificate has expired or is not yet valid, or the client’s clock is wrong.
Why this works
During the 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 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, The TLS 1.3 handshake and Anatomy of an X.509 certificate.
Go deeper
- nginx: ngx_http_ssl_module: every TLS directive, with defaults and versions.
- nginx: configuring HTTPS servers: chains, name-based HTTPS servers and SNI.
- RFC 9846, 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.