is it dns? / wiki ← back to the live monitor

CAA: who is allowed to issue your certs

Without CAA, any public CA that's in your browser's trust store can mint a cert for your domain. That's hundreds of CAs. CAA (RFC 8659) lets you say "no, just these."

The shape of a CAA record#

example.com.  IN  CAA  <flags>  <tag>  "<value>"
FieldValuesMeaning
flags0 or 128128 = "critical": a CA that doesn't understand the tag must not issue
tagissueAllows the named CA to issue non-wildcard certs; also serves as wildcard fallback when issuewild is absent
tagissuewildAllows the named CA to issue wildcard certs
tagiodefWhere to send violation reports (mailto:, http:, or https: URL)
valuestringCA's identifier (letsencrypt.org, pki.goog, etc.) plus optional account/method parameters

Multiple records OR together. An issue value naming no CA at all, written issue ";", means "no CA is authorized" (RFC 8659 §4.2): useful for sub-zones that should never have certs. Query type 257 is the IANA assignment for CAA.

A minimal policy#

example.com.  CAA  0 issue      "letsencrypt.org"
example.com.  CAA  0 issuewild  "letsencrypt.org"

Translation: only Let's Encrypt may issue, and it may also issue wildcards. The additional accounturi and validationmethods parameters are defined by RFC 8657; any CA can implement them, ACME or not. Let's Encrypt's CAA docs cover their specific support.

You can see a single live issue "letsencrypt.org" record at any time:

dig @1.1.1.1 +short CAA caa.example.isitdns.net
0 issue "letsencrypt.org"
nslookup -type=CAA caa.example.isitdns.net 1.1.1.1
Server:         1.1.1.1
Address:        1.1.1.1#53

Non-authoritative answer:
caa.example.isitdns.net rdata_257 = 0 issue "letsencrypt.org"

nslookup prints CAA as rdata_257 = ... on most platforms (current BIND 9.18 included) because it has no dedicated CAA decoder. The data is correct; only the label differs. Use nslookup-and-dig for a comparison of both tools.

The AD: true field in a DoH response confirms DNSSEC validated the record end-to-end:

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=caa.example.isitdns.net&type=CAA'
{
  "Status": 0,
  "TC": false,
  "RD": true,
  "RA": true,
  "AD": true,
  "CD": false,
  "Question": [{"name": "caa.example.isitdns.net", "type": 257}],
  "Answer": [{
    "name": "caa.example.isitdns.net",
    "type": 257,
    "TTL": 300,
    "data": "\\# 22 00 05 69 73 73 75 65 6c 65 74 73 65 6e 63 72 79 70 74 2e 6f 72 67"
  }]
}

The hex data field decodes to flags=0, tag=issue, value=letsencrypt.org. AD: true means a DNSSEC-validating resolver confirmed the signature chain; a rogue CA cannot substitute a different value without breaking validation. The TTL reads 300 when the answer is fresh and counts down while cached, so you may see less. Google's DoH JSON endpoint (https://dns.google/resolve?name=caa.example.isitdns.net&type=CAA) returns the same record already decoded, 0 issue "letsencrypt.org", instead of Cloudflare's raw hex.

nslookup cannot do DoH/DoT. Use the curl form above to query a specific DoH endpoint or to verify the AD bit.

The managed-DNS auto-injection catch#

If your zone is hosted on a managed DNS provider with its own edge SSL (e.g. Cloudflare's Universal SSL), the provider often auto-injects CAA records for all CAs that back its edge certificate ecosystem. At time of writing, Cloudflare's documented auto-inject set includes Let's Encrypt, Google Trust Services (pki.goog), SSL.com, and sectigo.com (Sectigo, formerly comodoca.com). DigiCert no longer appears in the current documented set, though it can persist in zones created before that change; this zone still carries digicert.com and comodoca.com entries from the earlier set alongside the current one. The provider notes the list may change. You'll see those in dig output even though they're not in the dashboard. The effective policy is the union of what's in the zone and what the provider adds.

If you want a strict policy the provider cannot widen, you either disable their edge SSL and serve your own origin certs only, or accept the union and rely on your issue allowlist for the real CAs you care about.

For Cloudflare specifically, see their CAA docs.

You can see the auto-injected set live on this zone:

dig @1.1.1.1 +short CAA isitdns.net | sort
0 issue "comodoca.com"
0 issue "digicert.com; cansignhttpexchanges=yes"
0 issue "letsencrypt.org"
0 issue "pki.goog; cansignhttpexchanges=yes"
0 issue "sectigo.com"
0 issue "ssl.com"
0 issuewild "comodoca.com"
0 issuewild "digicert.com; cansignhttpexchanges=yes"
0 issuewild "letsencrypt.org"
0 issuewild "pki.goog; cansignhttpexchanges=yes"
0 issuewild "sectigo.com"
0 issuewild "ssl.com"
nslookup -type=CAA isitdns.net 1.1.1.1
Server:         1.1.1.1
Address:        1.1.1.1#53

Non-authoritative answer:
isitdns.net     rdata_257 = 0 issue "comodoca.com"
isitdns.net     rdata_257 = 0 issue "digicert.com; cansignhttpexchanges=yes"
isitdns.net     rdata_257 = 0 issue "letsencrypt.org"
isitdns.net     rdata_257 = 0 issue "pki.goog; cansignhttpexchanges=yes"
isitdns.net     rdata_257 = 0 issue "sectigo.com"
isitdns.net     rdata_257 = 0 issue "ssl.com"
isitdns.net     rdata_257 = 0 issuewild "comodoca.com"
isitdns.net     rdata_257 = 0 issuewild "digicert.com; cansignhttpexchanges=yes"
isitdns.net     rdata_257 = 0 issuewild "letsencrypt.org"
isitdns.net     rdata_257 = 0 issuewild "pki.goog; cansignhttpexchanges=yes"
isitdns.net     rdata_257 = 0 issuewild "sectigo.com"
isitdns.net     rdata_257 = 0 issuewild "ssl.com"

nslookup cannot show the RRSIG or AD bit. Use dig +dnssec or the curl DoH form to confirm DNSSEC coverage.

The effective policy is the union. The letsencrypt.org entry here is the one the zone owner set deliberately (it also appears on Cloudflare's own list); the rest are auto-injected by the managed DNS provider. Record order in the response varies per query; the outputs on this page are sorted for comparison.

How CAA is checked#

When a CA receives a CSR for host.example.com, it must:

  1. Look up CAA at host.example.com. If present, use those.
  2. If absent, look up CAA at example.com. Use those.
  3. Walk up the tree if needed (RFC 8659 §3).
  4. If still nothing, no CAA: any CA is allowed.

issuewild is checked specifically when the CSR is for *.example.com. issue covers everything else.

When issuewild is absent from the RRset, the CA falls back to issue for wildcard CSRs as well (RFC 8659 §4.3). A zone with only issue and no issuewild permits the listed CAs to issue both non-wildcard and wildcard certs.

CAA also follows CNAME chains, but only at each exact name: if host.example.com is a CNAME pointing to host.cdn.example, the CA checks CAA at host.cdn.example itself, and if nothing is found there the tree climb continues at the parents of host.example.com, the original name, never at the parents of the CNAME target (RFC 8659 §3). Still surprising when a subdomain CNAMEs out to a SaaS provider that publishes its own CAA policy at the exact target name.

Two boundaries worth knowing. Checking CAA has been mandatory for publicly trusted CAs since 8 September 2017 (CA/Browser Forum Ballot 187), so this is not an optional courtesy. And it happens only at issuance time: browsers never look at CAA, and tightening your policy does not invalidate certs that already exist ("Relying Parties MUST NOT use CAA records as part of certificate validation", RFC 8659 §1).

Verifying CAA works#

You cannot observe a CA rejection from the outside: that happens inside the CA's issuance flow. What you can verify is that the policy is published correctly and reads what you expect.

To check whether a CA sees your policy the same way you do, SSLMate's CAA Record Helper can load your domain's current CAA record set and show which CAs it authorizes for wildcard and non-wildcard issuance. That's as close to an enforcement test as you can get without actually requesting a cert.

For publication proof, query directly against a known-good public resolver to bypass local caching. The sort brings issue and issuewild lines into a predictable order for comparison:

dig @1.1.1.1 +short CAA caa.example.isitdns.net
0 issue "letsencrypt.org"

For a real-world multi-CA policy, the isitdns.net apex shows Cloudflare auto-injection in action:

dig @1.1.1.1 +short CAA isitdns.net | sort
0 issue "comodoca.com"
0 issue "digicert.com; cansignhttpexchanges=yes"
0 issue "letsencrypt.org"
0 issue "pki.goog; cansignhttpexchanges=yes"
0 issue "sectigo.com"
0 issue "ssl.com"
0 issuewild "comodoca.com"
0 issuewild "digicert.com; cansignhttpexchanges=yes"
0 issuewild "letsencrypt.org"
0 issuewild "pki.goog; cansignhttpexchanges=yes"
0 issuewild "sectigo.com"
0 issuewild "ssl.com"
nslookup -type=CAA caa.example.isitdns.net 1.1.1.1
Server:         1.1.1.1
Address:        1.1.1.1#53

Non-authoritative answer:
caa.example.isitdns.net rdata_257 = 0 issue "letsencrypt.org"

To confirm DNSSEC is protecting the record set, look for the RRSIG line:

dig @1.1.1.1 +dnssec +short CAA isitdns.net | sort
0 issue "comodoca.com"
0 issue "digicert.com; cansignhttpexchanges=yes"
0 issue "letsencrypt.org"
0 issue "pki.goog; cansignhttpexchanges=yes"
0 issue "sectigo.com"
0 issue "ssl.com"
0 issuewild "comodoca.com"
0 issuewild "digicert.com; cansignhttpexchanges=yes"
0 issuewild "letsencrypt.org"
0 issuewild "pki.goog; cansignhttpexchanges=yes"
0 issuewild "sectigo.com"
0 issuewild "ssl.com"
CAA 13 2 300 20260718131151 20260716111151 34505 isitdns.net. ...

The trailing CAA 13 2 300 ... line is the RRSIG covering the CAA RRset: algorithm 13 is ECDSAP256SHA256 (RFC 6605), 2 is the label count of isitdns.net, and 300 is the original TTL. The two timestamps are the signature's expiration and inception; Cloudflare re-signs the zone continuously, so the dates and signature you see will differ. For DNSSEC to protect your CAA policy, the zone must be both signed and have a DS record at the parent. isitdns.net meets both conditions. See dnssec for how the chain of trust works end to end.

nslookup cannot show RRSIG records or the AD bit. Use dig +dnssec or the curl DoH form to confirm DNSSEC coverage.

Bulletproof CAA: the checklist#

  • [x] Apex has at least one issue record (otherwise any CA wins)
  • [x] Apex has at least one issuewild record if you use wildcards
  • [ ] Optional: iodef to receive violation reports (requires a mailbox or an https: reporting endpoint)
  • [ ] Optional: include accounturi= to bind issuance to a specific ACME account
  • [ ] Optional: include validationmethods= to restrict ACME challenge types (e.g. dns-01 only)

The latter three are advanced posture: they fail closed when an attacker compromises the rest of your stack but not your DNS.

Worked example: locking issuance to a specific ACME account#

example.com.  IN  CAA  0  issue  "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456"

The full accounturi value is a single string in the zone record; the line is long but must not be split.

Now Let's Encrypt only allows issuance from ACME account 123456. Even an attacker with full DNS control cannot mint a cert from a different LE account. The accounturi and validationmethods parameters are defined by RFC 8657; any CA can implement them, ACME or not. The account URI format shown is the one Let's Encrypt documents; other CAs define their own.


See also: CAA record reference · TLSA: pinning the cert itself in DNS · DNSSEC · DoH and DoT · nslookup and dig · CNAME · All record types · Query types · RFC 8659 · RFC 8657