The README currently says,
A domain filter is mandatory. With --registry=txt external-dns filters deletions by owner ID, so a zone it has never written to is safe — but --registry=noop reports an empty owner ID and that filter is skipped entirely, at which point every record in a zone we report that the source does not produce becomes a deletion. There is deliberately no "manage the whole account" mode.
There is an untested third option worth exploring:
--registry=crd keeps ownership in a cluster resource rather than in the zone. For deSEC specifically that removes the single largest avoidable consumer of the write budget.
Why --registry=crd is friendlier to the rate limit
- Ownership TXT records are RRset writes, counted against
dns_api_per_domain_expensive (300/day, per zone) exactly like the records the user asked for. With --txt-prefix=externaldns-%{record_type}. that is one extra RRset per record type per name. Under crd there are none.
- It removes the apex artefact outright. The
--txt-prefix advice in the same README section exists only because the default prefix names an apex endpoint's companion record outside the zone (external-dns#5010). No companion record, no footgun.
NewCRDRegistry rejects an empty owner ID at construction (registry/crd/crd.go:69), so the deletion filter can never be skipped. That is a stronger guarantee than txt gives, and the opposite of noop, which is what the paragraph warns about.
Cost, for the docs to state honestly: ownership becomes cluster-local. Lose the cluster and you lose it, whereas TXT records can be read back out of the zone.
What is already verified
- The registry is provider-agnostic.
registry/factory/registry.go maps RegistryCRD: crd.New with no provider constraint, and NewCRDRegistry takes any provider.Provider, so it wraps the webhook provider like any other. Nothing in the webhook protocol changes.
- The resource is
DNSRecord in externaldns.k8s.io/v1alpha1 (apis/v1alpha1/dnsrecord.go). Not DNSEndpoint, which is the CRD source; the two are easy to confuse.
- The registry reads through a controller-runtime informer cache and writes through the API server, and sets status conditions on each record. It skips records whose
Ready condition is not true (crd.go:155), which is behaviour with no analogue under txt and is worth understanding before recommending it.
Two things that will block a naive attempt
It is unreleased. registry/crd does not exist at v0.21.0, the latest release (2026-04-06): the flag enum there is txt, noop, dynamodb, aws-sd. The registry landed on master on 2026-06-21 in kubernetes-sigs/external-dns#5372. So this needs a build from master, or waiting for the next release. Worth checking again before starting.
It cannot be proven in the offline lab. The lab in docs/testing.md §3 exists precisely because external-dns runs without a cluster when the only source is fake. crd builds a REST config and talks to the API server, so it does not. This belongs under "Where a real cluster is still needed", and proving it needs kind or k3d plus the DNSRecord CRD installed.
Consequences for this codebase
Under crd, external-dns synthesizes no ownership records at all. Two behaviours we currently rely on stop being exercised by that path:
Both must stay, since txt is the default and the one most people will run. The point is only that a crd run does not cover them, so it cannot replace the existing tests.
The task
- Prove the path works: a kind cluster, external-dns from master,
--registry=crd --source=fake, against this webhook and a deSEC test account. Confirm a second reconcile writes nothing.
- Make whatever adjustments that turns up. None are expected, since the registry sits above the provider and the webhook contract is unchanged, but "expected" is the reason this issue says untested.
- Update the docs to recommend it, with the cluster-local ownership trade-off stated. The README is generated, so the edit goes in
crates/external-dns-desec-webhook/src/lib.rs and then just readme; docs/rate-limits.md should say what it saves against the per-zone budget.
The README currently says,
There is an untested third option worth exploring:
--registry=crdkeeps ownership in a cluster resource rather than in the zone. For deSEC specifically that removes the single largest avoidable consumer of the write budget.Why
--registry=crdis friendlier to the rate limitdns_api_per_domain_expensive(300/day, per zone) exactly like the records the user asked for. With--txt-prefix=externaldns-%{record_type}.that is one extra RRset per record type per name. Undercrdthere are none.--txt-prefixadvice in the same README section exists only because the default prefix names an apex endpoint's companion record outside the zone (external-dns#5010). No companion record, no footgun.NewCRDRegistryrejects an empty owner ID at construction (registry/crd/crd.go:69), so the deletion filter can never be skipped. That is a stronger guarantee thantxtgives, and the opposite ofnoop, which is what the paragraph warns about.Cost, for the docs to state honestly: ownership becomes cluster-local. Lose the cluster and you lose it, whereas TXT records can be read back out of the zone.
What is already verified
registry/factory/registry.gomapsRegistryCRD: crd.Newwith no provider constraint, andNewCRDRegistrytakes anyprovider.Provider, so it wraps the webhook provider like any other. Nothing in the webhook protocol changes.DNSRecordinexternaldns.k8s.io/v1alpha1(apis/v1alpha1/dnsrecord.go). NotDNSEndpoint, which is the CRD source; the two are easy to confuse.Readycondition is not true (crd.go:155), which is behaviour with no analogue undertxtand is worth understanding before recommending it.Two things that will block a naive attempt
It is unreleased.
registry/crddoes not exist atv0.21.0, the latest release (2026-04-06): the flag enum there istxt, noop, dynamodb, aws-sd. The registry landed on master on 2026-06-21 in kubernetes-sigs/external-dns#5372. So this needs a build from master, or waiting for the next release. Worth checking again before starting.It cannot be proven in the offline lab. The lab in
docs/testing.md§3 exists precisely because external-dns runs without a cluster when the only source isfake.crdbuilds a REST config and talks to the API server, so it does not. This belongs under "Where a real cluster is still needed", and proving it needs kind or k3d plus theDNSRecordCRD installed.Consequences for this codebase
Under
crd, external-dns synthesizes no ownership records at all. Two behaviours we currently rely on stop being exercised by that path:plan::resolve, which exists because ownership records arrive withTTL 0and bypass/adjustendpoints(Ownership TXT records never reach/adjustendpoints#2)plan.rs, which exists for the apex artefactBoth must stay, since
txtis the default and the one most people will run. The point is only that acrdrun does not cover them, so it cannot replace the existing tests.The task
--registry=crd --source=fake, against this webhook and a deSEC test account. Confirm a second reconcile writes nothing.crates/external-dns-desec-webhook/src/lib.rsand thenjust readme;docs/rate-limits.mdshould say what it saves against the per-zone budget.