mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 00:00:52 +00:00
fix: DNS Manual SSL certificate issues for wildcard domains (#11642)
This fix addresses three bugs in the DNS Manual SSL certificate flow: 1. **Order caching fails when Expires is zero**: ACME orders often have zero Expires initially. The condition `!Expires.IsZero()` caused valid cached orders to be deleted and recreated with different TXT values. Fixed by checking `Expires.IsZero() || Expires.After(now)`. 2. **Wildcard and base domain TXT records overwrite each other**: When requesting SSL for both `example.com` and `*.example.com`, both authorizations have identifier `example.com`, causing one TXT value to overwrite the other. Fixed by using `*.domain` as the map key. 3. **Only first TXT record checked**: When multiple TXT records exist, only the first was checked. Fixed by returning all TXT values and checking if expected value exists in any of them. ```release-note Fix DNS Manual SSL certificate issues for wildcard domains ``` Co-authored-by: DeployThemAll <deploythemall@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
DeployThemAll
Claude Opus 4.5
parent
a5816f1dc9
commit
55ccb9fe61
@@ -71,12 +71,39 @@ type Resolve struct {
|
||||
}
|
||||
|
||||
func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.WebsiteSSL) (map[string]Resolve, error) {
|
||||
order, err := c.client.AuthorizeOrder(ctx, acme.DomainIDs(getWebsiteSSLDomains(websiteSSL)...))
|
||||
var order *acme.Order
|
||||
var err error
|
||||
|
||||
// Check if we have an existing valid order for this SSL
|
||||
existingOrder, exists := Orders[websiteSSL.ID]
|
||||
if exists && existingOrder != nil {
|
||||
// Verify the order is still valid (not expired and still pending)
|
||||
// If Expires is zero, order is still valid (ACME doesn't always set expiry immediately)
|
||||
isNotExpired := existingOrder.Expires.IsZero() || existingOrder.Expires.After(time.Now())
|
||||
if isNotExpired && (existingOrder.Status == acme.StatusPending || existingOrder.Status == acme.StatusReady) {
|
||||
// Try to reuse the existing order
|
||||
records, err := c.extractDNSChallenges(ctx, existingOrder)
|
||||
if err == nil && len(records) > 0 {
|
||||
return records, nil
|
||||
}
|
||||
// If extraction failed, fall through to create a new order
|
||||
}
|
||||
// Existing order is expired or invalid, remove it
|
||||
delete(Orders, websiteSSL.ID)
|
||||
}
|
||||
|
||||
// Create a new order
|
||||
order, err = c.client.AuthorizeOrder(ctx, acme.DomainIDs(getWebsiteSSLDomains(websiteSSL)...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Orders[websiteSSL.ID] = order
|
||||
|
||||
return c.extractDNSChallenges(ctx, order)
|
||||
}
|
||||
|
||||
// extractDNSChallenges extracts DNS-01 challenge values from an ACME order
|
||||
func (c *ManualClient) extractDNSChallenges(ctx context.Context, order *acme.Order) (map[string]Resolve, error) {
|
||||
records := make(map[string]Resolve)
|
||||
|
||||
for _, authzURL := range order.AuthzURLs {
|
||||
@@ -103,7 +130,14 @@ func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.Webs
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records[domain] = Resolve{
|
||||
// Use different map key for wildcard vs non-wildcard to avoid overwriting
|
||||
// Both use the same DNS record name (_acme-challenge.domain) but different values
|
||||
mapKey := domain
|
||||
if authz.Wildcard {
|
||||
mapKey = "*." + domain
|
||||
}
|
||||
|
||||
records[mapKey] = Resolve{
|
||||
Key: fmt.Sprintf("_acme-challenge.%s", domain),
|
||||
Value: txtValue,
|
||||
}
|
||||
@@ -111,15 +145,15 @@ func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.Webs
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func queryDNSRecords(domain string) (map[string]string, error) {
|
||||
func queryDNSRecords(domain string) (map[string][]string, error) {
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
txts, err := net.LookupTXT(recordName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records := make(map[string]string)
|
||||
records := make(map[string][]string)
|
||||
if len(txts) > 0 {
|
||||
records[recordName] = txts[0]
|
||||
records[recordName] = txts
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
@@ -159,7 +193,7 @@ func (c *ManualClient) handleAuthorization(ctx context.Context, authzURL string,
|
||||
|
||||
for {
|
||||
c.logger.Printf("[INFO] [%s] acme: Checking DNS record propagation.", domain)
|
||||
var currentRecords map[string]string
|
||||
var currentRecords map[string][]string
|
||||
var queryErr error
|
||||
if len(nameservers) == 0 {
|
||||
currentRecords, queryErr = queryDNSRecords(domain)
|
||||
@@ -177,16 +211,26 @@ func (c *ManualClient) handleAuthorization(ctx context.Context, authzURL string,
|
||||
return fmt.Errorf("failed to query DNS records: %v", queryErr)
|
||||
}
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
providedRecord, exists := currentRecords[recordName]
|
||||
if exists && providedRecord == expectedRecord {
|
||||
providedRecords, exists := currentRecords[recordName]
|
||||
// Check if expected record is in any of the TXT values
|
||||
found := false
|
||||
if exists {
|
||||
for _, record := range providedRecords {
|
||||
if record == expectedRecord {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
if !exists {
|
||||
if !exists || len(providedRecords) == 0 {
|
||||
return fmt.Errorf("TXT record not provided for domain %s after retrying", domain)
|
||||
}
|
||||
c.logger.Printf("[INFO] [%s] TXT record mismatch for %s: expected %s, got %s\"", domain, domain, expectedRecord, providedRecord)
|
||||
return fmt.Errorf("TXT record mismatch for %s: expected %s, got %s", domain, expectedRecord, providedRecord)
|
||||
c.logger.Printf("[INFO] [%s] TXT record mismatch for %s: expected %s, got %v", domain, domain, expectedRecord, providedRecords)
|
||||
return fmt.Errorf("TXT record mismatch for %s: expected %s, got %v", domain, expectedRecord, providedRecords)
|
||||
}
|
||||
time.Sleep(pollingInterval)
|
||||
}
|
||||
@@ -339,7 +383,7 @@ func handleNameserver(nameserver string) string {
|
||||
return fmt.Sprintf("%s:53", nameserver)
|
||||
}
|
||||
|
||||
func queryDNSRecordsWithResolver(ctx context.Context, logger *log.Logger, domain string, dnsServer string) (map[string]string, error) {
|
||||
func queryDNSRecordsWithResolver(ctx context.Context, logger *log.Logger, domain string, dnsServer string) (map[string][]string, error) {
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
c := new(dns.Client)
|
||||
c.Timeout = 10 * time.Second
|
||||
@@ -367,16 +411,19 @@ func queryDNSRecordsWithResolver(ctx context.Context, logger *log.Logger, domain
|
||||
return nil, fmt.Errorf("DNS query failed with code: %s", dns.RcodeToString[r.Rcode])
|
||||
}
|
||||
|
||||
records := make(map[string]string)
|
||||
records := make(map[string][]string)
|
||||
var txtValues []string
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
if txt, ok := answer.(*dns.TXT); ok {
|
||||
if len(txt.Txt) > 0 {
|
||||
records[recordName] = txt.Txt[0]
|
||||
break
|
||||
txtValues = append(txtValues, txt.Txt[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(txtValues) > 0 {
|
||||
records[recordName] = txtValues
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user