Search

Detecting Unknown Subdomains with Certificate Transparency — A Practical Introduction Using Cert Spotter

Tadashi Shigeoka · Wed, January 6, 2021

If you own a root domain, you eventually want to know whether unexpected subdomains or SSL/TLS certificates have appeared under it. Shadow IT detection, mapping unmanaged environments, catching phishing or misconfigurations (the motivations vary, but the question is the same): how do you find hostnames from the outside that you did not know about?

This article walks through subdomain discovery and monitoring using Certificate Transparency (CT) logs, the Cert Spotter API, and crt.sh, at a level someone new to CT can follow. Internal-viewpoint monitoring like DNS provider audit logs only covers zones your own account is authoritative for, so it is out of scope here. Every domain in the examples is example.com.

There Is No Real “Register a Subdomain” Operation

Before anything else, some vocabulary.

Consider this structure.

example.com
├── www.example.com
├── api.example.com
└── dev.example.com

www.example.com and api.example.com are not stored as “subdomain” entities anywhere. They exist because the authoritative DNS server for example.com has DNS records such as the following added to it.

  • A
  • AAAA
  • CNAME
  • TXT
  • NS

That is why the requirement,

“Detect when a new subdomain is registered”

does not translate cleanly on its own. In practice, it hides several distinct requirements.

  1. Detect when a new SSL/TLS certificate is issued
  2. Detect when a new website appears on the public internet
  3. Discover subdomains that you (the owner) did not know about

This article focuses on the part of that space you can observe from the outside: Certificate Transparency and Passive DNS.

What Is Certificate Transparency

If your reach extends beyond authoritative DNS you own (subsidiaries with separate DNS, shadow IT, or third parties issuing certificates for your name), you need a signal that covers “any newly issued certificate that mentions our hostnames.” That is what Certificate Transparency provides.

CT Logs

Certificate Transparency, defined in RFC 6962 and updated by RFC 9162, requires public certificate authorities (CAs) to record every publicly-trusted SSL/TLS certificate they issue into an append-only public log (a CT log). Major browsers (Chrome, Safari, Firefox) refuse to treat a public certificate as valid unless it appears in CT logs, so effectively every publicly-trusted certificate ends up in a CT log.

Certificates carry a list of hostnames in the Subject Alternative Name (SAN) field. For example, if a certificate lists the following, all four hostnames are discoverable through CT.

example.com
www.example.com
api.example.com
staging.example.com

Which means you can check from the outside,

“Has any certificate been issued for a subdomain of ours that we did not know about?”

without touching your own DNS or CA accounts.

CT Logs Are Not the Same as HTTPS Sites

An important caveat up front.

CT logs record certificate issuance, not the appearance of an HTTPS site.

Suppose you already have a wildcard certificate,

*.example.com

and you then bring up

new.example.com

as a new website using that same wildcard cert. No new certificate is issued, so nothing new appears in CT logs. From a CT-only perspective, “nothing happened.”

So do not treat,

CT log monitoring = HTTPS site monitoring

as equivalent. CT tells you about newly-issued certificates and the hostnames in them, and no more.

Do Not Put Confidential Information in Subdomain Names

CT logs are a data source for you to detect unknown subdomains from the outside, and equally a data source for anyone else to enumerate your subdomains. crt.sh and Cert Spotter accept the same queries from anyone, so the moment you obtain a certificate from a public CA, the hostname is visible to attackers, competitors, and OSINT tooling around the world.

That is why you should not encode confidential information into subdomain names themselves: customer company names, unreleased project codenames, unannounced strategic initiatives, and so on. The hostname alone can leak the underlying fact.

  • Names that expose a customer company (e.g., bigcustomer-portal.example.com)
  • Unreleased project codenames (e.g., project-nemo.example.com)
  • Hostnames hinting at an unannounced M&A or new business line (e.g., newventure-acquisition.example.com)
  • Names that reveal internal architecture or versions (e.g., admin-mysql-primary.example.com)

You can avoid CT by staying on a wildcard certificate or by running a private CA (an internal-only CA), but the former creates the detection blind spot discussed later in this article, and the latter is not trusted by browsers. The cleaner approach in operation is to keep hostnames boring at the naming layer (for example, use only internal management codes like portal-a1.example.com).

Using the Cert Spotter API

For continuous CT-log queries, the first tool to reach for is the Cert Spotter API from SSLMate. SSLMate ingests and indexes CT logs continuously and exposes them through an official API.

The basic query looks like this.

curl -s \
  'https://api.certspotter.com/v1/issuances?domain=example.com&include_subdomains=true&expand=dns_names' \
  | jq -r '.[].dns_names[]' \
  | sort -u

include_subdomains=true matches certificates that include example.com or any subdomain of it. expand=dns_names asks the API to include the DNS names field, which is omitted by default.

Cert Spotter allows limited unauthenticated evaluation calls, but for production monitoring you should register an API key and send it as Authorization: Bearer <API_KEY> (or HTTP Basic Auth with the key as the username). Rate limits are managed per SSLMate account.

Why Unrelated Domains Show Up in the Results

Query domain=example.com and you may see output like this.

*.example.com
example.com
example.edu
example.net
example.org
www.example.com
www.example.edu
www.example.net
www.example.org

The obvious question: why do example.net, example.edu, and example.org appear when the query was example.com?

The answer: Cert Spotter searches for certificates matching example.com, then expand=dns_names returns every DNS name (SAN) present in those matched certificates.

Suppose one certificate has the following SAN.

Subject Alternative Names:
 
example.com
www.example.com
example.net
www.example.net
example.org
www.example.org

Because example.com is in that SAN, the entire certificate matches the query. expand=dns_names then returns every SAN entry in it.

Visually,

flowchart TB
    A["domain=example.com"] --> B["Find certificates that contain example.com"]
    B --> C["Certificate SAN<br/>example.com ← HIT<br/>www.example.com<br/>example.net<br/>example.org<br/>example.edu"]
    C --> D["expand=dns_names"]
    D --> E["Return every SAN entry"]

example.net and example.org are riding on the same certificate as example.com and are surfaced as a side effect, not because they belong to your zone. For monitoring purposes, you have to filter them out.

Narrowing the Results to example.com

To keep only names that are example.com or end with .example.com, filter with jq.

curl -s \
  'https://api.certspotter.com/v1/issuances?domain=example.com&include_subdomains=true&expand=dns_names' \
  | jq -r '.[].dns_names[]
      | select(. == "example.com" or endswith(".example.com"))' \
  | sort -u

Expected output (example.com is a reserved domain per RFC 2606, so unlike a real production zone it has no api. / staging. certificates issued for it, and the filtered list stays minimal).

*.example.com
example.com
www.example.com

The leading dot in endswith(".example.com") matters. Without it, a name like notexample.com would also match.

A Reusable Shell Snippet

Parameterize the domain so you can reuse the same script.

DOMAIN="example.com"
 
curl -s \
  "https://api.certspotter.com/v1/issuances?domain=${DOMAIN}&include_subdomains=true&expand=dns_names" \
  | jq -r --arg domain "$DOMAIN" \
      '.[].dns_names[]
       | select(. == $domain or endswith("." + $domain))' \
  | sort -u

Change,

DOMAIN="example.com"

to any domain and the rest of the pipeline works as-is. In CI or a job runner, pass DOMAIN as an environment variable.

Continuous Monitoring

A one-off search is fine for triage. Actual monitoring means,

notify me when a new certificate or subdomain appears.

The shape is roughly,

flowchart TB
    A[Cert Spotter] --> B[Scheduled run]
    B --> C[Current DNS Names list]
    C --> D[Diff against the previous list]
    D --> E[New subdomains]
    E --> F["Slack / Teams / SIEM"]

Reasonable places to host that scheduled job.

For example, suppose two consecutive runs produce these lists.

Previous
 
api.example.com
example.com
www.example.com
Current
 
api.example.com
example.com
new.example.com
www.example.com

The diff,

+ new.example.com

is what you notify on. Persist the previous list somewhere durable (S3, Cloud Storage, or a Git-tracked file) that fits your existing infrastructure.

Incremental Fetch with after

The Cert Spotter API has a cursor-style parameter for fetching only issuances discovered since a previous run. Per the SSLMate CT Search API v1 docs, the pattern is,

  • Each issuance object has an id field
  • On the next call, pass after=<id of the last issuance you saw>
  • The API returns only issuances discovered after that cursor

In shape,

flowchart TB
    A[Previous cursor] --> B[Only new certificates]
    B --> C[Check dns_names]
    C --> D[Notify if any unknown subdomain]

A simple example (assume LAST_ID was persisted from the previous run).

DOMAIN="example.com"
LAST_ID="$(cat ./certspotter-cursor)"
 
curl -s \
  "https://api.certspotter.com/v1/issuances?domain=${DOMAIN}&include_subdomains=true&expand=dns_names&after=${LAST_ID}"

When there are no new issuances, Cert Spotter returns an empty array along with a Retry-After header telling you how long to wait before polling again. Honor that header rather than polling on your own schedule.

You can always re-fetch all issuances and diff, but for continuous monitoring the after cursor is the intended path (fewer round trips, less pressure on rate limits). Since API details can change, verify against the official documentation when you implement.

crt.sh as a Quick-Check Backup

With Cert Spotter as the primary path, crt.sh is still worth knowing about. It is a public service run by Sectigo, with both a SQL-style search UI and a JSON API. For one-off manual investigation (poking a domain from the UI, sanity-checking something Cert Spotter surfaced), it is still handy, but 502s are frequent (see below), which makes it a poor choice as the main path for automated monitoring. Position it as “a preview before wiring up Cert Spotter” or “a cross-check for interesting Cert Spotter results.”

In the UI, use % as a wildcard.

%.example.com

The same query as JSON (% needs URL-encoding as %25).

curl -s \
  'https://crt.sh/?q=%25.example.com&output=json'

To extract only the hostnames, pipe through jq and dedupe with sort -u.

curl -s \
  'https://crt.sh/?q=%25.example.com&output=json' \
  | jq -r '.[].name_value' \
  | sort -u

name_value may contain multiple hostnames separated by newlines (the SAN and Common Name combined). For a strict one-hostname-per-line output, use jq -r '.[].name_value | split("\n")[]'.

Caveats When Using crt.sh

crt.sh is convenient, but it is not an SLA-backed API. During peak traffic or heavy crawler load, you frequently see errors like the following.

502 Bad Gateway

If a curl | jq pipeline blindly consumes that, you get,

jq: parse error: Invalid numeric literal

which looks like a jq bug at first glance. It is not. The pipeline is,

flowchart LR
    A[curl] --> B[An HTML error page]
    B --> C[jq]

jq is being handed HTML and told it is JSON.

The fastest triage is -i to see the headers.

curl -i \
  'https://crt.sh/?q=%25.example.com&output=json'

Now you can check,

  • HTTP status (200 or 5xx)
  • Content-Type (application/json or text/html)
  • The actual body

Anything other than 200 with application/json should be retried after a delay, or you should just route your automated pipeline through the Cert Spotter API and keep crt.sh for previews and manual cross-checks.

The Wildcard Certificate Blind Spot

Everything above hinges on a new certificate being issued. Wildcard certificates break that assumption.

If you already have,

*.example.com

and you bring up,

app1.example.com
app2.example.com
secret.example.com

under that same wildcard, no CA action takes place. Nothing new appears in CT logs. So Certificate Transparency alone cannot detect these new subdomains.

In other words, anywhere your organization uses a wildcard certificate,

  • CT logs will not surface new subdomains
  • Cert Spotter, crt.sh, and any other CT search tool inherit the same blind spot

This is a structural limit of CT-only monitoring, not a bug in the tools.

Wildcard DNS Is Also Invisible from the Outside

DNS has a structurally similar blind spot. If your authoritative DNS has a wildcard DNS record,

*.example.com → 203.0.113.10

then

foo.example.com
bar.example.com
anything.example.com

all resolve without any explicit A or CNAME record. No record was added, so for exactly the same reason as wildcard certificates, external CT queries and external zone enumeration cannot surface the new hostnames. To make this space visible, you need a data source that captures actual resolver traffic, which is where Passive DNS comes in.

Given the above, the practical answer is to not rely on Certificate Transparency alone, and instead layer external observation sources.

flowchart TB
    Root[example.com]
    Root --> CT[CT Log]
    Root --> PDNS[Passive DNS]
    CT --> CS[Cert Spotter]
    PDNS --> PA[External observation archive]
    CS --> INV[Subdomain inventory]
    PA --> INV
    INV --> ALLOW[Compare to allowlist]
    ALLOW --> Known[Known]
    ALLOW --> Unknown[Unknown]
    Unknown --> Alert["Slack / SIEM alert"]

CT picks up “a certificate was issued.” Passive DNS picks up “this name was actually resolved.” Between them, the blind spots that wildcard certificates and wildcard DNS create are covered. It costs more to operate, but it closes most single-signal misses.

Comparing the Approaches

The main external-observation options.

MethodWhat it detectsReal-timeWildcard-tolerantNotes
Certificate TransparencyCertificate issuanceHighVisible from outside your org
Cert SpotterCT log / APIHighWell-suited to automated monitoring
crt.shCT log searchMediumConvenient but 502-prone; use for previews and manual checks
Passive DNSDNS observationsMediumFinds hostnames seen in the wild

The for wildcard tolerance reflects the same underlying issue: under a wildcard certificate or wildcard DNS, no new event is emitted for a new hostname, so there is nothing for CT to catch. In wildcard-heavy environments, Passive DNS carries proportionally more weight.

Conclusion

Split by intent,

You Want to Detect New SSL/TLS Certificates from the Outside

Use Certificate Transparency.

Certificate Transparency

Preferred tools, in order,

Cert Spotter (main path for automated monitoring)
crt.sh       (preview and manual cross-check)

For continuous monitoring, the Cert Spotter API after cursor is the right primitive.

You Want the Broadest Possible Coverage of Unknown Subdomains

Do not pick one signal. Layer them.

Certificate Transparency
+
Passive DNS

The zones where wildcard certificates silence CT are exactly where Passive DNS shines, and niche use cases that Passive DNS never observes are the ones CT still catches.

And to state the single most important caveat one more time,

“Certificate Transparency reveals hostnames for which a certificate was issued, not every HTTPS site or subdomain that currently exists.”

It is easy to install CT monitoring and assume you now see every subdomain your organization owns. Anything hidden behind a wildcard certificate or a wildcard DNS record is invisible to CT. Share that limitation with the operators, and layer CT on top of Passive DNS. That is the realistic shape of subdomain detection from the outside.

That’s all from walking through Certificate Transparency, the Cert Spotter API and crt.sh for external unknown-subdomain detection, the blind spots created by wildcard certificates and wildcard DNS, and a layered setup that combines CT with Passive DNS, from the Gemba.

References