CAA records bound to your ACME account, the secure way
A plain CAA record says "only Let's Encrypt". That still includes anyone who briefly controls your DNS or your web server and has a free ACME account, which is the exact attacker you were worried about.
The short answer
Publish CAA at the registered domain naming one CA, with accounturi set to your ACME account URL and validationmethods limited to the method you use. Forbid wildcards with issuewild ";" unless you need them, add an iodef contact, sign the zone with DNSSEC, and give delegated subdomains their own CAA.
On this page
What goes wrong
Any public certificate authority (CA) may issue a certificate for your domain once the requester proves control of it. Control is proven by serving a token over HTTP, answering a TLS handshake, or publishing a TXT record. Anyone who can do one of those for a few minutes can get a valid certificate.
CAA records (RFC 8659) name the CAs allowed to issue. That removes the other
CAs. It does not stop someone from using the CA you allowed: an ACME account
costs nothing. RFC 8657 adds two parameters. accounturi names the ACME
account that may request certificates. validationmethods names the
challenge types the CA may use. Together they turn "any Let's Encrypt user
who controls my web server for a minute" into "my account, with DNS-01".
Three more gaps are common: no rule for wildcard certificates, CAA on the wrong name, and a zone without DNSSEC, where the CAA answer itself can be forged.
What the docs say
This can be used to ensure that someone who temporarily hijacks your domain, but doesn't have access to your ACME Account key, can't issue malicious certificates.
Source: Let's Encrypt docs, CAA, the accounturi parameter
Domains configuring CAA records for a CA MUST NOT assume that the restrictions implied by the "accounturi" and "validationmethods" parameters are effective in the absence of explicit indication as such from that CA.
Source: RFC 8657, Section 5.2
The search for a CAA RRset climbs the DNS name tree from the specified label up to, but not including, the DNS root "." until a CAA RRset is found.
Source: RFC 8659, Section 3
CAA records are therefore not an effective way of restricting or controlling issuance for subdomains of a domain, where control over those subdomains is delegated to another party
Source: RFC 8657, Section 5.7
Let's Encrypt documents both parameters. Its page does not warn that the account URL differs between staging and production, or that CAA is public, so the account URL links your domains together (RFC 8657 Section 5.9 does). DNS server docs say nothing about CAA beyond the record syntax.
The secure configuration
Find your ACME account URL. Most clients store it next to the account key
(for example certbot show_account, or the status.acme.uri field of a
cert-manager ClusterIssuer).
Zone records at the registered domain:
; Only Let's Encrypt, only this ACME account, only DNS-01.
example.com. CAA 0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890; validationmethods=dns-01"
; No wildcard certificates from anyone.
example.com. CAA 0 issuewild ";"
; Where a CA may report a refused request.
example.com. CAA 0 iodef "mailto:[email protected]"
; A team with its own ACME account for api.example.com and names below it.
api.example.com. CAA 0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/9876543210; validationmethods=dns-01"
api.example.com. CAA 0 issuewild ";"Rules for the record set:
- One "issue" record per allowed CA and account. Records of the same tag add up:
if any one allows the request, the CA may issue.
- A name with its own CAA RRset replaces the parent's set completely for that
name and everything below it. Repeat issuewild and iodef there.
- Use the production account URL. Staging accounts have different URLs.
- If you use a second CA as a fallback, give it its own issue record with its
own account binding, and check that it supports RFC 8657.Sign the zone (see the Knot automatic signing page) so a CA with a validating resolver cannot be fed a forged CAA answer.
Prove it
The test (secure-tests/caa-records-bound-acme-account/run.sh) serves the
records above from a signed Knot 3.5.4 zone. It checks what a CA would
read. It does not request a certificate: that needs a public CA.
The apex CAA set, and its signature validated:
kdig @127.0.0.1 example.com CAA +norecurse +short
delv @127.0.0.1 -a anchor.conf +root=example.com example.com CAA0 iodef "mailto:[email protected]"
0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890; validationmethods=dns-01"
0 issuewild ";"
; fully validatedA name without its own CAA gets a signed, empty answer. RFC 8659 Section 6.4 explains why this matters: an empty answer and a bogus one mean very different things to a CA.
delv @127.0.0.1 -a anchor.conf +root=example.com www.example.com CAA;; resolution failed: ncache nxrrset
; negative response, fully validatedThe tree climb from RFC 8659 Section 3, done with kdig, shows which set a
CA applies to each name:
$ caa_lookup www.example.com
relevant CAA RRset for www.example.com found at example.com:
0 iodef "mailto:[email protected]"
0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890; validationmethods=dns-01"
0 issuewild ";"
$ caa_lookup v2.api.example.com
relevant CAA RRset for v2.api.example.com found at api.example.com:
0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/9876543210; validationmethods=dns-01"
0 issuewild ";" The zone file passes Knot's checks:
$ kzonecheck -o example.com /config/example.com.zone
zone file OKMistakes people make
CAA without accounturi
"Only letsencrypt.org" keeps out other CAs. It does not keep out someone who controls your web server for a minute and has their own free account. Bind the account.
Allowing a validation method you do not use
If your certificates come from DNS-01, an HTTP-01 challenge for your domain
is never legitimate. Set validationmethods=dns-01 and a web server
takeover alone can no longer get a certificate.
Forgetting that a subdomain CAA replaces the parent's
The climb stops at the first CAA set it finds. A subdomain set with only an
issue record allows wildcards there again, because the parent's
issuewild ";" no longer applies. Repeat every rule you want.
Assuming every CA honors the parameters
RFC 8657 is explicit: do not assume accounturi and validationmethods
work unless the CA says so. Name only CAs that document support, and use
flag 128 (critical) only if you understand that CAs which do not know a tag
must then refuse.
Rotating the ACME account and forgetting CAA
A new account key with a new registration gets a new account URL. Renewals then fail CAA. Update the record first, wait its TTL, then switch accounts.
No DNSSEC
Without DNSSEC, an attacker who can spoof DNS answers to the CA can also spoof the CAA answer. Sign the zone.
Checklist
- Publish CAA at the registered domain with one
issuerecord per allowed CA. - Add
accounturi=with your production ACME account URL. - Add
validationmethods=with only the challenge type you use. - Add
issuewild ";"unless you need wildcard certificates. - Add an
iodefcontact address. - Give delegated or separately managed subdomains a complete CAA set of their own.
- Confirm your CA documents support for
accounturiandvalidationmethods. - Sign the zone with DNSSEC and confirm
delvvalidates the CAA answer. - Update CAA before changing ACME accounts.
CAA started as a list of allowed CAs. With `accounturi`, it becomes a list of allowed keys, which is what you meant in the first place.
H2-CPQE
Learn it on a live range
DNSSEC with Knot, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome