Repository navigation
fix(ingress): CAA correctness for Route53, wildcards and the dnsguide check - #139
Merged
Merged
Conversation
| import os | ||
| import sys | ||
| import types | ||
| import unittest |
kvinwang
force-pushed
the
fix/ingress-caa
branch
from
October 14, 2026 10:46
ec02a85 to
b60811a
Compare
kvinwang
force-pushed
the
fix/ingress-namecheap
branch
from
October 15, 2026 14:25
a4c5bb9 to
2958653
Compare
kvinwang
force-pushed
the
fix/ingress-caa
branch
from
October 15, 2026 15:01
3896065 to
93a97c1
Compare
… check - Route53 create_caa_record ignored the requested name and value: it upserted hard-coded issuers at the zone apex without accounturi, so under DELEGATION_ZONE the only issuance guard was written where the CA never looks. It now merges the value into the record set at the requested name in one UPSERT, replacing only the same tag. Record names are decoded from Route53's octal escapes (\052 -> *). - Where a CAA shares the name with the gateway pointer (the delegated name, or a plain domain with SET_CAA=true), callers tell set_alias_record so (--with-caa) and the provider picks the form, as before. By the DNS rules a CNAME hides a CAA beside it, and Route53 and Linode refuse the pair, so the default is an A record via the new set_address_record (one change batch on Route53). Cloudflare answers a CAA query with the CAA beside a CNAME (CAA_BESIDE_CNAME) and keeps the CNAME. A name that already holds a CAA is treated the same way. - Wildcards published only issuewild, which per RFC 8659 leaves foo.<base> unrestricted; they now publish issue as well, and warn once per start when the base is a zone apex. - check_caa treated a set without the relevant tag as forbidding issuance and did not fall back to issue for wildcards; it now follows RFC 8659 tag relevance and the critical flag as Boulder does. - The base set_* helpers ignored delete failures and stopped at the first match, so a stale accounturi pin could survive. They now create the new value first, then delete every stale one, failing if a delete fails (the delegation hook's challenge TXT tolerates a stale value). Signed-off-by: Kevin Wang <wy721@qq.com>
kvinwang
force-pushed
the
fix/ingress-caa
branch
from
October 15, 2026 16:19
93a97c1 to
20c4d90
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
create_caa_recordignored the requested name and value. It upserted hard-codedletsencrypt.org+ Amazon issuers at the zone apex, droppedaccounturi/validationmethods, and underDELEGATION_ZONE(where this CAA is the only protection) wrote the record where the CA never looks.<domain>.<zone>) andSET_CAAon a served name both put the two on one name. Cloudflare is the exception: it answers a CAA query with the CAA beside a CNAME (verified on real deployments).issuewild. Per RFC 8659 that does not restrict non-wildcard names, so any CA could issue forfoo.<base>.dnsguide.pytreated a CAA set without the wanted tag (onlyiodef, or onlyissuewildfor a plain name) as forbidding issuance, and did not fall back toissuefor wildcards. Valid setups waited outDNS_SETUP_TIMEOUTand exited 1.set_*helpers inbase.pyignored delete failures and returned at the first matching record, so a staleaccounturipin could survive forever.What
dnsman.py set_alias --with-caa, used by dns01.sh for the delegated name and for a non-wildcard domain withSET_CAA=true).set_alias_recordthen publishes an A record (set_address_record, replacing any CNAME/AAAA) unless the provider setsCAA_BESIDE_CNAME, which Cloudflare does, so Cloudflare keeps the CNAME. All other deployments keep a CNAME. Trade-off for the A record (documented): it follows a gateway move only everyRENEW_INTERVAL, and only one gateway address is published. Linode still always publishes A. If the name already holds a CAA, for example afterSET_CAAis turned off (the container cannot tell its own CAA from the operator's, so it leaves it),set_aliaskeeps the A record and logs why, instead of failing on a rejected or ineffective CNAME.set_address_recorddeletes the CNAME/AAAA and upserts the A in one change batch, so the name never goes unanswered. Record names are decoded from Route53's octal escapes (\052→*), so wildcard records are found.set_caa_recordmerges the value into the record set at the requested name in one UPSERT. Only values with the same tag are replaced, and a repeat run makes no change. A failed CAA listing makesdnsman.pyexit 1.base.py:set_a/cname/txt/caa_recorddelete every non-matching record (for CAA, only those with the same tag) and return False if any delete fails.set_a_record,set_txt_recordandset_caa_recordcreate the new value first and only then delete the stale ones (for CAA, same-tag only), so a failed create leaves the old A/TXT/CAA in place instead of none (under delegation the CAA is the only issuance guard). Values may coexist briefly. Only the CNAME↔A switch keeps delete-then-create, since the two cannot coexist. Route53 and Namecheap (fix(ingress): publish Namecheap CAA records instead of skipping them #144) override it with a single zone write. The delegation hook's challenge TXT is the exception: it passesset_txt --tolerate-stale, because an extra challenge value does no harm.issueandissuewild, and dnsguide prints both.*.<zone apex>withSET_CAA=true: theissuerecord at the apex replaces every existingissuevalue there, including the Amazon issuers that older versions wrongly added on Route53. It governs every name in the zone that has no CAA of its own. The container warns once per start when the base answers SOA for itself.x.comand*.x.comoverwrite each other'sissuerecord. Second, a wildcard's webhook payload contains two CAA records with the same name, so receivers must not upsert by (type, name).check_caa: a set with no relevant tag is unrestricted, and a wildcard falls back toissuewhen there is noissuewild. A critical record with an unknown tag still forbids issuance. The known tags are Let's Encrypt's (issue,issuewild,iodef,issuemail,issuevmc), and flag bit 1 counts as critical, as in Boulder.Relies on #135's Namecheap fixes (name-scoped records, content-based deletes) and #144's Namecheap CAA writes, both merged. Namecheap now uses the base
set_alias_record, so going back to a CNAME also removes the A record.Tests
Re-run on top of current main (after #133, #135, #136, #143, #144) with the same fake-provider lab: every case below passes. With
CAA_BESIDE_CNAMECloudflare publishes CNAME + CAA under delegation andSET_CAA, with no writes on rerun; Linode, Namecheap and Route53 are unchanged. The original results:scripts/tests/test_dnsguide.py: 33 pass.scripts/tests/test_route53.py(new): 3 pass. The escaped-name test fails on the previous head. Both suites pass on the host and indstack-ingress:2.6.*as\052, and the DoH mock follows CNAMEs the way a resolver does. The test runsprocess_domainfrom dns01.sh with stubbed issuance. Namecheap was run on this branch merged with fix(ingress): stop Namecheap updates from wiping unrelated records #135 and fix(ingress): publish Namecheap CAA records instead of skipping them #144.app.example.comand*.app.example.com, rc 0 on all four providers: A + TXT + CAA (issue, plusissuewildfor the wildcard) at<name>.deleg.net, and the delegation checks pass. A rerun makes no writes. Upgrading from a CNAME at the delegated name: Route53 sends DELETE CNAME + UPSERT A in one batch.app.example.comwith an existing CNAME: replaced by A, and the CAA is written, on all four providers. A rerun makes no writes.*.example.com: the wildcard keeps its CNAME (Linode: A, as before). CAAissue+issuewildare written at the apex. The apex warning appears on the first pass only.app.example.com+*.app.example.comtogether: the base gets A + CAA, and the wildcard keeps its CNAME.SET_CAAon, then off: rc 0 on all four providers. The A and CAA records stay, and later passes make no writes. Before this fix, Route53 deleted the A record and then failed the CNAME write. A plain domain whose zone has a CAA only at the apex still gets a CNAME.SET_CAAon, then off, then the CAA deleted: A + CAA, then A kept while the CAA exists (no writes), then only a CNAME at the name.set_alias --with-caafollowed by plainset_aliasleaves only the CNAME (before, both A and CNAME). UnrelatedwwwA andblogCNAME are untouched, and reruns make no writes.SET_CAA=false): CNAME on Cloudflare, Namecheap and Route53. A rerun makes no writes.\052.example.com.CNAME, Amazonissuewildvalues andiodefat the apex. Result: rc 0 and the CNAME is kept. The Amazon values are replaced byissue/issuewildwithaccounturi, andiodefis kept. A rerun makes no writes.set_alias --with-caaon*.example.comwith the escaped CNAME: one batch, then no change. An unresolvable gateway exits 1 with no change.dnsman.py set_caa, oldissue/issuewild/iodefpresent): Cloudflare/Linode withDNSMOCK_FAIL=<p>:createexit 1 and keep the oldissue(before: exit 1 with noissueleft). With<p>:delete, exit 1 with old and new present, pruned on the next pass. Success leaves exactly the newissueand keepsissuewild/iodef; a rerun makes no writes. Route53 (r53:change) and Namecheap (nc:sethosts) are unchanged. The fullprocess_domainSET_CAA run (on/off, account change, wildcard) gives the same records as before, with only the DELETE moved after the POST.set_alias --with-caawith a moved gateway,set_txt): a failed create (cf:create,linode:create,nc:sethosts,r53:change) exits 1 and keeps the old value on all four providers (before: Cloudflare and Linode were left with none). Success leaves exactly the new value, and a rerun makes no writes; Namecheap and Route53 now need one write instead of two. CNAME→A and A→CNAME switches are unchanged. The fullprocess_domainmatrix gives identical exit codes; only Cloudflare/Linode create/delete-failure cases differ, now keeping the old A/TXT.authwhen the stale-TXT delete fails (Cloudflare, Linode, Route53): the new value is written and the hook exits 0. Plainset_txtstill exits 1.