<!-- https://pkizerotohero.com/guides/monitor-certificate-transparency/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Watch Certificate Transparency logs for your domains: manual searches, a small scheduled script with an allow-list of CAs, alerts, and what to do next.

Source: https://pkizerotohero.com/guides/monitor-certificate-transparency/ · Last reviewed: 2026-10-20

How-to guide · Operate and monitor

# How to monitor Certificate Transparency for your domains

**In one sentence:**

You will have a scheduled check that lists every new publicly trusted certificate for your domains and flags any certificate issued by a CA you did not allow.

Task

[Operate and monitor](https://pkizerotohero.com/guides/#task-operate)

Platforms

- Linux
- macOS
- Windows

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Expert](https://pkizerotohero.com/start/#expert)
- [Developer](https://pkizerotohero.com/start/#developer)

Level

Practitioner · 5 steps

## Before you start

- The list of registered domains your organisation owns, including parked and mail-only ones.
- The list of CAs you use: the same values as in your CAA records (see [How to set CAA records](https://pkizerotohero.com/guides/caa-records/)).
- Python 3.8 or later on a machine that can run scheduled jobs, or a CI system with schedules.
- A person or team who will read the alerts.

Why this matters: every publicly trusted TLS certificate is published in [Certificate Transparency logs](https://pkizerotohero.com/handbook/glossary/#ct-log), because Chrome requires Certificate Transparency for publicly trusted TLS certificates (since April 2018). A certificate for your name that you did not order is visible to anyone who looks. A [CT monitor](https://pkizerotohero.com/handbook/glossary/#ct-monitor) makes sure that someone is you.

## Steps

### 1. Choose how you will watch

| Option | Good for |
| --- | --- |
| A hosted monitoring service (several have free tiers) | Most organisations: alerts by email or webhook, no code |
| Your CA’s or certificate-management platform’s CT alerts | Teams that already use such a platform |
| A small script against a CT search API (below) | Learning, small estates, or feeding alerts into your own tools |

Whichever you choose, the alert must reach a named owner, and a failed check must be as loud as a finding.

### 2. Search by hand once

Look at what already exists before you automate. In a browser, open `https://crt.sh/?q=example.com` with your own domain: crt.sh is a free public search over the logs. For scripts, the same search exists as JSON (`&output=json`).

crt.sh is often overloaded. On 20 October 2026, every request we made returned `502 Bad Gateway`. That is why a monitor should not depend on one free service, and why it must report its own failures.

Write down every issuer you see and every name you do not recognise. This is your starting point.

### 3. Set up a scheduled check

This script uses SSLMate’s Cert Spotter search API, which merges each certificate with its [precertificate](https://pkizerotohero.com/handbook/glossary/#precertificate) into one entry and can tell you the issuer’s CAA values. It needs only the Python standard library:

```python
#!/usr/bin/env python3
"""ct-watch.py: list certificates for your domains that appeared in CT logs since the last run."""
import json, os, pathlib, sys, urllib.request

DOMAINS = ["example.com"]                    # your registered domains
ALLOWED_CAA = {"letsencrypt.org"}            # the CA values in your CAA records
STATE = pathlib.Path("ct-watch-state.json")  # remembers where the last run stopped
API = "https://api.certspotter.com/v1/issuances"

def fetch(domain, after):
    url = (f"{API}?domain={domain}&include_subdomains=true"
           "&expand=dns_names&expand=issuer&expand=issuer.caa_domains")
    if after:
        url += f"&after={after}"
    request = urllib.request.Request(url)
    if key := os.environ.get("CERTSPOTTER_API_KEY"):
        request.add_header("Authorization", f"Bearer {key}")
    with urllib.request.urlopen(request, timeout=60) as response:
        return json.load(response)

state = json.loads(STATE.read_text()) if STATE.exists() else {}
unexpected = 0
for domain in DOMAINS:
    after = state.get(domain)
    while page := fetch(domain, after):
        for item in page:
            issuer = item["issuer"]
            known = set(issuer.get("caa_domains") or []) & ALLOWED_CAA
            unexpected += not known
            print("ok " if known else "NEW", item["not_before"][:10],
                  issuer["name"], " ".join(item["dns_names"]))
        after = page[-1]["id"]
    state[domain] = after
STATE.write_text(json.dumps(state))
sys.exit(1 if unexpected else 0)
```

How it behaves:

- The first run lists every unexpired certificate: your baseline. Later runs list only certificates that appeared since.
- Each certificate is reported once. `ok` means the issuer is one of your allowed CAs; `NEW` means it is not.
- The script exits with status 1 when it finds a `NEW` certificate, and crashes with an error when the API cannot be reached. Both make a scheduled job fail visibly.
- Without an API key, the service allows a limited number of queries per hour “for personal or evaluation purposes”. For production, get a key and set `CERTSPOTTER_API_KEY`, or use a hosted monitor.

We ran it on 20 October 2026 for `rfc-editor.org`, with an allow-list that deliberately held only Let’s Encrypt (34 lines; a selection):

```text
ok  2026-07-23 C=US, O=Let's Encrypt, CN=YR2 *.demo.rfc-editor.org demo.rfc-editor.org
NEW 2026-08-11 C=US, O=Google Trust Services, CN=WR1 history.rfc-editor.org
NEW 2026-08-12 C=US, O=Google Trust Services, CN=WR1 *.rfc-editor.org rfc-editor.org
ok  2026-08-14 C=US, O=Let's Encrypt, CN=YR1 *.rfc-editor.org rfc-editor.org
NEW 2026-09-09 C=US, O=Google Trust Services, CN=WE1 755a3119.sni.cloudflaressl.com data.diff.rfc-editor.org
ok  2026-09-19 C=US, O=Let's Encrypt, CN=YR1 *.dev.rfc-editor.org *.prod.rfc-editor.org *.staging.rfc-editor.org dev.rfc-editor.org prod.rfc-editor.org staging.rfc-editor.org
ok  2026-10-13 C=US, O=Let's Encrypt, CN=YR2 *.rfc-editor.org rfc-editor.org
```

The second run, a minute later, printed nothing and exited with 0. The `NEW` lines are a typical innocent surprise: certificates that a content delivery network obtained for some of the site’s names (note the `cloudflaressl.com` name). After checking, the owner would add `pki.goog` to the allow-list, and to the CAA records if they use them.

Schedule it, for example every six hours with cron on Linux:

```text
# m  h    dom mon dow  command
17   */6  *   *   *    cd /opt/ct-watch && python3 ct-watch.py > last.txt 2>&1 || mail -s "CT watch: check last.txt" pki-team@example.com < last.txt
```

On Windows, use Task Scheduler; in CI, a scheduled pipeline whose failure notifies the owning team.

### 4. Decide what deserves a question

For each `NEW` line, and for any `ok` line you cannot explain, ask:

- **Which issuer?** A CA you do not use is the strongest signal.
- **Which names?** Unknown subdomains, wildcards, and names that should only exist internally.
- **When?** A certificate nobody expected, for example when no renewal was due.

Two lines with the same names and dates often come from one provider issuing an RSA and an ECDSA certificate side by side; Google Trust Services’ `WR1` above is an RSA intermediate and `WE1` an ECDSA one. Check internally first: most surprises turn out to be a CDN, a SaaS platform or a forgotten test server.

### 5. Act on a certificate that is not yours

1. **Keep the evidence**: the issuer, serial number and SHA-256 fingerprint. Download the certificate from the CT search and paste it into the decoder below.
2. **Report it to the issuing CA.** Every public CA must publish how to report misuse (Baseline Requirements section 4.9.3) and must investigate within 24 hours (section 4.9.5). If the validation cannot be relied on, the CA must revoke within 24 hours.
3. **Find out how validation was passed**: check your DNS records, web servers, registrar and DNS-provider accounts.
4. **Tighten your CAA records**, so that only your CAs may issue.

*Interactive tool: Certificate decoder (runs in the browser at [/tools/decoder/](https://pkizerotohero.com/tools/decoder/))*

## Check it worked

- Run the script by hand twice. The first run lists your current certificates; the second prints nothing and exits with 0.
- Prove the alert path: temporarily remove one CA from `ALLOWED_CAA`, delete `ct-watch-state.json`, run the job through the scheduler, and confirm the owning team receives the failure. Then restore both.

## If something goes wrong

- **The API is down or answers 502 or 429.** The job fails, which is correct. Retry later, and make sure repeated failures alert as loudly as findings.
- **Too many results.** Large domains have thousands of names. Watch the registered domain with subdomains included, but compare issuers rather than reading every line.
- **Constant `NEW` lines from a provider.** Add the provider’s CA to the allow-list and to CAA only after you confirm the provider is yours.
- **Internal host names appear in the logs.** Every name in a public certificate is public. Move internal-only names to a [private CA](https://pkizerotohero.com/handbook/private-ca/).
- **Certificates from your private CA never appear.** That is expected: CT applies to publicly trusted certificates only.

## Why this works

CT-enforcing browsers accept a public certificate only if it was logged, so the logs hold an append-only record of every certificate for your names that those browsers will accept ([Certificate Transparency](https://pkizerotohero.com/handbook/certificate-transparency/)). CT does not stop a bad certificate: [domain validation](https://pkizerotohero.com/handbook/domain-validation/) and CAA limit issuance, and monitoring turns a silent mis-issuance into an alert. Once you find one, the CA’s [revocation](https://pkizerotohero.com/handbook/revocation/) duties give you a path to remove it.

## Go deeper

- [RFC 9162](https://www.rfc-editor.org/rfc/rfc9162) and [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962): how CT logs, SCTs and monitors work.
- [Chrome’s CT policy](https://googlechrome.github.io/CertificateTransparency/ct_policy.html): what Chrome requires of every public certificate.
- [CA/Browser Forum Baseline Requirements](https://cabforum.org/working-groups/server/baseline-requirements/requirements/), section 4.9.3: how CAs must accept problem reports.
- [SSLMate: Cert Spotter API reference](https://sslmate.com/help/reference/ct_search_api_v1): parameters, pagination and API keys for the search API used above.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/monitor-certificate-transparency/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 17: Certificate Transparency](https://pkizerotohero.com/handbook/certificate-transparency/)
- [Chapter 11: Proving you own the name: DV, OV, EV and CAA](https://pkizerotohero.com/handbook/domain-validation/)
- [Chapter 16: Taking it back: CRLs, OCSP and stapling](https://pkizerotohero.com/handbook/revocation/)

### Tools

- [Certificate decoder](https://pkizerotohero.com/tools/decoder/): Paste a PEM certificate and read every field in plain words.

### Topics

- [Revocation and transparency](https://pkizerotohero.com/topics/revocation-and-transparency/)

## More guides: Operate and monitor

- [How to check when a certificate expires, and monitor it](https://pkizerotohero.com/guides/check-certificate-expiry/)
- [How to prepare for short-lived certificates: a checklist](https://pkizerotohero.com/guides/prepare-47-day-certificates/)
