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