Repository: go-acme/lego
Pull request #2368 (2024-12-01 02:52:03)

fix(cli): use retryable client for ACME server calls
Related to #2367
Related to #1546
Closes #1280


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index b3383969..68fd32a6 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -249,6 +249,10 @@ issues:
       text: 'cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high'
       linters:
         - gocyclo
+    - path: cmd/cmd_renew.go
+      text: "Function 'renewForDomains' has too many statements"
+      linters:
+        - funlen
     - path: providers/dns/cpanel/cpanel.go
       text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
       linters:
diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go
index 3cce35b4..c4c68023 100644
--- a/cmd/cmd_renew.go
+++ b/cmd/cmd_renew.go
@@ -123,8 +123,7 @@ func createRenew() *cli.Command {
 }
 
 func renew(ctx *cli.Context) error {
-	account, client := setup(ctx, NewAccountsStorage(ctx))
-	setupChallenges(ctx, client)
+	account, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
 
 	if account.Registration == nil {
 		log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
@@ -138,14 +137,14 @@ func renew(ctx *cli.Context) error {
 
 	// CSR
 	if ctx.IsSet(flgCSR) {
-		return renewForCSR(ctx, client, certsStorage, bundle, meta)
+		return renewForCSR(ctx, account, keyType, certsStorage, bundle, meta)
 	}
 
 	// Domains
-	return renewForDomains(ctx, client, certsStorage, bundle, meta)
+	return renewForDomains(ctx, account, keyType, certsStorage, bundle, meta)
 }
 
-func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
+func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
 	domains := ctx.StringSlice(flgDomains)
 	domain := domains[0]
 
@@ -162,7 +161,11 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
 	var ariRenewalTime *time.Time
 	var replacesCertID string
 
+	var client *lego.Client
+
 	if !ctx.Bool(flgARIDisable) {
+		client = setupClient(ctx, account, keyType)
+
 		ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
 		if ariRenewalTime != nil {
 			now := time.Now().UTC()
@@ -189,6 +192,10 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
 		return nil
 	}
 
+	if client == nil {
+		client = setupClient(ctx, account, keyType)
+	}
+
 	// This is just meant to be informal for the user.
 	timeLeft := cert.NotAfter.Sub(time.Now().UTC())
 	log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
@@ -250,7 +257,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
 	return launchHook(ctx.String(flgRenewHook), meta)
 }
 
-func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
+func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
 	csr, err := readCSRFile(ctx.String(flgCSR))
 	if err != nil {
 		log.Fatal(err)
@@ -274,7 +281,11 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
 	var ariRenewalTime *time.Time
 	var replacesCertID string
 
+	var client *lego.Client
+
 	if !ctx.Bool(flgARIDisable) {
+		client = setupClient(ctx, account, keyType)
+
 		ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
 		if ariRenewalTime != nil {
 			now := time.Now().UTC()
@@ -296,6 +307,10 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
 		return nil
 	}
 
+	if client == nil {
+		client = setupClient(ctx, account, keyType)
+	}
+
 	// This is just meant to be informal for the user.
 	timeLeft := cert.NotAfter.Sub(time.Now().UTC())
 	log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
diff --git a/cmd/cmd_revoke.go b/cmd/cmd_revoke.go
index 2ecfd301..667bebe1 100644
--- a/cmd/cmd_revoke.go
+++ b/cmd/cmd_revoke.go
@@ -38,12 +38,14 @@ func createRevoke() *cli.Command {
 }
 
 func revoke(ctx *cli.Context) error {
-	acc, client := setup(ctx, NewAccountsStorage(ctx))
+	account, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
 
-	if acc.Registration == nil {
-		log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
+	if account.Registration == nil {
+		log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
 	}
 
+	client := newClient(ctx, account, keyType)
+
 	certsStorage := NewCertificatesStorage(ctx)
 	certsStorage.CreateRootFolder()
 
diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go
index a1d4cd51..f2cec565 100644
--- a/cmd/cmd_run.go
+++ b/cmd/cmd_run.go
@@ -93,8 +93,9 @@ backups of this folder is ideal.
 func run(ctx *cli.Context) error {
 	accountsStorage := NewAccountsStorage(ctx)
 
-	account, client := setup(ctx, accountsStorage)
-	setupChallenges(ctx, client)
+	account, keyType := setupAccount(ctx, accountsStorage)
+
+	client := setupClient(ctx, account, keyType)
 
 	if account.Registration == nil {
 		reg, err := register(ctx, client)
diff --git a/cmd/setup.go b/cmd/setup.go
index 4a802ba1..3fc05038 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"crypto/tls"
 	"crypto/x509"
 	"encoding/pem"
 	"fmt"
@@ -14,12 +13,22 @@ import (
 	"github.com/go-acme/lego/v4/lego"
 	"github.com/go-acme/lego/v4/log"
 	"github.com/go-acme/lego/v4/registration"
+	"github.com/hashicorp/go-retryablehttp"
 	"github.com/urfave/cli/v2"
 )
 
 const filePerm os.FileMode = 0o600
 
-func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
+// setupClient creates a new client with challenge settings.
+func setupClient(ctx *cli.Context, account *Account, keyType certcrypto.KeyType) *lego.Client {
+	client := newClient(ctx, account, keyType)
+
+	setupChallenges(ctx, client)
+
+	return client
+}
+
+func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) {
 	keyType := getKeyType(ctx)
 	privateKey := accountsStorage.GetPrivateKey(keyType)
 
@@ -30,9 +39,7 @@ func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.
 		account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
 	}
 
-	client := newClient(ctx, account, keyType)
-
-	return account, client
+	return account, keyType
 }
 
 func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
@@ -51,11 +58,20 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
 	}
 
 	if ctx.Bool(flgTLSSkipVerify) {
-		config.HTTPClient.Transport = &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		defaultTransport, ok := config.HTTPClient.Transport.(*http.Transport)
+		if ok { // This is always true because the default client used by the CLI defined the transport.
+			tr := defaultTransport.Clone()
+			tr.TLSClientConfig.InsecureSkipVerify = true
+			config.HTTPClient.Transport = tr
 		}
 	}
 
+	retryClient := retryablehttp.NewClient()
+	retryClient.RetryMax = 5
+	retryClient.HTTPClient = config.HTTPClient
+
+	config.HTTPClient = retryClient.StandardClient()
+
 	client, err := lego.NewClient(config)
 	if err != nil {
 		log.Fatalf("Could not create client: %v", err)
diff --git a/providers/dns/azuredns/azuredns.go b/providers/dns/azuredns/azuredns.go
index dd591d92..860d1969 100644
--- a/providers/dns/azuredns/azuredns.go
+++ b/providers/dns/azuredns/azuredns.go
@@ -278,12 +278,3 @@ func getZoneName(config *Config, fqdn string) (string, error) {
 
 	return authZone, nil
 }
-
-func deref[T any](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
-	}
-
-	return *v
-}
diff --git a/providers/dns/azuredns/private.go b/providers/dns/azuredns/private.go
index c3d6cf35..24fb3d5e 100644
--- a/providers/dns/azuredns/private.go
+++ b/providers/dns/azuredns/private.go
@@ -14,6 +14,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"
 	"github.com/go-acme/lego/v4/challenge"
 	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 )
 
 var _ challenge.ProviderTimeout = (*DNSProviderPrivate)(nil)
@@ -184,7 +185,7 @@ func privateUniqueRecords(recordSet armprivatedns.RecordSet, value string) map[s
 		for _, txtRecord := range recordSet.Properties.TxtRecords {
 			// Assume Value doesn't contain multiple strings
 			if len(txtRecord.Value) > 0 {
-				uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
+				uniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{}
 			}
 		}
 	}
diff --git a/providers/dns/azuredns/public.go b/providers/dns/azuredns/public.go
index f6c00b2a..f7e46150 100644
--- a/providers/dns/azuredns/public.go
+++ b/providers/dns/azuredns/public.go
@@ -14,6 +14,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
 	"github.com/go-acme/lego/v4/challenge"
 	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 )
 
 var _ challenge.ProviderTimeout = (*DNSProviderPublic)(nil)
@@ -182,7 +183,7 @@ func publicUniqueRecords(recordSet armdns.RecordSet, value string) map[string]st
 		for _, txtRecord := range recordSet.Properties.TxtRecords {
 			// Assume Value doesn't contain multiple strings
 			if len(txtRecord.Value) > 0 {
-				uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
+				uniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{}
 			}
 		}
 	}
diff --git a/providers/dns/azuredns/servicediscovery.go b/providers/dns/azuredns/servicediscovery.go
index 62dfd662..882e1924 100644
--- a/providers/dns/azuredns/servicediscovery.go
+++ b/providers/dns/azuredns/servicediscovery.go
@@ -9,6 +9,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 )
 
 type ServiceDiscoveryZone struct {
@@ -88,7 +89,7 @@ func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.To
 		*requestOptions.Skip += ResourceGraphQueryOptionsTop
 
 		if result.TotalRecords != nil {
-			if int64(deref(requestOptions.Skip)) >= deref(result.TotalRecords) {
+			if int64(ptr.Deref(requestOptions.Skip)) >= ptr.Deref(result.TotalRecords) {
 				break
 			}
 		}
diff --git a/providers/dns/bunny/bunny.go b/providers/dns/bunny/bunny.go
index 63a5a01e..9716a20c 100644
--- a/providers/dns/bunny/bunny.go
+++ b/providers/dns/bunny/bunny.go
@@ -5,12 +5,16 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"slices"
 	"time"
 
 	"github.com/go-acme/lego/v4/challenge"
 	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/platform/config/env"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
+	"github.com/miekg/dns"
 	"github.com/nrdcg/bunny-go"
+	"golang.org/x/net/publicsuffix"
 )
 
 // Environment variables names.
@@ -94,32 +98,27 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 	info := dns01.GetChallengeInfo(domain, keyAuth)
 
-	authZone, err := getZoneName(info.EffectiveFQDN)
-	if err != nil {
-		return fmt.Errorf("bunny: could not find zone for domain %q: %w", domain, err)
-	}
-
 	ctx := context.Background()
 
-	zone, err := d.findZone(ctx, authZone)
+	zone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))
 	if err != nil {
 		return fmt.Errorf("bunny: %w", err)
 	}
 
-	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain))
 	if err != nil {
 		return fmt.Errorf("bunny: %w", err)
 	}
 
 	record := &bunny.AddOrUpdateDNSRecordOptions{
-		Type:  pointer(bunny.DNSRecordTypeTXT),
-		Name:  pointer(subDomain),
-		Value: pointer(info.Value),
-		TTL:   pointer(int32(d.config.TTL)),
+		Type:  ptr.Pointer(bunny.DNSRecordTypeTXT),
+		Name:  ptr.Pointer(subDomain),
+		Value: ptr.Pointer(info.Value),
+		TTL:   ptr.Pointer(int32(d.config.TTL)),
 	}
 
-	if _, err := d.client.DNSZone.AddDNSRecord(ctx, deref(zone.ID), record); err != nil {
-		return fmt.Errorf("bunny: failed to add TXT record: fqdn=%s, zoneID=%d: %w", info.EffectiveFQDN, deref(zone.ID), err)
+	if _, err := d.client.DNSZone.AddDNSRecord(ctx, ptr.Deref(zone.ID), record); err != nil {
+		return fmt.Errorf("bunny: failed to add TXT record: fqdn=%s, zoneID=%d: %w", info.EffectiveFQDN, ptr.Deref(zone.ID), err)
 	}
 
 	return nil
@@ -129,26 +128,21 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 	info := dns01.GetChallengeInfo(domain, keyAuth)
 
-	authZone, err := getZoneName(info.EffectiveFQDN)
-	if err != nil {
-		return fmt.Errorf("bunny: could not find zone for domain %q: %w", domain, err)
-	}
-
 	ctx := context.Background()
 
-	zone, err := d.findZone(ctx, authZone)
+	zone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))
 	if err != nil {
 		return fmt.Errorf("bunny: %w", err)
 	}
 
-	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain))
 	if err != nil {
 		return fmt.Errorf("bunny: %w", err)
 	}
 
 	var record *bunny.DNSRecord
 	for _, r := range zone.Records {
-		if deref(r.Name) == subDomain && deref(r.Type) == bunny.DNSRecordTypeTXT {
+		if ptr.Deref(r.Name) == subDomain && ptr.Deref(r.Type) == bunny.DNSRecordTypeTXT {
 			r := r
 			record = &r
 			break
@@ -156,11 +150,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 	}
 
 	if record == nil {
-		return fmt.Errorf("bunny: could not find TXT record zone=%d, subdomain=%s", deref(zone.ID), subDomain)
+		return fmt.Errorf("bunny: could not find TXT record zone=%d, subdomain=%s", ptr.Deref(zone.ID), subDomain)
 	}
 
-	if err := d.client.DNSZone.DeleteDNSRecord(ctx, deref(zone.ID), deref(record.ID)); err != nil {
-		return fmt.Errorf("bunny: failed to delete TXT record: id=%d, name=%s: %w", deref(record.ID), deref(record.Name), err)
+	if err := d.client.DNSZone.DeleteDNSRecord(ctx, ptr.Deref(zone.ID), ptr.Deref(record.ID)); err != nil {
+		return fmt.Errorf("bunny: failed to delete TXT record: id=%d, name=%s: %w", ptr.Deref(record.ID), ptr.Deref(record.Name), err)
 	}
 
 	return nil
@@ -172,37 +166,51 @@ func (d *DNSProvider) findZone(ctx context.Context, authZone string) (*bunny.DNS
 		return nil, err
 	}
 
-	var zone *bunny.DNSZone
-	for _, item := range zones.Items {
-		if item != nil && deref(item.Domain) == authZone {
-			zone = item
-			break
-		}
-	}
-
+	zone := findZone(zones, authZone)
 	if zone == nil {
-		return nil, fmt.Errorf("could not find DNSZone zone=%s", authZone)
+		return nil, fmt.Errorf("could not find DNSZone domain=%s", authZone)
 	}
 
 	return zone, nil
 }
 
-func getZoneName(fqdn string) (string, error) {
-	authZone, err := dns01.FindZoneByFqdn(fqdn)
-	if err != nil {
-		return "", err
+func findZone(zones *bunny.DNSZones, domain string) *bunny.DNSZone {
+	domains := possibleDomains(domain)
+
+	var domainLength int
+
+	var zone *bunny.DNSZone
+	for _, item := range zones.Items {
+		if item == nil {
+			continue
+		}
+
+		curr := ptr.Deref(item.Domain)
+
+		if slices.Contains(domains, curr) && domainLength < len(curr) {
+			domainLength = len(curr)
+
+			zone = item
+		}
 	}
 
-	return dns01.UnFqdn(authZone), nil
+	return zone
 }
 
-func pointer[T string | int | int32 | int64](v T) *T { return &v }
+func possibleDomains(domain string) []string {
+	var domains []string
+
+	labelIndexes := dns.Split(domain)
+
+	for _, index := range labelIndexes {
+		tld, _ := publicsuffix.PublicSuffix(domain)
+		if tld == domain[index:] {
+			// skip the TLD
+			break
+		}
 
-func deref[T string | int | int32 | int64](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
+		domains = append(domains, dns01.UnFqdn(domain[index:]))
 	}
 
-	return *v
+	return domains
 }
diff --git a/providers/dns/huaweicloud/huaweicloud.go b/providers/dns/huaweicloud/huaweicloud.go
index 9d20c27a..fbb594f5 100644
--- a/providers/dns/huaweicloud/huaweicloud.go
+++ b/providers/dns/huaweicloud/huaweicloud.go
@@ -13,6 +13,7 @@ import (
 	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/platform/config/env"
 	"github.com/go-acme/lego/v4/platform/wait"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 	hwauthbasic "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
 	hwconfig "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config"
 	hwdns "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2"
@@ -155,7 +156,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 			return false, fmt.Errorf("show record set: %w", errShow)
 		}
 
-		return !strings.HasSuffix(deref(rs.Status), "PENDING_"), nil
+		return !strings.HasSuffix(ptr.Deref(rs.Status), "PENDING_"), nil
 	})
 	if err != nil {
 		return fmt.Errorf("huaweicloud: %w", err)
@@ -208,7 +209,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.ChallengeInfo) (string, error) {
 	records, err := d.client.ListRecordSetsByZone(&hwmodel.ListRecordSetsByZoneRequest{
 		ZoneId: zoneID,
-		Name:   pointer(info.EffectiveFQDN),
+		Name:   ptr.Pointer(info.EffectiveFQDN),
 	})
 	if err != nil {
 		return "", fmt.Errorf("record list: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err)
@@ -216,8 +217,8 @@ func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.C
 
 	var existingRecordSet *hwmodel.ListRecordSets
 
-	for _, record := range deref(records.Recordsets) {
-		if deref(record.Type) == "TXT" && deref(record.Name) == info.EffectiveFQDN {
+	for _, record := range ptr.Deref(records.Recordsets) {
+		if ptr.Deref(record.Type) == "TXT" && ptr.Deref(record.Name) == info.EffectiveFQDN {
 			existingRecordSet = &record
 		}
 	}
@@ -229,9 +230,9 @@ func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.C
 			ZoneId: zoneID,
 			Body: &hwmodel.CreateRecordSetRequestBody{
 				Name:        info.EffectiveFQDN,
-				Description: pointer("Added TXT record for ACME dns-01 challenge using lego client"),
+				Description: ptr.Pointer("Added TXT record for ACME dns-01 challenge using lego client"),
 				Type:        "TXT",
-				Ttl:         pointer(d.config.TTL),
+				Ttl:         ptr.Pointer(d.config.TTL),
 				Records:     []string{value},
 			},
 		}
@@ -241,18 +242,18 @@ func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.C
 			return "", fmt.Errorf("create record set: %w", errCreate)
 		}
 
-		return deref(resp.Id), nil
+		return ptr.Deref(resp.Id), nil
 	}
 
 	updateRequest := &hwmodel.UpdateRecordSetRequest{
 		ZoneId:      zoneID,
-		RecordsetId: deref(existingRecordSet.Id),
+		RecordsetId: ptr.Deref(existingRecordSet.Id),
 		Body: &hwmodel.UpdateRecordSetReq{
 			Name:        existingRecordSet.Name,
 			Description: existingRecordSet.Description,
 			Type:        existingRecordSet.Type,
 			Ttl:         existingRecordSet.Ttl,
-			Records:     pointer(append(deref(existingRecordSet.Records), value)),
+			Records:     ptr.Pointer(append(ptr.Deref(existingRecordSet.Records), value)),
 		},
 	}
 
@@ -261,7 +262,7 @@ func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.C
 		return "", fmt.Errorf("update record set: %w", err)
 	}
 
-	return deref(resp.Id), nil
+	return ptr.Deref(resp.Id), nil
 }
 
 func (d *DNSProvider) getZoneID(authZone string) (string, error) {
@@ -270,22 +271,11 @@ func (d *DNSProvider) getZoneID(authZone string) (string, error) {
 		return "", fmt.Errorf("unable to get zone: %w", err)
 	}
 
-	for _, zone := range deref(zones.Zones) {
-		if deref(zone.Name) == authZone {
-			return deref(zone.Id), nil
+	for _, zone := range ptr.Deref(zones.Zones) {
+		if ptr.Deref(zone.Name) == authZone {
+			return ptr.Deref(zone.Id), nil
 		}
 	}
 
 	return "", fmt.Errorf("zone %q not found", authZone)
 }
-
-func pointer[T any](v T) *T { return &v }
-
-func deref[T any](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
-	}
-
-	return *v
-}
diff --git a/providers/dns/infomaniak/infomaniak.go b/providers/dns/infomaniak/infomaniak.go
index 84f49421..79c6f577 100644
--- a/providers/dns/infomaniak/infomaniak.go
+++ b/providers/dns/infomaniak/infomaniak.go
@@ -47,9 +47,9 @@ type Config struct {
 func NewDefaultConfig() *Config {
 	return &Config{
 		APIEndpoint:        env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL),
-		TTL:                env.GetOrDefaultInt(EnvTTL, 7200),
-		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
-		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+		TTL:                env.GetOrDefaultInt(EnvTTL, 300),
+		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
 		HTTPClient: &http.Client{
 			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
 		},
diff --git a/providers/dns/internal/ptr/types.go b/providers/dns/internal/ptr/types.go
new file mode 100644
index 00000000..b0c7974e
--- /dev/null
+++ b/providers/dns/internal/ptr/types.go
@@ -0,0 +1,12 @@
+package ptr
+
+func Deref[T any](v *T) T {
+	if v == nil {
+		var zero T
+		return zero
+	}
+
+	return *v
+}
+
+func Pointer[T any](v T) *T { return &v }
diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go
index cefea832..f316fd54 100644
--- a/providers/dns/inwx/inwx.go
+++ b/providers/dns/inwx/inwx.go
@@ -97,14 +97,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 
 // Present creates a TXT record using the specified parameters.
 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
-	challengeInfo := dns01.GetChallengeInfo(domain, keyAuth)
+	info := dns01.GetChallengeInfo(domain, keyAuth)
 
-	authZone, err := dns01.FindZoneByFqdn(challengeInfo.EffectiveFQDN)
+	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 	if err != nil {
-		return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, challengeInfo.EffectiveFQDN, err)
+		return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err)
 	}
 
-	info, err := d.client.Account.Login()
+	login, err := d.client.Account.Login()
 	if err != nil {
 		return fmt.Errorf("inwx: %w", err)
 	}
@@ -116,27 +116,24 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 		}
 	}()
 
-	err = d.twoFactorAuth(info)
+	err = d.twoFactorAuth(login)
 	if err != nil {
 		return fmt.Errorf("inwx: %w", err)
 	}
 
 	request := &goinwx.NameserverRecordRequest{
 		Domain:  dns01.UnFqdn(authZone),
-		Name:    dns01.UnFqdn(challengeInfo.EffectiveFQDN),
+		Name:    dns01.UnFqdn(info.EffectiveFQDN),
 		Type:    "TXT",
-		Content: challengeInfo.Value,
+		Content: info.Value,
 		TTL:     d.config.TTL,
 	}
 
 	_, err = d.client.Nameservers.CreateRecord(request)
 	if err != nil {
 		var er *goinwx.ErrorResponse
-		if errors.As(err, &er) {
-			if er.Message == "Object exists" {
-				return nil
-			}
-			return fmt.Errorf("inwx: %w", err)
+		if errors.As(err, &er) && er.Message == "Object exists" {
+			return nil
 		}
 
 		return fmt.Errorf("inwx: %w", err)
@@ -147,14 +144,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 
 // CleanUp removes the TXT record matching the specified parameters.
 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
-	challengeInfo := dns01.GetChallengeInfo(domain, keyAuth)
+	info := dns01.GetChallengeInfo(domain, keyAuth)
 
-	authZone, err := dns01.FindZoneByFqdn(challengeInfo.EffectiveFQDN)
+	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 	if err != nil {
-		return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, challengeInfo.EffectiveFQDN, err)
+		return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err)
 	}
 
-	info, err := d.client.Account.Login()
+	login, err := d.client.Account.Login()
 	if err != nil {
 		return fmt.Errorf("inwx: %w", err)
 	}
@@ -166,29 +163,40 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 		}
 	}()
 
-	err = d.twoFactorAuth(info)
+	err = d.twoFactorAuth(login)
 	if err != nil {
 		return fmt.Errorf("inwx: %w", err)
 	}
 
 	response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{
 		Domain: dns01.UnFqdn(authZone),
-		Name:   dns01.UnFqdn(challengeInfo.EffectiveFQDN),
+		Name:   dns01.UnFqdn(info.EffectiveFQDN),
 		Type:   "TXT",
 	})
 	if err != nil {
 		return fmt.Errorf("inwx: %w", err)
 	}
 
-	var lastErr error
+	var recordID int
 	for _, record := range response.Records {
-		err = d.client.Nameservers.DeleteRecord(record.ID)
-		if err != nil {
-			lastErr = fmt.Errorf("inwx: %w", err)
+		if record.Content != info.Value {
+			continue
 		}
+
+		recordID = record.ID
+		break
+	}
+
+	if recordID == 0 {
+		return errors.New("inwx: TXT record not found")
+	}
+
+	err = d.client.Nameservers.DeleteRecord(recordID)
+	if err != nil {
+		return fmt.Errorf("inwx: %w", err)
 	}
 
-	return lastErr
+	return nil
 }
 
 // Timeout returns the timeout and interval to use when checking for DNS propagation.
diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go
index c0a3146a..8246cd0a 100644
--- a/providers/dns/route53/route53.go
+++ b/providers/dns/route53/route53.go
@@ -21,6 +21,7 @@ import (
 	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/platform/config/env"
 	"github.com/go-acme/lego/v4/platform/wait"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 )
 
 // Environment variables names.
@@ -151,7 +152,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 
 	var found bool
 	for _, record := range records {
-		if deref(record.Value) == realValue {
+		if ptr.Deref(record.Value) == realValue {
 			found = true
 		}
 	}
@@ -196,7 +197,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 
 	var nonLegoRecords []awstypes.ResourceRecord
 	for _, record := range existingRecords {
-		if deref(record.Value) != `"`+info.Value+`"` {
+		if ptr.Deref(record.Value) != `"`+info.Value+`"` {
 			nonLegoRecords = append(nonLegoRecords, record)
 		}
 	}
@@ -255,7 +256,7 @@ func (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAc
 				return true, nil
 			}
 
-			return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID))
+			return false, fmt.Errorf("unable to retrieve change: ID=%s", ptr.Deref(changeID))
 		})
 	}
 
@@ -281,7 +282,7 @@ func (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, f
 	var records []awstypes.ResourceRecord
 
 	for _, recordSet := range recordSetsOutput.ResourceRecordSets {
-		if deref(recordSet.Name) == fqdn {
+		if ptr.Deref(recordSet.Name) == fqdn {
 			records = append(records, recordSet.ResourceRecords...)
 		}
 	}
@@ -311,8 +312,8 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string,
 	var hostedZoneID string
 	for _, hostedZone := range resp.HostedZones {
 		// .Name has a trailing dot
-		if !hostedZone.Config.PrivateZone && deref(hostedZone.Name) == authZone {
-			hostedZoneID = deref(hostedZone.Id)
+		if !hostedZone.Config.PrivateZone && ptr.Deref(hostedZone.Name) == authZone {
+			hostedZoneID = ptr.Deref(hostedZone.Id)
 			break
 		}
 	}
@@ -394,12 +395,3 @@ func createAWSConfigCheckParams(config *Config) error {
 
 	return nil
 }
-
-func deref[T string | int | int32 | int64 | bool](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
-	}
-
-	return *v
-}
diff --git a/providers/dns/volcengine/volcengine.go b/providers/dns/volcengine/volcengine.go
index 2fcba1b0..ed554459 100644
--- a/providers/dns/volcengine/volcengine.go
+++ b/providers/dns/volcengine/volcengine.go
@@ -12,6 +12,7 @@ import (
 	"github.com/go-acme/lego/v4/challenge"
 	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/platform/config/env"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 	"github.com/miekg/dns"
 	"github.com/volcengine/volc-sdk-golang/base"
 	volc "github.com/volcengine/volc-sdk-golang/service/dns"
@@ -126,16 +127,16 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 		return fmt.Errorf("volcengine: get zone ID: %w", err)
 	}
 
-	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, deref(zone.ZoneName))
+	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.ZoneName))
 	if err != nil {
 		return fmt.Errorf("volcengine: %w", err)
 	}
 
 	crr := &volc.CreateRecordRequest{
-		Host:  pointer(subDomain),
-		TTL:   pointer(int64(d.config.TTL)),
-		Type:  pointer("TXT"),
-		Value: pointer(info.Value),
+		Host:  ptr.Pointer(subDomain),
+		TTL:   ptr.Pointer(int64(d.config.TTL)),
+		Type:  ptr.Pointer("TXT"),
+		Value: ptr.Pointer(info.Value),
 		ZID:   zone.ZID,
 	}
 
@@ -178,8 +179,8 @@ func (d *DNSProvider) getZone(ctx context.Context, fqdn string) (volc.TopZoneRes
 		domain := fqdn[index:]
 
 		lzr := &volc.ListZonesRequest{
-			Key:        pointer(dns01.UnFqdn(domain)),
-			SearchMode: pointer("exact"),
+			Key:        ptr.Pointer(dns01.UnFqdn(domain)),
+			SearchMode: ptr.Pointer("exact"),
 		}
 
 		zones, err := d.client.ListZones(ctx, lzr)
@@ -187,7 +188,7 @@ func (d *DNSProvider) getZone(ctx context.Context, fqdn string) (volc.TopZoneRes
 			return volc.TopZoneResponse{}, fmt.Errorf("list zones: %w", err)
 		}
 
-		total := deref(zones.Total)
+		total := ptr.Deref(zones.Total)
 
 		if total == 0 || len(zones.Zones) == 0 {
 			continue
@@ -233,14 +234,3 @@ func newClient(config *Config) *volc.Client {
 
 	return volc.NewClient(caller)
 }
-
-func pointer[T any](v T) *T { return &v }
-
-func deref[T any](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
-	}
-
-	return *v
-}


Test patch:
diff --git a/providers/dns/bunny/bunny_test.go b/providers/dns/bunny/bunny_test.go
index e5724bcd..4cf0f6b0 100644
--- a/providers/dns/bunny/bunny_test.go
+++ b/providers/dns/bunny/bunny_test.go
@@ -4,6 +4,9 @@ import (
 	"testing"
 
 	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
+	"github.com/nrdcg/bunny-go"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -123,3 +126,117 @@ func TestLiveCleanUp(t *testing.T) {
 	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
 	require.NoError(t, err)
 }
+
+func Test_findZone(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		domain   string
+		items    []*bunny.DNSZone
+		expected *bunny.DNSZone
+	}{
+		{
+			desc:   "found subdomain",
+			domain: "_acme-challenge.foo.bar.example.com",
+			items: []*bunny.DNSZone{
+				{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")},
+				{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")},
+				{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")},
+				{ID: ptr.Pointer[int64](5), Domain: ptr.Pointer("bar.example.com")},
+				{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")},
+			},
+			expected: &bunny.DNSZone{
+				ID:     ptr.Pointer[int64](5),
+				Domain: ptr.Pointer("bar.example.com"),
+			},
+		},
+		{
+			desc:   "found the longest subdomain",
+			domain: "_acme-challenge.foo.bar.example.com",
+			items: []*bunny.DNSZone{
+				{ID: ptr.Pointer[int64](7), Domain: ptr.Pointer("foo.bar.example.com")},
+				{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")},
+				{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")},
+				{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")},
+				{ID: ptr.Pointer[int64](5), Domain: ptr.Pointer("bar.example.com")},
+				{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")},
+			},
+			expected: &bunny.DNSZone{
+				ID:     ptr.Pointer[int64](7),
+				Domain: ptr.Pointer("foo.bar.example.com"),
+			},
+		},
+		{
+			desc:   "found apex",
+			domain: "_acme-challenge.foo.bar.example.com",
+			items: []*bunny.DNSZone{
+				{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")},
+				{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")},
+				{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")},
+				{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")},
+			},
+			expected: &bunny.DNSZone{
+				ID:     ptr.Pointer[int64](1),
+				Domain: ptr.Pointer("example.com"),
+			},
+		},
+		{
+			desc:   "not found",
+			domain: "_acme-challenge.foo.bar.example.com",
+			items: []*bunny.DNSZone{
+				{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")},
+				{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")},
+				{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			zones := &bunny.DNSZones{Items: test.items}
+
+			zone := findZone(zones, test.domain)
+
+			assert.Equal(t, test.expected, zone)
+		})
+	}
+}
+
+func Test_possibleDomains(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		domain   string
+		expected []string
+	}{
+		{
+			desc:     "apex",
+			domain:   "example.com",
+			expected: []string{"example.com"},
+		},
+		{
+			desc:     "CCTLD",
+			domain:   "example.co.uk",
+			expected: []string{"example.co.uk"},
+		},
+		{
+			desc:     "long domain",
+			domain:   "_acme-challenge.foo.bar.example.com",
+			expected: []string{"_acme-challenge.foo.bar.example.com", "foo.bar.example.com", "bar.example.com", "example.com"},
+		},
+		{
+			desc:   "empty",
+			domain: "",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			domains := possibleDomains(test.domain)
+
+			assert.Equal(t, test.expected, domains)
+		})
+	}
+}
diff --git a/providers/dns/lightsail/lightsail_integration_test.go b/providers/dns/lightsail/lightsail_integration_test.go
index 20e45ee2..1b96e87f 100644
--- a/providers/dns/lightsail/lightsail_integration_test.go
+++ b/providers/dns/lightsail/lightsail_integration_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/aws/aws-sdk-go-v2/aws"
 	awsconfig "github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/service/lightsail"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 	"github.com/stretchr/testify/require"
 )
 
@@ -52,19 +53,10 @@ func TestLiveTTL(t *testing.T) {
 
 	entries := resp.Domain.DomainEntries
 	for _, entry := range entries {
-		if deref(entry.Type) == "TXT" && deref(entry.Name) == fqdn {
+		if ptr.Deref(entry.Type) == "TXT" && ptr.Deref(entry.Name) == fqdn {
 			return
 		}
 	}
 
 	t.Fatalf("Could not find a TXT record for _acme-challenge.%s", domain)
 }
-
-func deref[T string | int | int32 | int64 | bool](v *T) T {
-	if v == nil {
-		var zero T
-		return zero
-	}
-
-	return *v
-}
diff --git a/providers/dns/route53/route53_integration_test.go b/providers/dns/route53/route53_integration_test.go
index 2fbcf520..9467fb77 100644
--- a/providers/dns/route53/route53_integration_test.go
+++ b/providers/dns/route53/route53_integration_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/aws/aws-sdk-go-v2/aws"
 	awsconfig "github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/service/route53"
+	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
 	"github.com/stretchr/testify/require"
 )
 
@@ -52,7 +53,7 @@ func TestLiveTTL(t *testing.T) {
 	require.NoError(t, err)
 
 	for _, v := range resp.ResourceRecordSets {
-		if deref(v.Name) == fqdn && v.Type == "TXT" && deref(v.TTL) == 10 {
+		if ptr.Deref(v.Name) == fqdn && v.Type == "TXT" && ptr.Deref(v.TTL) == 10 {
 			return
 		}
 	}