certfan

Plate 1 · TLS fingerprint survey · v0.3.0

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.

    checkone-IP probecertfanhow
    Every A and AAAA addressresolved 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 SNIchain and hostname verified, served certificate kept
    Fingerprints compared across nodesthe odd one out fails, named by address
    Expiry warninglast 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 thresholdscertfan check -f hosts.txt with optional warn_days=N / warn_pct=N
    Machine-readable resultexit 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://github.com/Blackman99/certfan" 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.