Fig. 1 Did every node get the renewed certificate?
certfan resolves every A/AAAA address behind a hostname, handshakes each one with the right SNI, and fails when they don’t all serve the same valid, not-nearly-expired certificate.
shop.example.com → 4 addresses. Glyph = SHA-256 of the certificate each address served, one spoke per byte. lime consistent, pink the odd one out.
resolving…
Fig. 2
What gets checked
A typical uptime probe resolves the name once, hits one address, and reports green. With 47-day certificates (and Let’s Encrypt’s 6-day ones) a node that missed the reload is a weekly chance, not a yearly accident.
| check | one-IP probe | certfan | how |
|---|---|---|---|
| Every A and AAAA address | resolved by default, or pass backends with --connect 10.0.0.7:443 | ||
| Every resolver's answer (GeoDNS, split-horizon) | repeat --resolver 1.1.1.1 --resolver 8.8.8.8; the union is probed, each node tagged with who returned it | ||
| TLS handshake per address, right SNI | chain and hostname verified, served certificate kept | ||
| Fingerprints compared across nodes | the odd one out fails, named by address | ||
| Expiry warning | last days, or last third of the certificate’s lifetime, on every node | ||
| Presented chain compared (intermediates) | FAIL when intermediates differ across nodes that share the same leaf; OCSP staple presence noted | ||
| Mail and database nodes v0.4.0 | --starttls smtp|imap|pop3|postgres on every MX or replica; a node that stopped offering STARTTLS fails on its own (Fig. 4) | ||
| Same ALPN on every node v0.4.0 | --alpn h2,http/1.1 warns when one node quietly stopped negotiating h2 | ||
| Targets file with per-host thresholds | certfan check -f hosts.txt with optional warn_days=N / warn_pct=N | ||
| Machine-readable result | exit 0 / 1 / 2, --format json, --format prom, --prom-file |
Fig. 3
Certificate served per node, days 0–50. ACME renews on day 3 and the deploy hook reloads nginx. Node .12 was draining and never reloaded: it keeps the old certificate until it expires on day 47 and one request in four fails.
The one-IP probe (ticks on .10) stays green throughout. certfan’s daily run (ticks across all lanes) flags .12 from day 3.
Fig. 4
Before the handshake
Mail servers and databases only start TLS after a few lines of plaintext. With --starttls certfan speaks exactly enough of each protocol to ask for it, on every node, then compares certificates as in Fig. 1. No login, no MAIL FROM, no query.
New in v0.4.0: until that release is tagged, pipx install "git+https:// installs it from main.
Table 1
One command
Python 3.9+, no dependencies. Exit codes drop straight into cron, CI or a Kubernetes CronJob.
$ certfan check shop.example.com shop.example.com 4 address(es), 2 distinct certificate(s) PASS 203.0.113.10 cert A expires 2026-11-20 TLSv1.3 41ms PASS 203.0.113.11 cert A expires 2026-11-20 TLSv1.3 38ms FAIL 203.0.113.12 cert B expires 2026-10-12 TLSv1.3 44ms - 203.0.113.12: serves a different certificate than the other nodes PASS 2001:db8::10 cert A expires 2026-11-20 TLSv1.3 40ms verdict: FAIL exit 2
install
pipx install "git+https://github.com/Blackman99/certfan@v0.3.0"then certfan demo starts three local nodes, one serving a different cert. Offline.