Skip to content

Usage

Check hostnames

certfan check example.com                  # port 443, all A + AAAA
certfan check api.example.com:8443 www.example.com
certfan check -4 example.com               # IPv4 only (-6 for IPv6 only)

Targets file

Check many hosts from a file (and still accept positional targets):

certfan check -f hosts.txt
certfan check -f hosts.txt edge.example.com
# production edges
shop.example.com warn_days=3 warn_pct=25
api.example.com:8443
# staging
staging.example.com warn_days=14

Blank lines and # comments are ignored. Optional trailing warn_days=N / warn_pct=N override the global --warn-days / --warn-pct for that line only.

Check explicit backends

When the nodes sit behind one VIP, or DNS returns only some of them, list them yourself:

certfan check shop.example.com --connect 10.0.0.7 --connect 10.0.0.8 --connect 10.0.0.9:8443

SNI and hostname verification use shop.example.com. Override with --sni.

Ask several resolvers (GeoDNS, split-horizon)

GeoDNS, latency-based routing and split-horizon DNS give different clients different addresses, so the system resolver only ever shows you part of the fleet. Ask several resolvers and certfan probes the union:

certfan check shop.example.com --resolver 1.1.1.1 --resolver 8.8.8.8 --resolver 10.0.0.2
shop.example.com  3 address(es) from 3 resolvers (answers differ), 2 distinct certificate(s)
  PASS  203.0.113.10   cert A  serial 51fe63574478  expires 2026-11-20  TLSv1.3  chain:2  ocsp  41ms  via 1.1.1.1,8.8.8.8,10.0.0.2
  PASS  198.51.100.4   cert A  serial 51fe63574478  expires 2026-11-20  TLSv1.3  chain:2  ocsp  92ms  via 8.8.8.8
  FAIL  10.0.4.17      cert B  serial 61c4ec55df4c  expires 2026-10-12  TLSv1.3  chain:2  ocsp  3ms  via 10.0.0.2
    - 10.0.4.17: serves a different certificate (serial 61c4ec55df4cf8bb, issued 2026-08-13) than the other nodes
  verdict: FAIL

Different answers on their own are normal for GeoDNS, so by default they are only shown. Use --dns-divergence warn (or fail) when every resolver should agree, for example to catch a decommissioned node that is still in one zone. A resolver that times out or returns SERVFAIL is a WARN; NXDOMAIN just means no addresses. Queries go out over UDP (TCP when the answer is truncated), all resolvers in parallel, with no extra dependencies. --resolver and --connect are alternatives.

Mail servers and databases (STARTTLS)

Behind mx.example.com there are usually several MTAs, and behind db.internal a primary and replicas. They renew too, and they forget to reload too. With --starttls certfan says hello in plaintext first, asks for TLS, and then compares certificates exactly as it does for HTTPS:

certfan check mx.example.com --starttls smtp          # EHLO, STARTTLS        (port 25)
certfan check mail.example.com --starttls imap        # CAPABILITY, STARTTLS  (port 143)
certfan check mail.example.com --starttls pop3        # CAPA, STLS            (port 110)
certfan check db.internal --starttls postgres --cafile corp-root.pem   # SSLRequest (port 5432)
mx.example.com:25  4 address(es) over SMTP STARTTLS, 2 distinct certificate(s)
  PASS  192.0.2.21   cert A  serial 51fe63574478  expires 2026-11-20  TLSv1.3  chain:2  no-ocsp  38ms  as mx1.example.com
  PASS  192.0.2.22   cert A  serial 51fe63574478  expires 2026-11-20  TLSv1.3  chain:2  no-ocsp  41ms  as mx2.example.com
  FAIL  192.0.2.23   cert B  serial 61c4ec55df4c  expires 2026-10-12  TLSv1.3  chain:2  no-ocsp  44ms  as mx3.example.com
  FAIL  192.0.2.24   server does not advertise STARTTLS (EHLO offered: PIPELINING SIZE 8BITMIME)  as mx4.example.com
    - 192.0.2.24: SMTP STARTTLS failed: server does not advertise STARTTLS (EHLO offered: PIPELINING SIZE 8BITMIME)
    - 192.0.2.23: serves a different certificate (serial 61c4ec55df4cf8bb, issued 2026-08-13) than the other nodes
  verdict: FAIL

A node that never advertises STARTTLS, refuses it, or stalls before TLS starts is a FAIL that names the stage, so a mail relay that quietly fell back to plaintext cannot hide behind --allow-unreachable. If a server sends extra plaintext right after its go-ahead (the STARTTLS response-injection bug class), certfan refuses to start TLS on that stream. Ports 465 / 993 / 995 already speak TLS directly: check those without --starttls. Point plain certfan at port 25 or 5432 by mistake and the error tells you which --starttls to add. certfan demo --starttls smtp shows all of this on four local nodes.

ALPN

--alpn h2,http/1.1 offers those protocols in the handshake. When nodes select different ones (one replica rebuilt without HTTP/2, a proxy that lost its h2 config), the odd ones out are a WARN:

  WARN  203.0.113.12   cert A  serial 51fe63574478  expires 2026-11-20  TLSv1.3  chain:2  ocsp  alpn:http/1.1  44ms
    - 203.0.113.12: negotiated ALPN http/1.1 while other nodes negotiate h2

What gets flagged

Finding Level
A node serves a different certificate from the majority (on a tie, from the most recently issued one) FAIL
Same leaf, different presented intermediates (chain mismatch) FAIL
Issuer CN differs across nodes (when full chain is unavailable) FAIL
Chain or hostname verification fails FAIL
Certificate expired FAIL
Address unreachable (WARN with --allow-unreachable) FAIL
With --starttls: STARTTLS not advertised, refused, or plaintext sent after the go-ahead FAIL
Fewer than --warn-days (7) days left, or less than --warn-pct (33%) of lifetime left WARN
Some nodes present an OCSP staple and others do not WARN
A --resolver timed out or failed WARN
With --alpn: a node selects a different ALPN protocol from the others WARN
An address only some --resolvers returned (with --dns-divergence warn / fail) WARN / FAIL

The lifetime percentage matters for short-lived certs: a 6-day cert with 1.5 days left is at 25%, which should already have renewed.

Full certificate-chain capture (leaf + intermediates) needs Python 3.13+ (get_unverified_chain). On 3.9–3.12 certfan still compares the leaf fingerprint and issuer CN.

Private CAs, self-signed

certfan check internal.corp:443 --cafile corp-root.pem
certfan check lab.local --no-verify     # only compare + expiry