If you have ever stared at a Let's Encrypt failure that says "CAA forbids issuance for this name" and wondered what you did wrong, this post is for you. The CAA record is a DNS-level instruction to certificate authorities, and it is one of the few cases where a misconfiguration in DNS produces a failure in a completely different system, often hours after the change.
What CAA actually does
CAA (Certification Authority Authorization, RFC 8659) lets a domain owner publish a DNS record listing which CAs are permitted to issue certificates for that name. Before a CA issues, it is required by the CA/Browser Forum baseline requirements to fetch the CAA record set and check that its own identifier appears. If it does not, the CA must refuse to issue.
Browsers do not consult CAA at all. It is purely an issuance-side gate. A wrongly issued certificate is still valid in the browser; CAA is the policy layer that aims to prevent the wrong issuance from happening in the first place.
example.com. 300 IN CAA 0 issue "letsencrypt.org"
example.com. 300 IN CAA 0 issuewild "letsencrypt.org"
example.com. 300 IN CAA 0 iodef "mailto:[email protected]"
Three properties matter:
issue: which CA may issue regular certs.issuewild: which CA may issue wildcard certs (separate from non-wild).iodef: where to send violation reports.
The flag byte at the start (the 0) is the criticality bit. 0 means "if you don't understand this property, ignore it". Set to 128, it means "if you don't understand this, do not issue".
The traversal rule
This is where most people trip. CAA is checked at the queried name and walks up the tree. If you ask for a cert for www.api.example.com, the CA queries CAA at each of:
www.api.example.comapi.example.comexample.com
The walk stops at the first level that returns any CAA records. Those records govern issuance — not the apex by default. So if you set example.com CAA 0 issue "letsencrypt.org" but a hostname five levels deep has its own conflicting CAA, the deeper one wins.
Conversely: if no CAA exists at any level, any CA may issue. The absence of a CAA record set is the most permissive policy possible.
Why your Let's Encrypt issuance is failing
The five real-world causes, in rough order of frequency:
- Wrong identifier. The Let's Encrypt identifier is
letsencrypt.org, notletsencrypt, notle.org, notletsencrypt.com. Typos here look fine to humans and break ACME silently. issuewithoutissuewildon a wildcard cert. A cert covering*.example.comrequiresissuewild. Theissueproperty alone does not authorise wildcards.- CNAME at the queried name. CAA querying follows CNAMEs. If
www.example.comis a CNAME tocdn.somecdn.net, the CA may end up reading CAA atcdn.somecdn.net, which often forbids non-CDN issuance. - DNSSEC failure on the CAA record. CAs typically use validating resolvers. A broken DNSSEC chain that returns SERVFAIL is treated as "could not determine policy, refuse to issue".
- Cached old CAA. Your CA's resolver has the old, restrictive CAA in cache and won't refresh until TTL expires. This is why CAA changes need a TTL lower than your ACME retry interval.
Debugging flow
Step one: query CAA for the exact hostname you are trying to issue for, then walk up. dig +short CAA www.api.example.com, then dig +short CAA api.example.com, then dig +short CAA example.com. The first one that returns non-empty is the binding policy. Verify it includes the correct CA identifier.
Step two: re-run the same lookups through other resolvers to confirm the change has propagated. The fastest visual for this is dnscheck — the CAA panel shows the matrix across all 12 resolvers. Try letsencrypt.org to see a well-formed example.
Step three: if CAA looks correct everywhere, check whether your ACME client is hitting a different resolver. Some Docker setups bake in a stale stub resolver that has not been told about the change.
Reference: RFC 8659 (CAA), Let's Encrypt CAA docs.