Watch Certificate Transparency logs for new certificates, unexpected issuers and subdomains (MIT, free; pipx install) https://errant.solutions/tools/cert-transparency-watcher.html
  • Python 88.8%
  • Shell 11.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ronald Farrer 5594460e58
All checks were successful
tests / test (3.14) (push) Successful in 1m22s
tests / test (3.6-bullseye) (push) Successful in 1m12s
ci: claims ledger retries the cross-repo file fetch with bounded backoff (~110 s), clear failure message
Root cause: nightly python-matrix (3.8) run 56 hit a transient fetch failure of the homebrew formula (5 tries x 3 s was too short); file exists on main. 404 still fails immediately.
2026-11-09 10:11:56 -08:00
.forgejo/workflows mutation/chaos CI on every rw-man host again (owner decision 2026-10-18: colo backlog too big, GuardDuty suppression kept): EC2 guards are opt-in (refuse only with CI_COLO_ONLY=1, code kept), colo jobs move to the shared labels mutation-x86_64 / mutation-any (rw-man-01..05, 6g slots) 2026-10-18 13:45:29 -07:00
ci ci: claims ledger retries the cross-repo file fetch with bounded backoff (~110 s), clear failure message 2026-11-09 10:11:56 -08:00
tests 1.3.0: --allow-issuer-preset cloudflare (Cloudflare edge certs come from Let's Encrypt, Google Trust Services or SSL.com; --allow-issuer Cloudflare alone flagged its own GTS/LE certs), report note pointing at the preset, --help/README explain issuer matching; tests TestCloudflarePreset 2026-10-20 05:39:43 -07:00
.gitignore nightly mutation: incremental (only functions changed since the last green run) except a weekly full run on the repo's own weekday; replace-queued concurrency; staggered cron 2026-10-18 12:22:46 -07:00
cert_transparency_watcher.py 1.3.0: --allow-issuer-preset cloudflare (Cloudflare edge certs come from Let's Encrypt, Google Trust Services or SSL.com; --allow-issuer Cloudflare alone flagged its own GTS/LE certs), report note pointing at the preset, --help/README explain issuer matching; tests TestCloudflarePreset 2026-10-20 05:39:43 -07:00
CHANGELOG.md 1.3.0: --allow-issuer-preset cloudflare (Cloudflare edge certs come from Let's Encrypt, Google Trust Services or SSL.com; --allow-issuer Cloudflare alone flagged its own GTS/LE certs), report note pointing at the preset, --help/README explain issuer matching; tests TestCloudflarePreset 2026-10-20 05:39:43 -07:00
claims.json 1.3.0: --allow-issuer-preset cloudflare (Cloudflare edge certs come from Let's Encrypt, Google Trust Services or SSL.com; --allow-issuer Cloudflare alone flagged its own GTS/LE certs), report note pointing at the preset, --help/README explain issuer matching; tests TestCloudflarePreset 2026-10-20 05:39:43 -07:00
LICENSE cert-transparency-watcher: free CT-log monitoring tool, zero deps, no API key needed 2026-09-16 04:56:03 -07:00
pyproject.toml Mutation testing: copy all of ci/ into mutmut's sandbox (also_copy of ci/check_claims.py failed: no mutants/ci dir), keep real sleeps in fixture-server threads (--timeout test), per-repo mutant parallelism (ci/mutation-children) 2026-10-16 23:56:27 -07:00
README.md 1.3.0: --allow-issuer-preset cloudflare (Cloudflare edge certs come from Let's Encrypt, Google Trust Services or SSL.com; --allow-issuer Cloudflare alone flagged its own GTS/LE certs), report note pointing at the preset, --help/README explain issuer matching; tests TestCloudflarePreset 2026-10-20 05:39:43 -07:00

cert-transparency-watcher

Catch rogue or unexpected TLS certificates before they catch you — using the public Certificate Transparency (CT) logs, for free, with zero signup.

Since 2018, all public CAs must log every certificate they issue to public CT logs. This means anyone can query, in near real time, every certificate ever issued for your domain — including ones you didn't request. That's exactly how subdomain takeovers, rogue internal CAs, and CA-level misissuance get caught early, if someone's watching.

This tool queries crt.sh (a free, public CT log search service — no account, no API key) for a domain and every name under it, falls back to Cert Spotter's free API when crt.sh is down or slow, and flags:

  • Unexpected issuers — a cert issued by a CA that isn't on your allowlist. Classic sign of a misissued cert, a compromised CA, or someone using a different provider than your team agreed on.
  • Unexpected subdomains — a subdomain that got a cert but isn't one you expected. Classic early warning for subdomain takeover attempts (someone points a forgotten DNS record at infra they control and gets a cert for it) or a forgotten dev/staging host.
  • Recent activity — even with no allowlists configured, it just lists everything issued in the last N days (default 30) so you have visibility.

Why this matters

Most teams have zero visibility into what certs exist for their own domains beyond the ones they issued themselves. CT logs already contain that data — publicly, for free — but almost nobody looks at it until after an incident.

Install

pipx install git+https://git.canutethegreat.com/CanuteTheGreat/cert-transparency-watcher.git        # latest release branch; puts `cert-transparency-watcher` on PATH
pipx install git+https://git.canutethegreat.com/CanuteTheGreat/cert-transparency-watcher.git@v1.3.0  # a pinned release
brew install canutethegreat/errant-tools/cert-transparency-watcher   # after: brew tap canutethegreat/errant-tools https://git.canutethegreat.com/CanuteTheGreat/homebrew-errant-tools
scoop install errant-tools/cert-transparency-watcher                 # after: scoop bucket add errant-tools https://git.canutethegreat.com/CanuteTheGreat/scoop-errant-tools

Or download the single file (stdlib-only Python 3.6+) and run it directly: curl -fsSLO https://errant.solutions/downloads/cert_transparency_watcher.py && python3 cert_transparency_watcher.py --help. Every release has a changelog, the wheel, the sdist and a SHA256SUMS file. Not on PyPI yet, so plain pipx install cert-transparency-watcher doesn't work until it's published there.

Usage

cert-transparency-watcher example.com
cert-transparency-watcher example.com --allow-issuer "Let's Encrypt" --allow-issuer "Google Trust"
cert-transparency-watcher example.com --allow-subdomain www --allow-subdomain mail --allow-subdomain "@"
cert-transparency-watcher example.com --days 90 --json
# daily cron: only certificates new since yesterday, and tell me
cert-transparency-watcher example.com --state ~/.ct-state.json --fail-on-new \
    --webhook https://hooks.example/ct --exec 'mail -s "new cert" me@example.com'
  • --allow-issuer SUBSTR (repeatable) — substrings of issuer names you expect (e.g. "Let's Encrypt", "Google Trust Services", "DigiCert", "Amazon"), matched case-insensitively against the whole issuer as CT logs record it (C=US, O=Google Trust Services, CN=WE1). Any cert whose issuer doesn't match any allowed substring is flagged. Omit entirely to just list issuers without flagging.
  • --allow-issuer-preset cloudflare (repeatable, combines with --allow-issuer) — allows every CA Cloudflare issues edge certificates from. See Cloudflare-proxied domains.
  • --allow-subdomain LABEL (repeatable) — subdomain labels you expect (e.g. www, mail, api). Use @ for the bare/apex domain and * for a wildcard certificate (*.example.com, valid for every subdomain; *.dev for *.dev.example.com), so a new wildcard cert is flagged unless you allow it. Any subdomain seen in a cert that isn't allowlisted is flagged. Omit entirely to just list subdomains without flagging.
  • --state FILE — remember every certificate seen (one file can hold many domains). Only certificates new since the last run are shown and flagged, so a daily cron job doesn't re-alert on the same certs. --fail-on-new flags every new certificate even without allow lists (Cert Spotter-style "tell me about anything new").
  • --exec CMD / --webhook URL — when something is flagged, run CMD (alert JSON on stdin, CTW_DOMAIN/CTW_ALERTS in the environment) and/or POST the JSON to URL. If a hook fails the exit code is 2 and the state isn't saved, so the next run alerts again instead of losing the alert.
  • --source auto|crtsh|certspotter — default auto (crt.sh with retries, then Cert Spotter). CERTSPOTTER_API_TOKEN is sent if set (higher rate limits).
  • --days N — only show certs issued in the last N days (default 30).
  • --json — machine-readable output (same exit codes as the text report).
  • --crtsh-url TEMPLATE / --certspotter-url URL — mirrors or local test servers.

A precertificate and its final certificate (two crt.sh rows, same serial) are counted once.

Cloudflare-proxied domains

Cloudflare's Universal, Advanced and Total TLS edge certificates are issued by Let's Encrypt, Google Trust Services or SSL.com, and Cloudflare picks the CA (and keeps a backup certificate from a second CA). In CT logs only the SSL.com ones carry Cloudflare's name (O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 4); the others read O=Google Trust Services, CN=WE1 or O=Let's Encrypt, CN=E7. So --allow-issuer Cloudflare alone flags Cloudflare's own certificates as an unexpected issuer (the report then adds a note pointing here). Use the preset instead:

cert-transparency-watcher example.com --allow-issuer-preset cloudflare

It allows issuers containing Cloudflare, Google Trust Services, Let's Encrypt, SSL Corporation or SSL.com. Those are whole public CAs, so a certificate someone else got from Let's Encrypt for your domain passes the issuer check: pair the preset with --allow-subdomain and/or --state --fail-on-new to still see every new certificate.

Exit codes: 1 if anything was flagged, 0 otherwise, 2 if no CT source could be queried or parsed, an alert hook failed, or usage was wrong. Python 3.6+.

Real example (found while building this)

Running this against rustinion.com during development turned up 3 certs in the last year: two from the expected Cloudflare/Google Trust Services pair, and a third from Amazon (Amazon RSA 2048 M04), covering rustinion.com and www.rustinion.com, dated 2026-08-28 -- a CA that wasn't on the allowlist we had in mind for that domain going in. We traced it to AWS infrastructure we control (not a compromise), but the point stands: without querying CT logs we had no idea a third CA had issued a cert for that domain at all. That's exactly the kind of "wait, who issued that?" moment this tool is built to surface. Nothing beats testing a security tool against your own real domains.

Requirements

Python 3.6+, standard library only — no pip installs, no API keys, no external dependencies. Uses crt.sh's free public JSON API, which is a community service and occasionally rate-limits or 502s under load — the tool retries automatically with backoff.

Pay what you want

Free to use, forever. If it's useful to you, there's a pay-what-you-want link — no obligation.

License

MIT — see LICENSE.

Tests

tests/ exercises every option and check end-to-end (against a local server speaking crt.sh's JSON format, including 502/retry and broken-JSON responses) in Forgejo CI on Python 3.6 and current Python; sh ci/run-tests.sh 3.13 reproduces it locally (needs docker).