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