From 921303fb2bc330307facdda04c5d89acdf305397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81ro=CC=82me=20Billiras?= Date: Fri, 25 Sep 2026 23:43:33 +0200 Subject: [PATCH] BUG/MEDIUM: acme: append dns-01 records instead of replacing them A certificate covering both `example.com` and `*.example.com` gets two dns-01 authorizations, each with its own token, but both use the same record name: `_acme-challenge.example.com`. `Present()` uses `SetRecords()`, which by libdns semantics replaces every record with the same name and type. When the second challenge is deployed, it deletes the TXT record of the first one, and one of the two validations fails. Use `AppendRecords()` when the provider implements `libdns.RecordAppender`, and keep `SetRecords()` as a fallback. Cleanup is unchanged: `DeleteRecords()` matches on name, type and value, so each challenge only removes its own record. --- acme/dns01.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/acme/dns01.go b/acme/dns01.go index 655598be..910e5afe 100644 --- a/acme/dns01.go +++ b/acme/dns01.go @@ -83,7 +83,16 @@ func (s *DNS01Solver) Present(ctx context.Context, domain, zone, keyAuth string) zone = rooted(zone) } - results, err := s.provider.SetRecords(ctx, zone, []libdns.Record{rec}) + // Append rather than Set: SetRecords replaces every record with the same + // name and type, which breaks certificates covering both a domain and its + // wildcard, since both challenges share the same _acme-challenge name. + var results []libdns.Record + var err error + if appender, ok := s.provider.(libdns.RecordAppender); ok { + results, err = appender.AppendRecords(ctx, zone, []libdns.Record{rec}) + } else { + results, err = s.provider.SetRecords(ctx, zone, []libdns.Record{rec}) + } if err != nil { return fmt.Errorf("adding temporary record for zone %q: %w", zone, err) }