azure.go raw
1 // Package azure implements a DNS provider for solving the DNS-01 challenge using azure DNS.
2 // Azure doesn't like trailing dots on domain names, most of the acme code does.
3 package azure
4
5 import (
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/Azure/go-autorest/autorest"
14 aazure "github.com/Azure/go-autorest/autorest/azure"
15 "github.com/Azure/go-autorest/autorest/azure/auth"
16 "github.com/go-acme/lego/v4/challenge"
17 "github.com/go-acme/lego/v4/platform/config/env"
18 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "AZURE_"
24
25 EnvEnvironment = envNamespace + "ENVIRONMENT"
26 EnvMetadataEndpoint = envNamespace + "METADATA_ENDPOINT"
27 EnvSubscriptionID = envNamespace + "SUBSCRIPTION_ID"
28 EnvResourceGroup = envNamespace + "RESOURCE_GROUP"
29 EnvTenantID = envNamespace + "TENANT_ID"
30 EnvClientID = envNamespace + "CLIENT_ID"
31 EnvClientSecret = envNamespace + "CLIENT_SECRET"
32 EnvZoneName = envNamespace + "ZONE_NAME"
33 EnvPrivateZone = envNamespace + "PRIVATE_ZONE"
34
35 EnvTTL = envNamespace + "TTL"
36 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
37 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
38 )
39
40 const defaultMetadataEndpoint = "http://169.254.169.254"
41
42 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
43
44 // Config is used to configure the creation of the DNSProvider.
45 type Config struct {
46 ZoneName string
47
48 // optional if using instance metadata service
49 ClientID string
50 ClientSecret string
51 TenantID string
52
53 SubscriptionID string
54 ResourceGroup string
55 PrivateZone bool
56
57 MetadataEndpoint string
58 ResourceManagerEndpoint string
59 ActiveDirectoryEndpoint string
60
61 PropagationTimeout time.Duration
62 PollingInterval time.Duration
63 TTL int
64 HTTPClient *http.Client
65 }
66
67 // NewDefaultConfig returns a default configuration for the DNSProvider.
68 func NewDefaultConfig() *Config {
69 return &Config{
70 ZoneName: env.GetOrFile(EnvZoneName),
71 TTL: env.GetOrDefaultInt(EnvTTL, 60),
72 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
73 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),
74 MetadataEndpoint: env.GetOrFile(EnvMetadataEndpoint),
75 ResourceManagerEndpoint: aazure.PublicCloud.ResourceManagerEndpoint,
76 ActiveDirectoryEndpoint: aazure.PublicCloud.ActiveDirectoryEndpoint,
77 }
78 }
79
80 // DNSProvider implements the challenge.Provider interface.
81 type DNSProvider struct {
82 provider challenge.ProviderTimeout
83 }
84
85 // NewDNSProvider returns a DNSProvider instance configured for azure.
86 // Credentials can be passed in the environment variables:
87 // AZURE_ENVIRONMENT, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,
88 // AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
89 // If the credentials are _not_ set via the environment,
90 // then it will attempt to get a bearer token via the instance metadata service.
91 // see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42
92 //
93 // Deprecated: use azuredns instead.
94 func NewDNSProvider() (*DNSProvider, error) {
95 config := NewDefaultConfig()
96
97 environmentName := env.GetOrFile(EnvEnvironment)
98 if environmentName != "" {
99 var environment aazure.Environment
100
101 switch environmentName {
102 case "china":
103 environment = aazure.ChinaCloud
104 case "german":
105 environment = aazure.GermanCloud
106 case "public":
107 environment = aazure.PublicCloud
108 case "usgovernment":
109 environment = aazure.USGovernmentCloud
110 default:
111 return nil, fmt.Errorf("azure: unknown environment %s", environmentName)
112 }
113
114 config.ResourceManagerEndpoint = environment.ResourceManagerEndpoint
115 config.ActiveDirectoryEndpoint = environment.ActiveDirectoryEndpoint
116 }
117
118 config.SubscriptionID = env.GetOrFile(EnvSubscriptionID)
119 config.ResourceGroup = env.GetOrFile(EnvResourceGroup)
120 config.ClientSecret = env.GetOrFile(EnvClientSecret)
121 config.ClientID = env.GetOrFile(EnvClientID)
122 config.TenantID = env.GetOrFile(EnvTenantID)
123 config.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false)
124
125 return NewDNSProviderConfig(config)
126 }
127
128 // NewDNSProviderConfig return a DNSProvider instance configured for Azure.
129 //
130 // Deprecated: use azuredns instead.
131 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
132 if config == nil {
133 return nil, errors.New("azure: the configuration of the DNS provider is nil")
134 }
135
136 if config.HTTPClient == nil {
137 config.HTTPClient = &http.Client{Timeout: 5 * time.Second}
138 }
139
140 authorizer, err := getAuthorizer(config)
141 if err != nil {
142 return nil, err
143 }
144
145 if config.SubscriptionID == "" {
146 subsID, err := getMetadata(config, "subscriptionId")
147 if err != nil {
148 return nil, fmt.Errorf("azure: %w", err)
149 }
150
151 if subsID == "" {
152 return nil, errors.New("azure: SubscriptionID is missing")
153 }
154
155 config.SubscriptionID = subsID
156 }
157
158 if config.ResourceGroup == "" {
159 resGroup, err := getMetadata(config, "resourceGroupName")
160 if err != nil {
161 return nil, fmt.Errorf("azure: %w", err)
162 }
163
164 if resGroup == "" {
165 return nil, errors.New("azure: ResourceGroup is missing")
166 }
167
168 config.ResourceGroup = resGroup
169 }
170
171 if config.PrivateZone {
172 return &DNSProvider{provider: &dnsProviderPrivate{config: config, authorizer: authorizer}}, nil
173 }
174
175 return &DNSProvider{provider: &dnsProviderPublic{config: config, authorizer: authorizer}}, nil
176 }
177
178 // Timeout returns the timeout and interval to use when checking for DNS propagation.
179 // Adjusting here to cope with spikes in propagation times.
180 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
181 return d.provider.Timeout()
182 }
183
184 // Present creates a TXT record to fulfill the dns-01 challenge.
185 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
186 return d.provider.Present(domain, token, keyAuth)
187 }
188
189 // CleanUp removes the TXT record matching the specified parameters.
190 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
191 return d.provider.CleanUp(domain, token, keyAuth)
192 }
193
194 func getAuthorizer(config *Config) (autorest.Authorizer, error) {
195 if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" {
196 credentialsConfig := auth.ClientCredentialsConfig{
197 ClientID: config.ClientID,
198 ClientSecret: config.ClientSecret,
199 TenantID: config.TenantID,
200 Resource: config.ResourceManagerEndpoint,
201 AADEndpoint: config.ActiveDirectoryEndpoint,
202 }
203
204 spToken, err := credentialsConfig.ServicePrincipalToken()
205 if err != nil {
206 return nil, fmt.Errorf("failed to get oauth token from client credentials: %w", err)
207 }
208
209 spToken.SetSender(config.HTTPClient)
210
211 return autorest.NewBearerAuthorizer(spToken), nil
212 }
213
214 return auth.NewAuthorizerFromEnvironment()
215 }
216
217 // Fetches metadata from environment or the instance metadata service.
218 // borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go
219 func getMetadata(config *Config, field string) (string, error) {
220 metadataEndpoint := config.MetadataEndpoint
221 if metadataEndpoint == "" {
222 metadataEndpoint = defaultMetadataEndpoint
223 }
224
225 endpoint, err := url.JoinPath(metadataEndpoint, "metadata", "instance", "compute", field)
226 if err != nil {
227 return "", err
228 }
229
230 req, err := http.NewRequest(http.MethodGet, endpoint, nil)
231 if err != nil {
232 return "", err
233 }
234
235 req.Header.Set("Metadata", "True")
236
237 q := req.URL.Query()
238 q.Add("format", "text")
239 q.Add("api-version", "2017-12-01")
240 req.URL.RawQuery = q.Encode()
241
242 resp, err := config.HTTPClient.Do(req)
243 if err != nil {
244 return "", errutils.NewHTTPDoError(req, err)
245 }
246
247 defer func() { _ = resp.Body.Close() }()
248
249 raw, err := io.ReadAll(resp.Body)
250 if err != nil {
251 return "", errutils.NewReadResponseError(req, resp.StatusCode, err)
252 }
253
254 return string(raw), nil
255 }
256