Detecting Unknown Subdomains with Certificate Transparency — A Practical Introduction Using Cert Spotter
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.comwww.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.
- Detect when a new SSL/TLS certificate is issued
- Detect when a new website appears on the public internet
- 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.comWhich 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.comand you then bring up
new.example.comas 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 monitoringas 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 -uinclude_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.orgThe 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.orgBecause 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 -uExpected 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.comThe 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 -uChange,
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.
- GitHub Actions scheduled workflows
- cron
- Cloud Run jobs triggered by Cloud Scheduler
- An existing CI/CD platform (drop it in as a scheduled job)
For example, suppose two consecutive runs produce these lists.
Previous
api.example.com
example.com
www.example.comCurrent
api.example.com
example.com
new.example.com
www.example.comThe diff,
+ new.example.comis 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
idfield - 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.comThe 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 -uname_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 GatewayIf a curl | jq pipeline blindly consumes that, you get,
jq: parse error: Invalid numeric literalwhich 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/jsonortext/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.comand you bring up,
app1.example.com
app2.example.com
secret.example.comunder 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.10then
foo.example.com
bar.example.com
anything.example.comall 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.
A Recommended Layered Setup
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.
| Method | What it detects | Real-time | Wildcard-tolerant | Notes |
|---|---|---|---|---|
| Certificate Transparency | Certificate issuance | High | △ | Visible from outside your org |
| Cert Spotter | CT log / API | High | △ | Well-suited to automated monitoring |
| crt.sh | CT log search | Medium | △ | Convenient but 502-prone; use for previews and manual checks |
| Passive DNS | DNS observations | Medium | ○ | Finds 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 TransparencyPreferred 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 DNSThe 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
- Certificate Transparency official site
- RFC 6962 — Certificate Transparency
- RFC 9162 — Certificate Transparency Version 2.0
- SSLMate CT Search API v1
- Cert Spotter API overview (SSLMate)
- crt.sh
- RFC 2606 — Reserved Top Level DNS Names (source of
example.comand other reserved domains) - RFC 1034 — Domain Names: Concepts and Facilities (Wildcard DNS)