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