Skip to content

fix(cloudflare): send CAA records as structured data - #6962

Open
maheshrijal wants to merge 1 commit into
kubernetes-sigs:masterfrom
maheshrijal:fix/cloudflare-caa-records
Open

maheshrijal wants to merge 1 commit into
kubernetes-sigs:masterfrom
maheshrijal:fix/cloudflare-caa-records

Conversation

@maheshrijal

@maheshrijal maheshrijal commented Nov 13, 2026 •

Copy link
Copy Markdown

What does it do ?

Cloudflare rejects a CAA record sent as content with flags is a required data field. The provider now sends data.flags, data.tag, and data.value on all four write paths: individual create, individual update, batch post, and batch put. SRV, NAPTR, and TLSA already work this way.

The PR also:

  • Adds CAA to KnownRecordTypes and to the recordType enum, then runs make crd. Since feat(crd): validate DNSEndpoint schema at admission #6620, the CRD rejects recordType: CAA at admission, so a provider fix alone does not help users on v0.23.1 or later.
  • Builds the read-back target from Cloudflare's structured data, and canonicalizes desired targets in AdjustEndpoints. A second sync plans no changes.
  • Adds endpoint.NewCAARecord to parse <flags> <tag> "<value>".
  • Adds caa- ownership TXT names to the mapper, keeps trailing dots in CAA values, and turns off proxying for CAA.

NewCAARecord does not use miekg/dns, unlike NAPTR. On an unquoted value, miekg/dns treats ; as a comment, so 0 issue letsencrypt.org; validationmethods=dns-01 loses its parameters. It also keeps DNS escapes in the value, and it caps the value at 255 octets, which RFC 8659 does not. CAA has one free-text field at the end, so a small parser is enough. It rejects values that contain " or \. Endpoint.Validate runs this parser for every provider, so a CAA target with " or \ is skipped with a warning everywhere, not only on Cloudflare.

make crd updates only config/crd/standard. The chart CRDs follow with the chart release, as docs/contributing/dev-guide.md describes. endpoint/crd_schema_test.go therefore lets only the types in chartPendingRecordTypes (here, CAA) be missing from the chart CRDs, and the make crd-chart PR empties that list. Until that chart release, a Helm install still rejects recordType: CAA. If you prefer to update the chart CRDs in this PR instead, I can change it.

Motivation

Fixes #6961.

Real-zone test

I ran --source=crd --registry=txt --policy=sync --once against a new Cloudflare zone, with --zone-id-filter set to that zone.

  • The current CRD accepts the DNSEndpoint from Cloudflare: Unable to create CAA records #6961. Before, it failed with Unsupported value: "CAA".
  • A v0.23.0 build fails with the error from the issue. This branch creates both records with structured data.
  • The second sync logs All records are already up to date.
  • A TTL change, value changes (128 issuewild ";", an unquoted iodef, validationmethods=dns-01), and deletion all apply, and each next sync plans no changes.
  • Cloudflare stores the tag as written, so 0 ISSUE "letsencrypt.org" does not cause repeated updates.

At a zone apex, the ownership TXT needs --txt-prefix=<prefix>-%{record_type}., as docs/registry/txt.md describes for every apex record. Without it, the TXT name falls outside the zone (#6955).

Commands and logs

The test zone was a throwaway zone, deleted after the run. The zone ID is redacted.

apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: caa
  namespace: default
spec:
  endpoints:
    - dnsName: extdns-caa-test-0e8ed292.com
      recordTTL: 300
      recordType: CAA
      targets:
        - '0 iodef "mailto:security@extdns-caa-test-0e8ed292.com"'
        - '0 issue "letsencrypt.org; validationmethods=dns-01"'

Current CRD on master:

$ kubectl apply -f caa.yaml
The DNSEndpoint "caa" is invalid:
* spec.endpoints[0].recordType: Unsupported value: "CAA": supported values: "A", "AAAA", "CNAME", "DNAME", "MX", "NAPTR", "NS", "PTR", "SRV", "TLSA", "TXT"

v0.23.0 build and CRD, same flags as below:

level=error msg="fallback: individual CREATE failed: POST \"https://api.cloudflare.com/client/v4/zones/<zone-id>/dns_records\": 400 Bad Request {\"result\":null,\"success\":false,\"errors\":[{\"code\":9101,\"message\":\"flags is a required data field.\"},{\"code\":9101,\"message\":\"tag is a required data field.\"},{\"code\":9101,\"message\":\"value is a required data field.\"}],\"messages\":[]}" action=CREATE content="0 issue \"letsencrypt.org; validationmethods=dns-01\"" record=extdns-caa-test-0e8ed292.com type=CAA zone=<zone-id>

This branch:

external-dns --once --source=crd --provider=cloudflare \
  --zone-id-filter=<zone-id> --domain-filter=extdns-caa-test-0e8ed292.com \
  --managed-record-types=CAA --registry=txt --txt-owner-id=caa-test \
  --txt-prefix='_extdns-%{record_type}.' --policy=sync
# first sync
level=info msg="Changing record." action=CREATE record=extdns-caa-test-0e8ed292.com ttl=300 type=CAA zone=<zone-id>
level=info msg="Changing record." action=CREATE record=extdns-caa-test-0e8ed292.com ttl=300 type=CAA zone=<zone-id>
level=info msg="Changing record." action=CREATE record=_extdns-caa.extdns-caa-test-0e8ed292.com ttl=1 type=TXT zone=<zone-id>
# second sync
level=info msg="All records are already up to date"
# recordTTL 300 -> 600
level=info msg="Changing record." action=UPDATE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=UPDATE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=UPDATE record=_extdns-caa.extdns-caa-test-0e8ed292.com ttl=1 type=TXT zone=<zone-id>
level=info msg="All records are already up to date"
# targets -> 0 issue "letsencrypt.org", 128 issuewild ";", 0 iodef mailto:... (unquoted)
level=info msg="Changing record." action=DELETE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=CREATE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=CREATE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=UPDATE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="All records are already up to date"
# kubectl delete dnsendpoint caa
level=info msg="Changing record." action=DELETE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=DELETE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=DELETE record=extdns-caa-test-0e8ed292.com ttl=600 type=CAA zone=<zone-id>
level=info msg="Changing record." action=DELETE record=_extdns-caa.extdns-caa-test-0e8ed292.com ttl=1 type=TXT zone=<zone-id>

Records as Cloudflare stores them after the target change:

{"content":"0 issue \"letsencrypt.org\"","data":{"flags":0,"tag":"issue","value":"letsencrypt.org"}}
{"content":"128 issuewild \";\"","data":{"flags":128,"tag":"issuewild","value":";"}}
{"content":"0 iodef \"mailto:security@extdns-caa-test-0e8ed292.com\"","data":{"flags":0,"tag":"iodef","value":"mailto:security@extdns-caa-test-0e8ed292.com"}}

Checks: go test ./... passes. golangci-lint run reports 0 issues on the touched packages. make crd is a no-op.

More

  • Yes, this PR title follows Conventional Commits
  • Yes, I added unit tests
  • Yes, I updated end user documentation accordingly

AI disclosure: this PR was written with the assistance of generative AI (Claude Opus 5.5 in Claude Code), with GPT-6.1 Sol used for code review. I reviewed the changes and ran the test above against a throwaway Cloudflare zone in my account.

@kubernetes-prow kubernetes-prow Bot added the do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. label Nov 13, 2026
@kubernetes-prow

Copy link
Copy Markdown
Contributor

Welcome @maheshrijal!

It looks like this is your first PR to kubernetes-sigs/external-dns 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/external-dns has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@kubernetes-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign vflaux for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow
kubernetes-prow Bot requested review from szuecs and vflaux November 13, 2026 17:30
@kubernetes-prow kubernetes-prow Bot added apis Issues or PRs related to API change needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Nov 13, 2026
@kubernetes-prow

Copy link
Copy Markdown
Contributor

Hi @maheshrijal. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubernetes-prow kubernetes-prow Bot added docs cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. provider Issues or PRs related to a provider registry Issues or PRs related to a registry source size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Nov 13, 2026
Cloudflare rejects a CAA record sent as content with "flags is a
required data field". Send data.flags, data.tag, and data.value on
individual create and update, and on batch post and put, as SRV, NAPTR,
and TLSA already do.

Add CAA to KnownRecordTypes and to the recordType enum. Since v0.23.1 the
CRD rejects recordType: CAA at admission, so the provider fix alone does
not reach users. make crd updates config/crd/standard only. The chart
CRDs follow with the chart release, so the schema test lets only the
types in chartPendingRecordTypes be missing from them.

Build the read-back target from Cloudflare's structured data and
canonicalize desired targets in AdjustEndpoints, so a second sync plans
no changes.

Parse targets with endpoint.NewCAARecord instead of miekg/dns. On an
unquoted value, miekg/dns treats ';' as a comment and drops CAA
parameters such as validationmethods=dns-01. It also keeps DNS escapes
and caps the value at 255 octets, which RFC 8659 does not.

Also map caa- ownership TXT names, keep trailing dots in CAA values,
and turn off proxying for CAA.

Signed-off-by: maheshrijal <62394512+maheshrijal@users.noreply.github.com>
@maheshrijal
maheshrijal force-pushed the fix/cloudflare-caa-records branch from 9af2a95 to 8aa986a Compare November 13, 2026 17:31
@kubernetes-prow kubernetes-prow Bot removed the do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. label Nov 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apis Issues or PRs related to API change cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. docs needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. provider Issues or PRs related to a provider registry Issues or PRs related to a registry size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare: Unable to create CAA records

1 participant