forked from TrueCloudLab/lego
azure: add support for Azure Private Zone DNS (#1561)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
27bb3f2256
commit
6dd5d1f814
7 changed files with 308 additions and 111 deletions
|
@ -265,6 +265,7 @@ func displayDNSHelp(name string) error {
|
||||||
ew.writeln(`Additional Configuration:`)
|
ew.writeln(`Additional Configuration:`)
|
||||||
ew.writeln(` - "AZURE_METADATA_ENDPOINT": Metadata Service endpoint URL`)
|
ew.writeln(` - "AZURE_METADATA_ENDPOINT": Metadata Service endpoint URL`)
|
||||||
ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`)
|
ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||||
|
ew.writeln(` - "AZURE_PRIVATE_ZONE": Set to true to use Azure Private DNS Zones and not public`)
|
||||||
ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||||
ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`)
|
ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`)
|
||||||
ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`)
|
ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`)
|
||||||
|
|
|
@ -47,6 +47,7 @@ More information [here](/lego/dns/#configuration-and-credentials).
|
||||||
|--------------------------------|-------------|
|
|--------------------------------|-------------|
|
||||||
| `AZURE_METADATA_ENDPOINT` | Metadata Service endpoint URL |
|
| `AZURE_METADATA_ENDPOINT` | Metadata Service endpoint URL |
|
||||||
| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check |
|
| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||||
|
| `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public |
|
||||||
| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||||
| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge |
|
| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||||
| `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in |
|
| `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in |
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
package azure
|
package azure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -11,11 +10,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
"github.com/Azure/go-autorest/autorest"
|
||||||
aazure "github.com/Azure/go-autorest/autorest/azure"
|
aazure "github.com/Azure/go-autorest/autorest/azure"
|
||||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||||
"github.com/Azure/go-autorest/autorest/to"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v4/platform/config/env"
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
)
|
)
|
||||||
|
@ -34,6 +32,7 @@ const (
|
||||||
EnvClientID = envNamespace + "CLIENT_ID"
|
EnvClientID = envNamespace + "CLIENT_ID"
|
||||||
EnvClientSecret = envNamespace + "CLIENT_SECRET"
|
EnvClientSecret = envNamespace + "CLIENT_SECRET"
|
||||||
EnvZoneName = envNamespace + "ZONE_NAME"
|
EnvZoneName = envNamespace + "ZONE_NAME"
|
||||||
|
EnvPrivateZone = envNamespace + "PRIVATE_ZONE"
|
||||||
|
|
||||||
EnvTTL = envNamespace + "TTL"
|
EnvTTL = envNamespace + "TTL"
|
||||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||||
|
@ -49,6 +48,7 @@ type Config struct {
|
||||||
|
|
||||||
SubscriptionID string
|
SubscriptionID string
|
||||||
ResourceGroup string
|
ResourceGroup string
|
||||||
|
PrivateZone bool
|
||||||
|
|
||||||
MetadataEndpoint string
|
MetadataEndpoint string
|
||||||
ResourceManagerEndpoint string
|
ResourceManagerEndpoint string
|
||||||
|
@ -74,8 +74,7 @@ func NewDefaultConfig() *Config {
|
||||||
|
|
||||||
// DNSProvider implements the challenge.Provider interface.
|
// DNSProvider implements the challenge.Provider interface.
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
config *Config
|
provider challenge.ProviderTimeout
|
||||||
authorizer autorest.Authorizer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for azure.
|
// NewDNSProvider returns a DNSProvider instance configured for azure.
|
||||||
|
@ -113,6 +112,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
config.ClientSecret = env.GetOrFile(EnvClientSecret)
|
config.ClientSecret = env.GetOrFile(EnvClientSecret)
|
||||||
config.ClientID = env.GetOrFile(EnvClientID)
|
config.ClientID = env.GetOrFile(EnvClientID)
|
||||||
config.TenantID = env.GetOrFile(EnvTenantID)
|
config.TenantID = env.GetOrFile(EnvTenantID)
|
||||||
|
config.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false)
|
||||||
|
|
||||||
return NewDNSProviderConfig(config)
|
return NewDNSProviderConfig(config)
|
||||||
}
|
}
|
||||||
|
@ -156,112 +156,27 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
config.ResourceGroup = resGroup
|
config.ResourceGroup = resGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{config: config, authorizer: authorizer}, nil
|
if config.PrivateZone {
|
||||||
|
return &DNSProvider{provider: &dnsProviderPrivate{config: config, authorizer: authorizer}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{provider: &dnsProviderPublic{config: config, authorizer: authorizer}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
// Adjusting here to cope with spikes in propagation times.
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
return d.provider.Timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record to fulfill the dns-01 challenge.
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
ctx := context.Background()
|
return d.provider.Present(domain, token, keyAuth)
|
||||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
|
||||||
|
|
||||||
zone, err := d.getHostedZoneID(ctx, fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("azure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
|
||||||
rsc.Authorizer = d.authorizer
|
|
||||||
|
|
||||||
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
|
||||||
|
|
||||||
// Get existing record set
|
|
||||||
rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, relative, dns.TXT)
|
|
||||||
if err != nil {
|
|
||||||
var detailed autorest.DetailedError
|
|
||||||
if !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound {
|
|
||||||
return fmt.Errorf("azure: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct unique TXT records using map
|
|
||||||
uniqRecords := map[string]struct{}{value: {}}
|
|
||||||
if rset.RecordSetProperties != nil && rset.TxtRecords != nil {
|
|
||||||
for _, txtRecord := range *rset.TxtRecords {
|
|
||||||
// Assume Value doesn't contain multiple strings
|
|
||||||
if txtRecord.Value != nil && len(*txtRecord.Value) > 0 {
|
|
||||||
uniqRecords[(*txtRecord.Value)[0]] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var txtRecords []dns.TxtRecord
|
|
||||||
for txt := range uniqRecords {
|
|
||||||
txtRecords = append(txtRecords, dns.TxtRecord{Value: &[]string{txt}})
|
|
||||||
}
|
|
||||||
|
|
||||||
rec := dns.RecordSet{
|
|
||||||
Name: &relative,
|
|
||||||
RecordSetProperties: &dns.RecordSetProperties{
|
|
||||||
TTL: to.Int64Ptr(int64(d.config.TTL)),
|
|
||||||
TxtRecords: &txtRecords,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, rec, "", "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("azure: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
ctx := context.Background()
|
return d.provider.CleanUp(domain, token, keyAuth)
|
||||||
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
|
||||||
|
|
||||||
zone, err := d.getHostedZoneID(ctx, fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("azure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
|
||||||
rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
|
||||||
rsc.Authorizer = d.authorizer
|
|
||||||
|
|
||||||
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("azure: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks that azure has a zone for this domain name.
|
|
||||||
func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
|
|
||||||
if zone := env.GetOrFile(EnvZoneName); zone != "" {
|
|
||||||
return zone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
authZone, err := dns01.FindZoneByFqdn(fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
dc := dns.NewZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
|
||||||
dc.Authorizer = d.authorizer
|
|
||||||
|
|
||||||
zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// zone.Name shouldn't have a trailing dot(.)
|
|
||||||
return to.String(zone.Name), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the relative record to the domain.
|
// Returns the relative record to the domain.
|
||||||
|
|
|
@ -16,11 +16,12 @@ Example = ''''''
|
||||||
AZURE_RESOURCE_GROUP = "Resource group"
|
AZURE_RESOURCE_GROUP = "Resource group"
|
||||||
'instance metadata service' = "If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service)."
|
'instance metadata service' = "If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service)."
|
||||||
[Configuration.Additional]
|
[Configuration.Additional]
|
||||||
|
AZURE_METADATA_ENDPOINT = "Metadata Service endpoint URL"
|
||||||
|
AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public"
|
||||||
|
AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in"
|
||||||
AZURE_POLLING_INTERVAL = "Time between DNS propagation check"
|
AZURE_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||||
AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||||
AZURE_TTL = "The TTL of the TXT record used for the DNS challenge"
|
AZURE_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||||
AZURE_METADATA_ENDPOINT = "Metadata Service endpoint URL"
|
|
||||||
AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in"
|
|
||||||
|
|
||||||
[Links]
|
[Links]
|
||||||
API = "https://docs.microsoft.com/en-us/go/azure/"
|
API = "https://docs.microsoft.com/en-us/go/azure/"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/platform/tester"
|
"github.com/go-acme/lego/v4/platform/tester"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,13 +60,16 @@ func TestNewDNSProvider(t *testing.T) {
|
||||||
|
|
||||||
p, err := NewDNSProvider()
|
p, err := NewDNSProvider()
|
||||||
|
|
||||||
if test.expected == "" {
|
if test.expected != "" {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, p)
|
require.NotNil(t, p)
|
||||||
require.NotNil(t, p.config)
|
require.NotNil(t, p.provider)
|
||||||
} else {
|
|
||||||
require.EqualError(t, err, test.expected)
|
assert.IsType(t, p.provider, new(dnsProviderPublic))
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,16 +82,27 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
subscriptionID string
|
subscriptionID string
|
||||||
tenantID string
|
tenantID string
|
||||||
resourceGroup string
|
resourceGroup string
|
||||||
|
privateZone bool
|
||||||
handler func(w http.ResponseWriter, r *http.Request)
|
handler func(w http.ResponseWriter, r *http.Request)
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "success",
|
desc: "success (public)",
|
||||||
clientID: "A",
|
clientID: "A",
|
||||||
clientSecret: "B",
|
clientSecret: "B",
|
||||||
tenantID: "C",
|
tenantID: "C",
|
||||||
subscriptionID: "D",
|
subscriptionID: "D",
|
||||||
resourceGroup: "E",
|
resourceGroup: "E",
|
||||||
|
privateZone: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success (private)",
|
||||||
|
clientID: "A",
|
||||||
|
clientSecret: "B",
|
||||||
|
tenantID: "C",
|
||||||
|
subscriptionID: "D",
|
||||||
|
resourceGroup: "E",
|
||||||
|
privateZone: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "SubscriptionID missing",
|
desc: "SubscriptionID missing",
|
||||||
|
@ -132,6 +147,7 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
config.SubscriptionID = test.subscriptionID
|
config.SubscriptionID = test.subscriptionID
|
||||||
config.TenantID = test.tenantID
|
config.TenantID = test.tenantID
|
||||||
config.ResourceGroup = test.resourceGroup
|
config.ResourceGroup = test.resourceGroup
|
||||||
|
config.PrivateZone = test.privateZone
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
server := httptest.NewServer(mux)
|
server := httptest.NewServer(mux)
|
||||||
|
@ -146,12 +162,19 @@ func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
|
|
||||||
p, err := NewDNSProviderConfig(config)
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
|
||||||
if test.expected == "" {
|
if test.expected != "" {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, p)
|
require.NotNil(t, p)
|
||||||
require.NotNil(t, p.config)
|
require.NotNil(t, p.provider)
|
||||||
|
|
||||||
|
if test.privateZone {
|
||||||
|
assert.IsType(t, p.provider, new(dnsProviderPrivate))
|
||||||
} else {
|
} else {
|
||||||
require.EqualError(t, err, test.expected)
|
assert.IsType(t, p.provider, new(dnsProviderPublic))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
128
providers/dns/azure/private.go
Normal file
128
providers/dns/azure/private.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package azure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dnsProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS.
|
||||||
|
type dnsProviderPrivate struct {
|
||||||
|
config *Config
|
||||||
|
authorizer autorest.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
|
func (d *dnsProviderPrivate) Timeout() (timeout, interval time.Duration) {
|
||||||
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
|
func (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
|
zone, err := d.getHostedZoneID(ctx, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
rsc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
||||||
|
|
||||||
|
// Get existing record set
|
||||||
|
rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative)
|
||||||
|
if err != nil {
|
||||||
|
var detailed autorest.DetailedError
|
||||||
|
if !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct unique TXT records using map
|
||||||
|
uniqRecords := map[string]struct{}{value: {}}
|
||||||
|
if rset.RecordSetProperties != nil && rset.TxtRecords != nil {
|
||||||
|
for _, txtRecord := range *rset.TxtRecords {
|
||||||
|
// Assume Value doesn't contain multiple strings
|
||||||
|
values := to.StringSlice(txtRecord.Value)
|
||||||
|
if len(values) > 0 {
|
||||||
|
uniqRecords[values[0]] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var txtRecords []privatedns.TxtRecord
|
||||||
|
for txt := range uniqRecords {
|
||||||
|
txtRecords = append(txtRecords, privatedns.TxtRecord{Value: &[]string{txt}})
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := privatedns.RecordSet{
|
||||||
|
Name: &relative,
|
||||||
|
RecordSetProperties: &privatedns.RecordSetProperties{
|
||||||
|
TTL: to.Int64Ptr(int64(d.config.TTL)),
|
||||||
|
TxtRecords: &txtRecords,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative, rec, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (d *dnsProviderPrivate) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
|
zone, err := d.getHostedZoneID(ctx, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
||||||
|
|
||||||
|
rsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
rsc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks that azure has a zone for this domain name.
|
||||||
|
func (d *dnsProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
|
||||||
|
if zone := env.GetOrFile(EnvZoneName); zone != "" {
|
||||||
|
return zone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
authZone, err := dns01.FindZoneByFqdn(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dc := privatedns.NewPrivateZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
dc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// zone.Name shouldn't have a trailing dot(.)
|
||||||
|
return to.String(zone.Name), nil
|
||||||
|
}
|
128
providers/dns/azure/public.go
Normal file
128
providers/dns/azure/public.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package azure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dnsProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS.
|
||||||
|
type dnsProviderPublic struct {
|
||||||
|
config *Config
|
||||||
|
authorizer autorest.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
|
func (d *dnsProviderPublic) Timeout() (timeout, interval time.Duration) {
|
||||||
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
|
func (d *dnsProviderPublic) Present(domain, token, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
|
zone, err := d.getHostedZoneID(ctx, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
rsc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
||||||
|
|
||||||
|
// Get existing record set
|
||||||
|
rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, relative, dns.TXT)
|
||||||
|
if err != nil {
|
||||||
|
var detailed autorest.DetailedError
|
||||||
|
if !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct unique TXT records using map
|
||||||
|
uniqRecords := map[string]struct{}{value: {}}
|
||||||
|
if rset.RecordSetProperties != nil && rset.TxtRecords != nil {
|
||||||
|
for _, txtRecord := range *rset.TxtRecords {
|
||||||
|
// Assume Value doesn't contain multiple strings
|
||||||
|
values := to.StringSlice(txtRecord.Value)
|
||||||
|
if len(values) > 0 {
|
||||||
|
uniqRecords[values[0]] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var txtRecords []dns.TxtRecord
|
||||||
|
for txt := range uniqRecords {
|
||||||
|
txtRecords = append(txtRecords, dns.TxtRecord{Value: &[]string{txt}})
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := dns.RecordSet{
|
||||||
|
Name: &relative,
|
||||||
|
RecordSetProperties: &dns.RecordSetProperties{
|
||||||
|
TTL: to.Int64Ptr(int64(d.config.TTL)),
|
||||||
|
TxtRecords: &txtRecords,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, rec, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (d *dnsProviderPublic) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
|
zone, err := d.getHostedZoneID(ctx, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
|
||||||
|
|
||||||
|
rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
rsc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("azure: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks that azure has a zone for this domain name.
|
||||||
|
func (d *dnsProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
|
||||||
|
if zone := env.GetOrFile(EnvZoneName); zone != "" {
|
||||||
|
return zone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
authZone, err := dns01.FindZoneByFqdn(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dc := dns.NewZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)
|
||||||
|
dc.Authorizer = d.authorizer
|
||||||
|
|
||||||
|
zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// zone.Name shouldn't have a trailing dot(.)
|
||||||
|
return to.String(zone.Name), nil
|
||||||
|
}
|
Loading…
Reference in a new issue