azuredns.go raw
1 // Package azuredns 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 azuredns
4
5 import (
6 "errors"
7 "fmt"
8 "net/http"
9 "time"
10
11 "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
12 "github.com/go-acme/lego/v4/challenge"
13 "github.com/go-acme/lego/v4/platform/config/env"
14 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
15 )
16
17 // Environment variables names.
18 const (
19 envNamespace = "AZURE_"
20
21 EnvEnvironment = envNamespace + "ENVIRONMENT"
22 EnvSubscriptionID = envNamespace + "SUBSCRIPTION_ID"
23 EnvResourceGroup = envNamespace + "RESOURCE_GROUP"
24 EnvZoneName = envNamespace + "ZONE_NAME"
25 EnvPrivateZone = envNamespace + "PRIVATE_ZONE"
26
27 EnvTenantID = envNamespace + "TENANT_ID"
28 EnvClientID = envNamespace + "CLIENT_ID"
29 EnvClientSecret = envNamespace + "CLIENT_SECRET"
30
31 EnvOIDCToken = envNamespace + "OIDC_TOKEN"
32 EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH"
33 EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL"
34 EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL"
35 altEnvArmOIDCRequestURL = "ARM_OIDC_REQUEST_URL"
36 EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN"
37 EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
38 altEnvArmOIDCRequestToken = "ARM_OIDC_REQUEST_TOKEN"
39
40 EnvServiceConnectionID = envNamespace + "SERVICE_CONNECTION_ID"
41 altEnvServiceConnectionID = "SERVICE_CONNECTION_ID"
42 altEnvArmAdoPipelineServiceConnectionID = "ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID"
43 altEnvArmOIDCAzureServiceConnectionID = "ARM_OIDC_AZURE_SERVICE_CONNECTION_ID"
44 EnvSystemAccessToken = envNamespace + "SYSTEM_ACCESS_TOKEN"
45 altEnvSystemAccessToken = "SYSTEM_ACCESSTOKEN"
46
47 EnvAuthMethod = envNamespace + "AUTH_METHOD"
48 EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT"
49
50 EnvServiceDiscoveryFilter = envNamespace + "SERVICEDISCOVERY_FILTER"
51
52 EnvTTL = envNamespace + "TTL"
53 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
54 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
55 )
56
57 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
58
59 // Config is used to configure the creation of the DNSProvider.
60 type Config struct {
61 ZoneName string
62
63 SubscriptionID string
64 ResourceGroup string
65 PrivateZone bool
66
67 Environment cloud.Configuration
68
69 // optional if using default Azure credentials
70 ClientID string
71 ClientSecret string
72 TenantID string
73
74 OIDCToken string
75 OIDCTokenFilePath string
76 OIDCRequestURL string
77 OIDCRequestToken string
78
79 ServiceConnectionID string
80 SystemAccessToken string
81
82 AuthMethod string
83 AuthMSITimeout time.Duration
84
85 PropagationTimeout time.Duration
86 PollingInterval time.Duration
87 TTL int
88 HTTPClient *http.Client
89
90 ServiceDiscoveryFilter string
91 }
92
93 // NewDefaultConfig returns a default configuration for the DNSProvider.
94 func NewDefaultConfig() *Config {
95 return &Config{
96 ZoneName: env.GetOrFile(EnvZoneName),
97 TTL: env.GetOrDefaultInt(EnvTTL, 60),
98 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
99 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),
100 Environment: cloud.AzurePublic,
101 }
102 }
103
104 // DNSProvider implements the challenge.Provider interface.
105 type DNSProvider struct {
106 provider challenge.ProviderTimeout
107 }
108
109 // NewDNSProvider returns a DNSProvider instance configured for azuredns.
110 func NewDNSProvider() (*DNSProvider, error) {
111 config := NewDefaultConfig()
112
113 environmentName := env.GetOrFile(EnvEnvironment)
114 if environmentName != "" {
115 switch environmentName {
116 case "china":
117 config.Environment = cloud.AzureChina
118 case "public":
119 config.Environment = cloud.AzurePublic
120 case "usgovernment":
121 config.Environment = cloud.AzureGovernment
122 default:
123 return nil, fmt.Errorf("azuredns: unknown environment %s", environmentName)
124 }
125 } else {
126 config.Environment = cloud.AzurePublic
127 }
128
129 config.SubscriptionID = env.GetOrFile(EnvSubscriptionID)
130 config.ResourceGroup = env.GetOrFile(EnvResourceGroup)
131 config.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false)
132
133 config.ClientID = env.GetOrFile(EnvClientID)
134 config.ClientSecret = env.GetOrFile(EnvClientSecret)
135 config.TenantID = env.GetOrFile(EnvTenantID)
136
137 config.OIDCToken = env.GetOrFile(EnvOIDCToken)
138 config.OIDCTokenFilePath = env.GetOrFile(EnvOIDCTokenFilePath)
139
140 config.ServiceDiscoveryFilter = env.GetOrFile(EnvServiceDiscoveryFilter)
141
142 oidcValues, _ := env.GetWithFallback(
143 []string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL, altEnvArmOIDCRequestURL},
144 []string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken, altEnvArmOIDCRequestToken},
145 )
146
147 config.OIDCRequestURL = oidcValues[EnvOIDCRequestURL]
148 config.OIDCRequestToken = oidcValues[EnvOIDCRequestToken]
149
150 // https://registry.terraform.io/providers/hashicorp/Azurerm/latest/docs/guides/service_principal_oidc
151 pipelineValues, _ := env.GetWithFallback(
152 []string{EnvServiceConnectionID, altEnvServiceConnectionID, altEnvArmAdoPipelineServiceConnectionID, altEnvArmOIDCAzureServiceConnectionID},
153 []string{EnvSystemAccessToken, altEnvArmOIDCRequestToken, altEnvSystemAccessToken},
154 )
155
156 config.ServiceConnectionID = pipelineValues[EnvServiceConnectionID]
157 config.SystemAccessToken = pipelineValues[EnvSystemAccessToken]
158
159 config.AuthMethod = env.GetOrFile(EnvAuthMethod)
160 config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second)
161
162 return NewDNSProviderConfig(config)
163 }
164
165 // NewDNSProviderConfig return a DNSProvider instance configured for Azure.
166 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
167 if config == nil {
168 return nil, errors.New("azuredns: the configuration of the DNS provider is nil")
169 }
170
171 if config.HTTPClient == nil {
172 config.HTTPClient = &http.Client{Timeout: 5 * time.Second}
173 }
174
175 config.HTTPClient = clientdebug.Wrap(config.HTTPClient)
176
177 credentials, err := getCredentials(config)
178 if err != nil {
179 return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err)
180 }
181
182 var dnsProvider challenge.ProviderTimeout
183 if config.PrivateZone {
184 dnsProvider, err = NewDNSProviderPrivate(config, credentials)
185 if err != nil {
186 return nil, fmt.Errorf("azuredns: %w", err)
187 }
188 } else {
189 dnsProvider, err = NewDNSProviderPublic(config, credentials)
190 if err != nil {
191 return nil, fmt.Errorf("azuredns: %w", err)
192 }
193 }
194
195 return &DNSProvider{provider: dnsProvider}, nil
196 }
197
198 // Timeout returns the timeout and interval to use when checking for DNS propagation.
199 // Adjusting here to cope with spikes in propagation times.
200 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
201 return d.provider.Timeout()
202 }
203
204 // Present creates a TXT record to fulfill the dns-01 challenge.
205 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
206 return d.provider.Present(domain, token, keyAuth)
207 }
208
209 // CleanUp removes the TXT record matching the specified parameters.
210 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
211 return d.provider.CleanUp(domain, token, keyAuth)
212 }
213