fix acme bugs
Kim committed
Apr 2, 2026 at 12:01 UTC
5c482107922acc064ac616c938e9fc4a93caa805
6 files changed
+244
-7
go.sum
-2
@@ -125,8 +125,6 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
125
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
126
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
127
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
128
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
129
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
128
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
129
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
130
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
portal/acme/acme.go
+63
-3
@@ -404,6 +404,9 @@ func (m *Manager) syncENSGasless(ctx context.Context) error {
404
if err := m.SyncENSGaslessHostname(ctx, m.cfg.BaseDomain, m.cfg.ENSGaslessAddress); err != nil {
405
return fmt.Errorf("ensure ens gasless txt: %w", err)
406
}
407
+ if err := m.syncTrackedENSGaslessHostARecords(ctx); err != nil {
408
+ return err
409
+ }
410
m.ensLogOnce.Do(func() {
411
log.Info().
412
Str("provider", m.dns.Name()).
@@ -434,6 +437,9 @@ func (m *Manager) SyncENSGaslessHostname(ctx context.Context, hostname, address
437
if err != nil {
438
return fmt.Errorf("normalize ens gasless address: %w", err)
439
}
440
+ if err := m.syncENSGaslessHostnameARecord(ctx, hostname); err != nil {
441
+ return err
442
+ }
443
if err := m.dns.EnsureTXTRecord(ctx, hostname, gaslessENSTXTPrefix+defaultENSGaslessResolver+" "+strings.TrimSpace(address)); err != nil {
444
return err
445
}
@@ -463,6 +469,9 @@ func (m *Manager) DeleteENSGaslessHostname(ctx context.Context, hostname string)
469
if err := m.dns.DeleteTXTRecords(ctx, hostname, gaslessENSTXTPrefix); err != nil {
470
return err
471
}
472
+ if err := m.dns.DeleteARecord(ctx, hostname); err != nil {
473
+ return err
474
+ }
475
return m.updateTrackedENSGaslessHostnames(func(hostnames []string) []string {
476
filtered := hostnames[:0]
477
for _, tracked := range hostnames {
@@ -487,6 +496,11 @@ func (m *Manager) reconcileTrackedENSGaslessHostnames(ctx context.Context) error
496
if err := m.dns.DeleteTXTRecords(ctx, hostname, gaslessENSTXTPrefix); err != nil {
497
remaining = append(remaining, hostname)
498
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("delete ens gasless txt for %s: %w", hostname, err))
499
+ continue
500
+ }
501
+ if err := m.dns.DeleteARecord(ctx, hostname); err != nil {
502
+ remaining = append(remaining, hostname)
503
+ cleanupErr = errors.Join(cleanupErr, fmt.Errorf("delete ens gasless A record for %s: %w", hostname, err))
504
}
505
}
506
return remaining
@@ -496,6 +510,53 @@ func (m *Manager) reconcileTrackedENSGaslessHostnames(ctx context.Context) error
510
return cleanupErr
511
}
512
513
+func (m *Manager) syncTrackedENSGaslessHostARecords(ctx context.Context) error {
514
+ hostnames, err := m.trackedENSGaslessHostnames()
515
+ if err != nil || len(hostnames) == 0 {
516
+ return err
517
+ }
518
+
519
+ publicIP, err := utils.ResolvePublicIPv4(ctx)
520
+ if err != nil {
521
+ return fmt.Errorf("detect public ip: %w", err)
522
+ }
523
+ for _, hostname := range hostnames {
524
+ if err := m.dns.EnsureARecord(ctx, hostname, publicIP); err != nil {
525
+ return fmt.Errorf("ensure ens gasless A record for %s: %w", hostname, err)
526
+ }
527
+ }
528
+ return nil
529
+}
530
+
531
+func (m *Manager) syncENSGaslessHostnameARecord(ctx context.Context, hostname string) error {
532
+ hostname = utils.NormalizeHostname(hostname)
533
+ if hostname == "" || hostname == m.cfg.BaseDomain {
534
+ return nil
535
+ }
536
+
537
+ publicIP, err := utils.ResolvePublicIPv4(ctx)
538
+ if err != nil {
539
+ return fmt.Errorf("detect public ip: %w", err)
540
+ }
541
+ if err := m.dns.EnsureARecord(ctx, hostname, publicIP); err != nil {
542
+ return fmt.Errorf("ensure ens gasless A record for %s: %w", hostname, err)
543
+ }
544
+ return nil
545
+}
546
+
547
+func (m *Manager) trackedENSGaslessHostnames() ([]string, error) {
548
+ if m == nil {
549
+ return nil, nil
550
+ }
551
+
552
+ path := filepath.Join(m.cfg.KeyDir, ensGaslessHostnamesFileName)
553
+ var hostnames []string
554
+ if _, err := utils.ReadJSONFileIfExists(path, &hostnames); err != nil {
555
+ return nil, err
556
+ }
557
+ return utils.NormalizeChildHostnames(hostnames, m.cfg.BaseDomain), nil
558
+}
559
+
560
func (m *Manager) updateTrackedENSGaslessHostnames(update func([]string) []string) error {
561
if m == nil {
562
return nil
@@ -505,11 +566,10 @@ func (m *Manager) updateTrackedENSGaslessHostnames(update func([]string) []strin
566
defer m.trackedMu.Unlock()
567
568
path := filepath.Join(m.cfg.KeyDir, ensGaslessHostnamesFileName)
508
- var hostnames []string
509
- if _, err := utils.ReadJSONFileIfExists(path, &hostnames); err != nil {
569
+ hostnames, err := m.trackedENSGaslessHostnames()
570
+ if err != nil {
571
return err
572
}
512
- hostnames = utils.NormalizeChildHostnames(hostnames, m.cfg.BaseDomain)
573
if update != nil {
574
hostnames = utils.NormalizeChildHostnames(update(hostnames), m.cfg.BaseDomain)
575
}
portal/acme/cloudflare/provider.go
+58
@@ -124,6 +124,64 @@ func (p *Provider) EnsureARecords(ctx context.Context, baseDomain, publicIPv4 st
124
return nil
125
}
126
127
+func (p *Provider) EnsureARecord(ctx context.Context, name, publicIPv4 string) error {
128
+ if p == nil {
129
+ return errors.New("cloudflare provider is nil")
130
+ }
131
+ name = utils.NormalizeHostname(name)
132
+ if name == "" {
133
+ return errors.New("record name is required")
134
+ }
135
+ if p.token == "" {
136
+ return errors.New("cloudflare token is required")
137
+ }
138
+ if err := utils.ValidateIPv4(publicIPv4); err != nil {
139
+ return err
140
+ }
141
+ publicIPv4 = strings.TrimSpace(publicIPv4)
142
+
143
+ zoneID, err := findZoneID(ctx, p.token, name)
144
+ if err != nil {
145
+ return fmt.Errorf("find cloudflare zone: %w", err)
146
+ }
147
+ if err := ensureDNSRecord(ctx, p.token, zoneID, name, "A", publicIPv4); err != nil {
148
+ return fmt.Errorf("ensure A record for %s: %w", name, err)
149
+ }
150
+ return nil
151
+}
152
+
153
+func (p *Provider) DeleteARecord(ctx context.Context, name string) error {
154
+ if p == nil {
155
+ return errors.New("cloudflare provider is nil")
156
+ }
157
+ name = utils.NormalizeHostname(name)
158
+ if name == "" {
159
+ return errors.New("record name is required")
160
+ }
161
+ if p.token == "" {
162
+ return errors.New("cloudflare token is required")
163
+ }
164
+
165
+ zoneID, err := findZoneID(ctx, p.token, name)
166
+ if err != nil {
167
+ return fmt.Errorf("find cloudflare zone: %w", err)
168
+ }
169
+
170
+ records, err := listDNSRecords(ctx, p.token, zoneID, name, "A")
171
+ if err != nil {
172
+ return err
173
+ }
174
+ for _, record := range records {
175
+ if !strings.EqualFold(record.Name, name) {
176
+ continue
177
+ }
178
+ if err := deleteDNSRecord(ctx, p.token, zoneID, record.ID); err != nil {
179
+ return fmt.Errorf("delete A record %s: %w", name, err)
180
+ }
181
+ }
182
+ return nil
183
+}
184
+
185
func (p *Provider) EnsureTXTRecord(ctx context.Context, name, value string) error {
186
if p == nil {
187
return errors.New("cloudflare provider is nil")
portal/acme/gcloud/provider.go
+57
@@ -106,6 +106,63 @@ func (p *Provider) EnsureARecords(ctx context.Context, baseDomain, publicIPv4 st
106
return nil
107
}
108
109
+func (p *Provider) EnsureARecord(ctx context.Context, name, publicIPv4 string) error {
110
+ if p == nil {
111
+ return errors.New("gcloud provider is nil")
112
+ }
113
+ name = utils.NormalizeHostname(name)
114
+ if name == "" {
115
+ return errors.New("record name is required")
116
+ }
117
+ if err := utils.ValidateIPv4(publicIPv4); err != nil {
118
+ return err
119
+ }
120
+
121
+ service, runtimeCfg, zone, err := newService(ctx, p.cfg, name)
122
+ if err != nil {
123
+ return err
124
+ }
125
+
126
+ if err := ensureRecordSet(ctx, service, runtimeCfg.ProjectID, zone.Name, &dns.ResourceRecordSet{
127
+ Name: fqdn(name),
128
+ Type: "A",
129
+ Ttl: defaultRecordTTL,
130
+ Rrdatas: []string{strings.TrimSpace(publicIPv4)},
131
+ }); err != nil {
132
+ return fmt.Errorf("upsert gcloud A record %s: %w", name, err)
133
+ }
134
+ return nil
135
+}
136
+
137
+func (p *Provider) DeleteARecord(ctx context.Context, name string) error {
138
+ if p == nil {
139
+ return errors.New("gcloud provider is nil")
140
+ }
141
+ name = utils.NormalizeHostname(name)
142
+ if name == "" {
143
+ return errors.New("record name is required")
144
+ }
145
+
146
+ service, runtimeCfg, zone, err := newService(ctx, p.cfg, name)
147
+ if err != nil {
148
+ return err
149
+ }
150
+
151
+ existing, err := listRecordSets(ctx, service, runtimeCfg.ProjectID, zone.Name, name, "A")
152
+ if err != nil {
153
+ return fmt.Errorf("list gcloud A records %s: %w", name, err)
154
+ }
155
+ if len(existing) == 0 {
156
+ return nil
157
+ }
158
+ if err := applyChange(ctx, service, runtimeCfg.ProjectID, zone.Name, &dns.Change{
159
+ Deletions: existing,
160
+ }); err != nil {
161
+ return fmt.Errorf("delete gcloud A record %s: %w", name, err)
162
+ }
163
+ return nil
164
+}
165
+
166
func (p *Provider) EnsureTXTRecord(ctx context.Context, name, value string) error {
167
if p == nil {
168
return errors.New("gcloud provider is nil")
portal/acme/provider.go
+2
@@ -23,6 +23,8 @@ type DNSProvider interface {
23
Name() string
24
ChallengeProvider(ctx context.Context) (challenge.Provider, error)
25
EnsureARecords(ctx context.Context, baseDomain, publicIPv4 string) error
26
+ EnsureARecord(ctx context.Context, name, publicIPv4 string) error
27
+ DeleteARecord(ctx context.Context, name string) error
28
EnsureTXTRecord(ctx context.Context, name, value string) error
29
DeleteTXTRecords(ctx context.Context, name, matchPrefix string) error
30
EnsureDNSSEC(ctx context.Context, baseDomain string) (types.DNSSECStatus, error)
portal/acme/route53/provider.go
+64
-2
@@ -109,6 +109,64 @@ func (p *Provider) EnsureARecords(ctx context.Context, baseDomain, publicIPv4 st
109
return nil
110
}
111
112
+func (p *Provider) EnsureARecord(ctx context.Context, name, publicIPv4 string) error {
113
+ if p == nil {
114
+ return errors.New("route53 provider is nil")
115
+ }
116
+ name = utils.NormalizeHostname(name)
117
+ if name == "" {
118
+ return errors.New("record name is required")
119
+ }
120
+ if err := utils.ValidateIPv4(publicIPv4); err != nil {
121
+ return err
122
+ }
123
+
124
+ client, err := newClient(ctx, p.cfg)
125
+ if err != nil {
126
+ return err
127
+ }
128
+
129
+ hostedZoneID, err := findHostedZoneID(ctx, client, name, p.cfg.HostedZoneID)
130
+ if err != nil {
131
+ return err
132
+ }
133
+ if err := upsertARecord(ctx, client, hostedZoneID, name, publicIPv4); err != nil {
134
+ return fmt.Errorf("upsert route53 A record %s: %w", name, err)
135
+ }
136
+ return nil
137
+}
138
+
139
+func (p *Provider) DeleteARecord(ctx context.Context, name string) error {
140
+ if p == nil {
141
+ return errors.New("route53 provider is nil")
142
+ }
143
+ name = utils.NormalizeHostname(name)
144
+ if name == "" {
145
+ return errors.New("record name is required")
146
+ }
147
+
148
+ client, err := newClient(ctx, p.cfg)
149
+ if err != nil {
150
+ return err
151
+ }
152
+
153
+ hostedZoneID, err := findHostedZoneID(ctx, client, name, p.cfg.HostedZoneID)
154
+ if err != nil {
155
+ return err
156
+ }
157
+ recordSet, err := getRecordSet(ctx, client, hostedZoneID, name, route53types.RRTypeA)
158
+ if err != nil {
159
+ return err
160
+ }
161
+ if recordSet == nil {
162
+ return nil
163
+ }
164
+ if err := deleteRecordSet(ctx, client, hostedZoneID, recordSet, "Managed by Portal ENS cleanup"); err != nil {
165
+ return fmt.Errorf("delete route53 A record %s: %w", name, err)
166
+ }
167
+ return nil
168
+}
169
+
170
func (p *Provider) EnsureTXTRecord(ctx context.Context, name, value string) error {
171
if p == nil {
172
return errors.New("route53 provider is nil")
@@ -396,6 +454,10 @@ func route53TXTContent(value string) string {
454
}
455
456
func getTXTRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneID, name string) (*route53types.ResourceRecordSet, error) {
457
+ return getRecordSet(ctx, client, hostedZoneID, name, route53types.RRTypeTxt)
458
+}
459
+
460
+func getRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneID, name string, recordType route53types.RRType) (*route53types.ResourceRecordSet, error) {
461
if client == nil {
462
return nil, errors.New("route53 client is nil")
463
}
@@ -407,7 +469,7 @@ func getTXTRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneI
469
out, err := client.ListResourceRecordSets(ctx, &awsroute53.ListResourceRecordSetsInput{
470
HostedZoneId: aws.String(hostedZoneID),
471
StartRecordName: aws.String(fqdn),
410
- StartRecordType: route53types.RRTypeTxt,
472
+ StartRecordType: recordType,
473
MaxItems: aws.Int32(1),
474
})
475
if err != nil {
@@ -417,7 +479,7 @@ func getTXTRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneI
479
return nil, nil
480
}
481
recordSet := out.ResourceRecordSets[0]
420
- if !strings.EqualFold(strings.TrimSpace(aws.ToString(recordSet.Name)), fqdn) || recordSet.Type != route53types.RRTypeTxt {
482
+ if !strings.EqualFold(strings.TrimSpace(aws.ToString(recordSet.Name)), fqdn) || recordSet.Type != recordType {
483
return nil, nil
484
}
485
return &recordSet, nil