aethercert
Documentation

Replace an SSL Certificate on NGINX

Replace the SSL certificate on NGINX: build and check the full-chain PEM, install it with a rollback path, test with nginx -t, reload without dropping connections, also in Docker.

NGINX reads a PEM certificate chain and a private key from the paths in ssl_certificate and ssl_certificate_key. Replacing the files is not enough: the configuration must pass nginx -t, and the master process must reload it.

Before you begin

  • You need three PEM files from your CA (names vary): the server certificate (here leaf.pem), the intermediate certificate(s) (intermediates.pem) and the private key (private.key, unencrypted). Do not include the root CA.

  • Find every certificate NGINX actually uses - nginx -T prints the complete loaded configuration, not just sites-enabled:

    sudo nginx -T | grep -E 'server_name|ssl_certificate'
  • Keep the current files: the procedure below installs the new ones into a new directory, so the old ones stay in place for a rollback.

  • With several servers behind a load balancer, take one out, update it, put it back.

Step-by-step certificate replacement

Run the commands as root (or with sudo).

  1. Build the full chain - server certificate first - and check it:

    cat leaf.pem intermediates.pem > fullchain.pem
    grep -c 'BEGIN CERTIFICATE' fullchain.pem
    openssl x509 -in fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
    openssl verify -untrusted intermediates.pem leaf.pem
    openssl pkey -in private.key -pubout | sha256sum
    openssl x509 -in leaf.pem -noout -pubkey | sha256sum

    The count must equal the number of certificates you concatenated - if a file had no final newline, two certificates merge into one broken block. openssl verify must print leaf.pem: OK (it uses the system's trusted roots). The two sha256sum values must be identical; otherwise key and certificate do not belong together.

  2. Install into a new, versioned directory and switch a symlink to it:

    install -d -m 0700 /etc/nginx/tls/example-2026
    install -m 0644 fullchain.pem /etc/nginx/tls/example-2026/fullchain.pem
    install -m 0600 private.key   /etc/nginx/tls/example-2026/private.key
    ln -sfn /etc/nginx/tls/example-2026 /etc/nginx/tls/example-current

    The NGINX master process runs as root and reads the key before starting the workers, so a root-only key (0600) is correct. If you run NGINX entirely as a non-root user, that user must be able to read the key instead.

  3. Make sure the server block points at the symlink:

    ssl_certificate     /etc/nginx/tls/example-current/fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/example-current/private.key;
  4. Test, then reload:

    nginx -t && systemctl reload nginx
    journalctl -u nginx --since "5 minutes ago"

    If nginx -t fails, nothing has changed for the running server - point the symlink back and investigate. A reload drops no connections: NGINX starts new workers with the new certificate, and the old workers finish their open connections (long-lived ones such as WebSockets can keep an old worker alive for a while). Check the journal or /var/log/nginx/error.log - systemctl is-active alone does not show whether the reload worked.

NGINX in a container

  • Mount the certificate directory into the container, not single files. Replacing a file on the host usually creates a new file (new inode), and a single-file bind mount keeps showing the old one.

  • Test and reload inside the container:

    docker exec <container> nginx -t
    docker exec <container> nginx -s reload
  • See also the Docker container guide.

Product-specific considerations

Key mismatch: nginx -t reporting SSL_CTX_use_PrivateKey ... key values mismatch means the first certificate in the chain file does not belong to the key. Either the wrong key was installed, or the chain file does not start with the server certificate. Compare the two hashes from step 1 and check the order.

SNI makes the tested hostname matter: several server blocks can use different certificates on the same address. SELinux or AppArmor can block access to a new path even when the file permissions look correct - on SELinux systems run restorecon -Rv /etc/nginx/tls after creating new files.

Verify the new certificate

openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null
openssl x509 -in leaf.pem -noout -fingerprint -sha256

Check subject and SAN, issuer, expiry date and that the intermediate is sent, and compare the served certificate's fingerprint with the one of leaf.pem. Test through every load balancer, too.

Rollback

Point the symlink back to the previous directory, test and reload:

ln -sfn /etc/nginx/tls/example-2025 /etc/nginx/tls/example-current
nginx -t && systemctl reload nginx

Then check the served certificate again. This requires the old directory to still exist - delete it only after the new certificate has been verified.

Common problems

  • Key and certificate do not belong together.
  • Intermediates are missing, or come before the server certificate.
  • Two certificates merged because a file had no final newline.
  • NGINX was reloaded without nginx -t.
  • The test request was sent without SNI and hit the default server.
  • In a container, a single-file bind mount still shows the old file.

Automating certificate replacement with aethercert

The aethercert NGINX target writes the certificate chain and private key, applies private-key ownership/mode handling based on the NGINX configuration, and runs the configured reload command.

See Deploy targets for the fields this preset takes, what it needs on the host, and how far it has been verified.

Sources

On this page