HTTPS and access

Install a certificate chain

Configure the certificate, private key, names, permissions, and chain required for a trusted HTTPS server.

8 minute lesson

~~~

TLS needs a certificate matching the requested hostname and a private key readable by the Nginx master process. In Nginx that’s two directives on the HTTPS server block:

server {
  listen 443 ssl;
  server_name shop.example.com;

  ssl_certificate     /etc/letsencrypt/live/shop.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;

  root /var/www/shop;
}

The word fullchain is the important one. Clients validate your certificate by walking a chain: your leaf certificate, signed by an intermediate, signed by a root the client already trusts. The client has the root but usually not the intermediate, so your server must send the leaf and the intermediates together. fullchain.pem is exactly that concatenation. Serve the complete certificate chain expected by clients, in that order — leaf first.

Protect the private key. It should be readable by root only:

sudo chmod 600 /etc/letsencrypt/live/shop.example.com/privkey.pem

That’s enough for Nginx because the master process runs as root and reads the key at startup and reload; the unprivileged workers never open the file themselves. A key that other users can read is a key you should consider leaked.

Verify the live handshake

Don’t trust the files — test what the server actually presents. Inspect the site with openssl s_client -connect host:443 -servername host:

openssl s_client -connect shop.example.com:443 -servername shop.example.com </dev/null
# Certificate chain
#  0 s:CN = shop.example.com
#    i:C = US, O = Let's Encrypt, CN = R11
#  1 s:C = US, O = Let's Encrypt, CN = R11
#    i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
# ...
# Verify return code: 0 (ok)

Read three things: the subject of certificate 0 matches your hostname, the chain includes the intermediate, and the verify return code is 0. The -servername flag matters because it drives SNI — without it a multi-site server may present a different certificate than browsers see. Check the expiry dates too, and repeat this test after every renewal or path change. None of this prints private key material, which is how it should be.

The classic failure is pointing ssl_certificate at the bare cert.pem instead of fullchain.pem. Your browser may still show a padlock, because browsers cache intermediates or fetch them on the fly. But curl, mobile apps, and API clients fail with unable to get local issuer certificate, and s_client reports Verify return code: 21. If browsers work while scripts fail, check the chain first.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →