Skip to content
Talk to our solutions team

Renew certificates before they expire

Certificates renewed before anything notices, on a schedule, with the renewal proved by an actual TLS handshake rather than by the absence of an error.

When you finish expiry is a thing that gets handled at 2am by a scheduled run rather than discovered at 9am by a customer.

Certificate expiry is the most predictable outage there is, which is what makes it embarrassing.

Without thisWith this
A calendar reminder someone snoozesA scheduled run
Renewal is remembered under time pressureIt happens with 30 days to spare
“Renewed” means the command exited zeroIt means a handshake presented the new certificate
One host renewed, three forgottenEvery host enumerated and checked
You needWhy
ACME reachable from the host, or a DNS credentialTo answer the challenge
A reload that does not drop connectionsThe install is pointless if it costs an outage
The renewal window you want30 days is the usual choice; below 14 leaves no room to retry

Renew on a window, not on a date. Anything inside the window gets renewed; everything else is left alone, which makes the run safe to execute daily.

name: renew-certificates
list: true
vars:
cert_path: /var/lib/letsencrypt
window_days: 30
tasks:
- name: inspect
letsencrypt:
op: info
certpath: "{{cert_path}}"
domain: "{{domain}}"
setvar: cert
- name: renew
letsencrypt:
op: renew
certpath: "{{cert_path}}"
domain: "{{domain}}"
retry:
max_attempts: 3
backoff_base: 30s
backoff_max: 5m

The retry is not decoration. ACME providers rate-limit, and a renewal that fails on the first attempt at day 30 has 29 days of runway, but only if something tries again.

A renewed certificate on disk that no process has re-read is not a renewed certificate.

- name: reload
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
install -m 0644 {{cert_path}}/{{domain}}/fullchain.pem /etc/nginx/certs/{{domain}}.crt
install -m 0600 {{cert_path}}/{{domain}}/privkey.pem /etc/nginx/certs/{{domain}}.key
nginx -t
systemctl reload nginx

nginx -t before the reload is what stops a bad certificate taking the service down: the test fails, the reload never runs, and the old certificate keeps serving.

The only check that means anything:

Terminal window
echo | openssl s_client -connect "$DOMAIN:443" -servername "$DOMAIN" 2>/dev/null \
| openssl x509 -noout -enddate -subject
notAfter=Nov 3 09:14:00 2026 GMT
subject=CN = www.example.com

If notAfter has not moved, the file changed and the process did not re-read it, go back to step 3. Checking the file on disk cannot tell you this, which is why the check is a connection.

FlowScript
Retry on a rate-limited providerretry: on the nodeWrite the loop yourself
Many domainsmap: with bounded concurrencySequential
Which domains renewedRecorded per nodeOnly what you logged
Runs unattended at 2amYesNeeds a wrapper

Renew with the flow. This is automation nobody watches, which puts a premium on the two things the flow has and the script does not: a retry policy, and a record of what happened while you were asleep.

- name: schedule
cron:
op: add
name: renew-certificates
schedule: "17 2 * * *"
command: "kis flow -f /etc/kis/renew-certificates.yaml"

Daily, at an odd minute. Every day is right because the window makes it idempotent, nothing outside 30 days is touched, and an odd minute keeps you out of the crowd hitting the provider on the hour.

CheckExpect
openssl s_client after a runnotAfter roughly 90 days out
Run it again immediatelyNothing renewed: everything outside the window
Service log during reloadNo dropped connections
The scheduled entrycron.list shows it, cron.logs shows last night’s run

That second row is the one worth confirming, a renewal that renews every time it runs will hit the provider’s rate limit exactly when you need it most.

ChangeHow
Many domainsA CSV of domains and a map: node
DNS-01 challengeProvide the DNS credential; the dns: operation can write the record
A different serverReplace nginx -t with that server’s config test. Keep the test
Certificates from an internal CASame shape, different issuing step