unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
backwards-compatibilityconfiguration-managementerror-handling

Do not require recursive nameservers propagation by default for the DNS-01 challenge

Project: traefik/traefik | Language: Go | Concepts: backwards-compatibility, configuration-management, error-handling | Difficulty: mid
Link: https://github.com/traefik/traefik/pull/13710

The problem

After upgrading the lego library to v5, the default behavior for DNS-01 challenge propagation checks became stricter: Traefik started requiring that recursive nameservers (RNS) see the challenge TXT record by default. This broke certificate issuance in environments where the default recursive resolver (like Docker's embedded 127.0.0.11) couldn't see the record, even though the authoritative nameservers had the correct data. This was a breaking change for users who hadn't explicitly configured propagation settings.

Why it's non-trivial

A naive fix might simply revert to the old behavior or hardcode a default, but this could ignore:

  • The need for backwards compatibility with existing user configurations.
  • The importance of allowing users to opt into the stricter propagation check if desired.
  • Correct mapping and merging of deprecated and new configuration fields.
  • Ensuring that the fix works across all configuration and serialization paths, including tests and documentation.

The approach

1. Change Propagation from pointer to value, and remove nil checks

Previously, the Propagation field was a pointer, allowing for a nil value to indicate "unset." The PR changes this to a value, so it always exists and defaults are always present.

-type DNSChallenge struct {
-	Provider    string       `description:"Use a DNS-01 based challenge provider rather than HTTPS." json:"provider,omitempty" toml:"provider,omitempty" yaml:"provider,omitempty" export:"true"`
-	Resolvers   []string     `description:"Use following DNS servers to resolve the FQDN authority." json:"resolvers,omitempty" toml:"resolvers,omitempty" yaml:"resolvers,omitempty"`
-	Propagation *Propagation `description:"DNS propagation checks configuration" json:"propagation,omitempty" toml:"propagation,omitempty" yaml:"propagation,omitempty"  label:"allowEmpty" file:"allowEmpty" export:"true"`
+type DNSChallenge struct {
+	Provider    string      `description:"Use a DNS-01 based challenge provider rather than HTTPS." json:"provider,omitempty" toml:"provider,omitempty" yaml:"provider,omitempty" export:"true"`
+	Resolvers   []string    `description:"Use following DNS servers to resolve the FQDN authority." json:"resolvers,omitempty" toml:"resolvers,omitempty" yaml:"resolvers,omitempty"`
+	Propagation Propagation `description:"DNS propagation checks configuration" json:"propagation,omitempty" toml:"propagation,omitempty" yaml:"propagation,omitempty"  label:"allowEmpty" file:"allowEmpty" export:"true"`

This ensures that all code paths have a Propagation value, avoiding nil-pointer errors and simplifying logic.

2. Remove unnecessary pointer logic in config merging

Previously, code had to check if Propagation was nil before setting fields (due to it being a pointer). Now, those checks are removed:

-			if resolver.ACME.DNSChallenge.Propagation == nil {
-				resolver.ACME.DNSChallenge.Propagation = &acmeprovider.Propagation{}
-			}

This is deleted throughout, since Propagation is now always present.

3. Restore old default for recursive nameserver propagation

The core behavioral change: Traefik now disables the strict recursive nameserver propagation check unless explicitly enabled by the user.

-			dns01.LazyCondOption(propagation != nil, func() dns01.ChallengeOption {
-				return dns01.CombineOptions(
-					dns01.CondOptions(propagation.DisableANSChecks, dns01.DisableAuthoritativeNssPropagationRequirement()),
-					dns01.CondOptions(!propagation.RequireAllRNS, dns01.DisableRecursiveNSsPropagationRequirement()),
-					dns01.PropagationWait(time.Duration(propagation.DelayBeforeChecks), propagation.DisableChecks),
-				)
-			}),
+			dns01.CondOptions(propagation.DisableANSChecks, dns01.DisableAuthoritativeNssPropagationRequirement()),
+			dns01.CondOptions(!propagation.RequireAllRNS, dns01.DisableRecursiveNSsPropagationRequirement()),
+			dns01.PropagationWait(time.Duration(propagation.DelayBeforeChecks), propagation.DisableChecks),

This means that unless RequireAllRNS is set to true by the user, Traefik disables the requirement that all recursive nameservers see the challenge record, restoring the previous default.

4. Update tests and documentation to match new structure

All tests and docs that referenced Propagation as a pointer are updated to use the value form:

-	Propagation: &acme.Propagation{
+	Propagation: acme.Propagation{

This ensures consistency and prevents test failures due to type mismatches.

From the review thread

amazon7737: One thing I'd like a second opinion on before this moves forward.

One could argue lego v5's stricter propagation default is intentional and Traefik should follow it, in which case this is a documentation gap rather than a code bug, and the right change would be documenting the new behavior instead of restoring the old default. My reading is that the documented requireAllRNS default should win.

What informs my reading is the release line. This change shipped in a patch release. A default DNS-01 configuration that obtained certificates in 3.7.5 fails in 3.7.6 without any configuration change. Discussing whether the stricter lego default is better belongs to a minor or major release. A patch release should not change the behavior of a documented default configuration.

I'd like to hear how you see it.

rtribotte: @amazon7737 I think reverting to the previous behavior is the right thing to do. I believe this change was unintended in the patch version.

vpmv: @rtribotte What's holding this PR back? All releases after 3.7.5 aren't able to upgrade certificates anymore.

Takeaways

  • Default behaviors must not change in a patch release unless strictly necessary. Breaking backwards compatibility, even if upstream dependencies change, should be avoided in patch versions to prevent unexpected outages for users.
  • Favor explicit configuration over implicit pointer/nil checks. Using value types for configuration avoids nil-pointer bugs and simplifies code, especially when defaults are important.
  • Restoring previous defaults is preferable when upstream libraries introduce stricter behaviors unexpectedly. If a dependency changes its default in a way that breaks existing workflows, the dependent project should explicitly set the desired default to maintain user expectations.