Skip to content

Deploy a renewed certificate

Three routes, in order of practicality:

From the Console. Open the certificate and download it. Fine for a handful of renewals, obviously not for a fleet.

From the CA directly. Your CA account has its own API and its own download endpoint. If you already hold credentials there, this is scriptable today.

From ACME, if the certificate is ACME-issued. An ACME client on the serving host does issuance and installation in one step, and the private key never moves. For Let’s Encrypt and similar, this is the path with the fewest moving parts - NextPKI then serves as the inventory and the approval gate rather than the delivery mechanism.

Pick the third where you can. It is the only one of the three that is both automated and keeps the key on the host.

Whatever gets you the file, the installation order is what prevents self-inflicted outages:

  1. Generate the key and CSR on the serving host. Never move a private key between hosts.
  2. Write the new key beside the old one, not over it.
  3. Obtain the certificate.
  4. Verify the certificate matches the key before touching live configuration.
  5. Test the configuration.
  6. Reload, do not restart.
  7. Only then promote the new key over the old.

Step 4 is the one people skip. It is two commands:

Terminal window
# The moduli must match, or the server will not start with this pair.
key_hash=$(openssl pkey -in server.key.new -pubout -outform der | openssl sha256)
crt_hash=$(openssl x509 -in server.crt.new -pubkey -noout -outform pem \
| openssl pkey -pubin -pubout -outform der | openssl sha256)
[ "$key_hash" = "$crt_hash" ] || { echo "key and certificate do not match" >&2; exit 1; }

Also confirm the chain is complete. A certificate that validates on your laptop and fails on a server is almost always a missing intermediate:

Terminal window
openssl verify -untrusted chain.pem server.crt.new
Terminal window
install -m 640 -o root -g root server.crt.new /etc/ssl/certs/example.com.pem
install -m 600 -o root -g root server.key.new /etc/ssl/private/example.com.key
nginx -t && systemctl reload nginx

nginx -t before reload is the whole safety net. Reload - not restart - keeps existing connections and avoids a gap in service.

Concatenate the leaf and the intermediates into ssl_certificate; nginx does not build the chain for you, and a bare leaf is the classic cause of “works in Chrome, fails in Java”.

SSLCertificateFile /etc/ssl/certs/example.com.pem
SSLCertificateKeyFile /etc/ssl/private/example.com.key
SSLCertificateChainFile /etc/ssl/certs/example.com.chain.pem
Terminal window
apachectl configtest && systemctl reload apache2

Traefik watches its dynamic configuration, so replacing the files is enough - no reload command. Write to a temporary path and rename, so Traefik never observes a half-written file:

Terminal window
install -m 600 server.key.new /etc/traefik/certs/.example.com.key.tmp
install -m 644 server.crt.new /etc/traefik/certs/.example.com.crt.tmp
mv /etc/traefik/certs/.example.com.key.tmp /etc/traefik/certs/example.com.key
mv /etc/traefik/certs/.example.com.crt.tmp /etc/traefik/certs/example.com.crt

mv within a filesystem is atomic; cp is not. Copying directly over a file that is being read is how you get a few seconds of TLS errors.

Update the secret in place and let the ingress controller pick it up:

Terminal window
kubectl create secret tls example-com-tls \
--cert=server.crt.new --key=server.key.new \
--dry-run=client -o yaml | kubectl apply -f -

--dry-run=client | apply is an update rather than a delete-and-recreate, so there is no window where the secret does not exist.

Whether the controller reloads without help depends on the controller. ingress-nginx watches secrets and reloads by itself. Others need a rollout:

Terminal window
kubectl rollout restart deployment/my-app

For a fleet, the right answer is cert-manager with ACME rather than pushing secrets from outside - see below.

Trust what a client sees, not what your file system says:

Terminal window
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -fingerprint -sha256

Compare the fingerprint with the one NextPKI reports for the new certificate. If they differ, something is still serving the old file - an unreloaded worker, a second node, or a CDN holding it.

Your sensors will observe the change on their next sweep, which closes the loop in the inventory. If a host still shows the old fingerprint a sweep later, that host did not get the update.

An internal ACME server against the private CA is planned, and for internal certificates it removes this entire page: the client on the host requests, receives and installs, and the key never exists anywhere else. That is the shape worth designing towards.