Skip to content

feat(ops): watch the certificates, three weeks before they take the box down - #426

Merged
catomean merged 1 commit into
mainfrom
feat/cert-expiry-watch
Aug 28, 2026
Merged

feat(ops): watch the certificates, three weeks before they take the box down#426
catomean merged 1 commit into
mainfrom
feat/cert-expiry-watch

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Nothing watched TLS expiry.

Caddy renews at 30 days remaining and has never missed — which is exactly why the check felt unnecessary. But renewal breaks for reasons no app ever sees: an ACME rate limit, port 80 closed by a firewall change, a DNS record moved. The first symptom is every site on the box going dark at once. Twenty domains, one failure. The warning exists for weeks; nobody was reading it.

Measured today, all twenty are fine — 65–88 days, staggered. That's the moment to add the check, not after the outage that proves it was needed.

No second list

It reuses the uptime sweep's targets via uptime-sweep.sh --certs, and I verified the lists are identical: 20 targets, 20 certificates. A separate list of domains is the gap that hid botsmann for weeks, and it would open here the same way — a new app registered in apps.conf would be probed for HTTP and silently unprotected against expiry.

Thresholds are Caddy's, not arbitrary

verdict days why
ok ≥ 21 Caddy renews at 30, so it has had nine days of tries
warn 7–20 renewal should have happened and did not — look while looking is cheap
critical < 7 an outage with a date on it

Unreadable is critical, never ok. "We could not check the certificate" and "the certificate is fine" must not share an outcome — that conflation is how botsmann's 503 read as healthy for weeks.

Daily, not every 15 minutes

A certificate doesn't change between sweeps. Twenty TLS handshakes a quarter-hour to relearn the same number is noise that earns nothing, and noise gets muted.

No issue/Telegram machinery either: fleet-uptime.yml owns that, and a second alerting path to keep in step isn't worth it for a check with three weeks of slack. That trade-off is stated in the workflow rather than left to be discovered.

Verification

66 unit tests (9 new, pinning every verdict boundary including negative days and unreadable input). --certs run against production: 20 domains, ok=20 warn=0 critical=0.

…ox down

Nothing watched TLS expiry. Caddy renews at 30 days remaining and has never
missed, which is exactly why: the check felt unnecessary because it had never
been needed. But renewal breaks for reasons no app ever sees — an ACME rate
limit, port 80 closed by a firewall change, a DNS record moved — and the first
symptom is every site on the box going dark at once. Twenty domains, one
failure. The warning exists for weeks; nobody was reading it.

Measured today, all twenty are fine: 65-88 days, staggered. That is the moment
to add the check, not after the outage that proves it was needed.

Reuses the uptime sweep's target list rather than taking a copy — verified
identical, 20 targets and 20 certificates. A second list of domains is the gap
that hid botsmann for weeks, and it would open here in exactly the same way: a
new app registered in apps.conf would be probed for HTTP and silently
unprotected against expiry.

Thresholds are Caddy's, not arbitrary. ok at 21+ days, because Caddy renews at
30 and has therefore had nine days of attempts; warn from 7 to 20, when renewal
should have happened and did not; critical under a week. Unreadable is CRITICAL,
never ok: "we could not check the certificate" and "the certificate is fine"
must not share an outcome — that conflation is how botsmann's 503 read as
healthy for weeks.

Daily, not every fifteen minutes. A certificate does not change between sweeps,
and twenty TLS handshakes a quarter-hour to relearn the same number is noise
that earns nothing — and noise gets muted.

No issue/Telegram machinery: fleet-uptime.yml owns that, and a second alerting
path to keep in step is not worth it for a check with three weeks of slack.
Stated in the workflow rather than left to be discovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018waGt1ieA9TjpscqrbrnGb
@catomean
catomean merged commit 27115b1 into main Aug 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant