lightsail.go raw
1 // Package lightsail implements a DNS provider for solving the DNS-01 challenge using AWS Lightsail DNS.
2 package lightsail
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "math/rand"
9 "strconv"
10 "time"
11
12 "github.com/aws/aws-sdk-go-v2/aws"
13 "github.com/aws/aws-sdk-go-v2/aws/retry"
14 awsconfig "github.com/aws/aws-sdk-go-v2/config"
15 "github.com/aws/aws-sdk-go-v2/service/lightsail"
16 awstypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types"
17 "github.com/go-acme/lego/v4/challenge"
18 "github.com/go-acme/lego/v4/challenge/dns01"
19 "github.com/go-acme/lego/v4/platform/config/env"
20 )
21
22 // Environment variables names.
23 const (
24 envNamespace = "LIGHTSAIL_"
25
26 EnvRegion = envNamespace + "REGION"
27 EnvDNSZone = "DNS_ZONE"
28
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 )
32
33 const maxRetries = 5
34
35 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
36
37 // Config is used to configure the creation of the DNSProvider.
38 type Config struct {
39 DNSZone string
40 Region string
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 }
44
45 // NewDefaultConfig returns a default configuration for the DNSProvider.
46 func NewDefaultConfig() *Config {
47 return &Config{
48 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
49 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
50 }
51 }
52
53 // DNSProvider implements the challenge.Provider interface.
54 type DNSProvider struct {
55 client *lightsail.Client
56 config *Config
57 }
58
59 // NewDNSProvider returns a DNSProvider instance configured for the AWS Lightsail service.
60 //
61 // AWS Credentials are automatically detected in the following locations
62 // and prioritized in the following order:
63 // 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
64 // [AWS_SESSION_TOKEN], [DNS_ZONE], [LIGHTSAIL_REGION]
65 // 2. Shared credentials file (defaults to ~/.aws/credentials)
66 // 3. Amazon EC2 IAM role
67 //
68 // public hosted zone via the FQDN.
69 //
70 // See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
71 func NewDNSProvider() (*DNSProvider, error) {
72 config := NewDefaultConfig()
73
74 config.DNSZone = env.GetOrFile(EnvDNSZone)
75 config.Region = env.GetOrDefaultString(EnvRegion, "us-east-1")
76
77 return NewDNSProviderConfig(config)
78 }
79
80 // NewDNSProviderConfig return a DNSProvider instance configured for AWS Lightsail.
81 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
82 if config == nil {
83 return nil, errors.New("lightsail: the configuration of the DNS provider is nil")
84 }
85
86 ctx := context.Background()
87
88 cfg, err := awsconfig.LoadDefaultConfig(ctx,
89 awsconfig.WithRegion(config.Region),
90 awsconfig.WithRetryer(func() aws.Retryer {
91 return retry.NewStandard(func(options *retry.StandardOptions) {
92 options.MaxAttempts = maxRetries
93
94 // It uses a basic exponential backoff algorithm that returns an initial
95 // delay of ~400ms with an upper limit of ~30 seconds which should prevent
96 // causing a high number of consecutive throttling errors.
97 // For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
98 options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) {
99 retryCount := min(attempt, 7)
100
101 delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
102
103 return time.Duration(delay) * time.Millisecond, nil
104 })
105 })
106 }),
107 )
108 if err != nil {
109 return nil, err
110 }
111
112 return &DNSProvider{
113 config: config,
114 client: lightsail.NewFromConfig(cfg),
115 }, nil
116 }
117
118 // Present creates a TXT record using the specified parameters.
119 func (d *DNSProvider) Present(domain, _, keyAuth string) error {
120 ctx := context.Background()
121 info := dns01.GetChallengeInfo(domain, keyAuth)
122
123 params := &lightsail.CreateDomainEntryInput{
124 DomainName: aws.String(d.config.DNSZone),
125 DomainEntry: &awstypes.DomainEntry{
126 Name: aws.String(info.EffectiveFQDN),
127 Target: aws.String(strconv.Quote(info.Value)),
128 Type: aws.String("TXT"),
129 },
130 }
131
132 _, err := d.client.CreateDomainEntry(ctx, params)
133 if err != nil {
134 return fmt.Errorf("lightsail: %w", err)
135 }
136
137 return nil
138 }
139
140 // CleanUp removes the TXT record matching the specified parameters.
141 func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
142 ctx := context.Background()
143 info := dns01.GetChallengeInfo(domain, keyAuth)
144
145 params := &lightsail.DeleteDomainEntryInput{
146 DomainName: aws.String(d.config.DNSZone),
147 DomainEntry: &awstypes.DomainEntry{
148 Name: aws.String(info.EffectiveFQDN),
149 Type: aws.String("TXT"),
150 Target: aws.String(strconv.Quote(info.Value)),
151 },
152 }
153
154 _, err := d.client.DeleteDomainEntry(ctx, params)
155 if err != nil {
156 return fmt.Errorf("lightsail: %w", err)
157 }
158
159 return nil
160 }
161
162 // Timeout returns the timeout and interval to use when checking for DNS propagation.
163 // Adjusting here to cope with spikes in propagation times.
164 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
165 return d.config.PropagationTimeout, d.config.PollingInterval
166 }
167