forked from TrueCloudLab/lego
4d9e4f1487
Route 53 allows multiple zones with the same name to co-exist in an account. The most common use case for this is a split-view DNS with one private and one public zone for the same domain name. This patch makes sure we don’t ever provision the authorization record to the private zone. The other case where a user has multiple public zones with the same name is not covered here since this would require a bigger change in code in order to determine which of the zones is active from the viewpoint of the internet. Also this is probably an edge use case that can be addressed once it comes up in the issues.
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
// Package route53 implements a DNS provider for solving the DNS-01 challenge
|
|
// using AWS Route 53 DNS.
|
|
package route53
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/client"
|
|
"github.com/aws/aws-sdk-go/aws/request"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/route53"
|
|
"github.com/xenolf/lego/acme"
|
|
)
|
|
|
|
const (
|
|
maxRetries = 5
|
|
)
|
|
|
|
// DNSProvider implements the acme.ChallengeProvider interface
|
|
type DNSProvider struct {
|
|
client *route53.Route53
|
|
}
|
|
|
|
// customRetryer implements the client.Retryer interface by composing the
|
|
// DefaultRetryer. It controls the logic for retrying recoverable request
|
|
// errors (e.g. when rate limits are exceeded).
|
|
type customRetryer struct {
|
|
client.DefaultRetryer
|
|
}
|
|
|
|
// RetryRules overwrites the DefaultRetryer's method.
|
|
// It uses a basic exponential backoff algorithm that returns an initial
|
|
// delay of ~400ms with an upper limit of ~30 seconds which should prevent
|
|
// causing a high number of consecutive throttling errors.
|
|
// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
|
|
func (d customRetryer) RetryRules(r *request.Request) time.Duration {
|
|
retryCount := r.RetryCount
|
|
if retryCount > 7 {
|
|
retryCount = 7
|
|
}
|
|
|
|
delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
|
|
return time.Duration(delay) * time.Millisecond
|
|
}
|
|
|
|
// NewDNSProvider returns a DNSProvider instance configured for the AWS
|
|
// Route 53 service.
|
|
//
|
|
// AWS Credentials are automatically detected in the following locations
|
|
// and prioritized in the following order:
|
|
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
|
|
// AWS_REGION, [AWS_SESSION_TOKEN]
|
|
// 2. Shared credentials file (defaults to ~/.aws/credentials)
|
|
// 3. Amazon EC2 IAM role
|
|
//
|
|
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
|
|
func NewDNSProvider() (*DNSProvider, error) {
|
|
r := customRetryer{}
|
|
r.NumMaxRetries = maxRetries
|
|
config := request.WithRetryer(aws.NewConfig(), r)
|
|
client := route53.New(session.New(config))
|
|
|
|
return &DNSProvider{client: client}, nil
|
|
}
|
|
|
|
// Present creates a TXT record using the specified parameters
|
|
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
|
value = `"` + value + `"`
|
|
return r.changeRecord("UPSERT", fqdn, value, ttl)
|
|
}
|
|
|
|
// CleanUp removes the TXT record matching the specified parameters
|
|
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
|
value = `"` + value + `"`
|
|
return r.changeRecord("DELETE", fqdn, value, ttl)
|
|
}
|
|
|
|
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
|
hostedZoneID, err := r.getHostedZoneID(fqdn)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err)
|
|
}
|
|
|
|
recordSet := newTXTRecordSet(fqdn, value, ttl)
|
|
reqParams := &route53.ChangeResourceRecordSetsInput{
|
|
HostedZoneId: aws.String(hostedZoneID),
|
|
ChangeBatch: &route53.ChangeBatch{
|
|
Comment: aws.String("Managed by Lego"),
|
|
Changes: []*route53.Change{
|
|
{
|
|
Action: aws.String(action),
|
|
ResourceRecordSet: recordSet,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := r.client.ChangeResourceRecordSets(reqParams)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to change Route 53 record set: %v", err)
|
|
}
|
|
|
|
statusID := resp.ChangeInfo.Id
|
|
|
|
return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
|
|
reqParams := &route53.GetChangeInput{
|
|
Id: statusID,
|
|
}
|
|
resp, err := r.client.GetChange(reqParams)
|
|
if err != nil {
|
|
return false, fmt.Errorf("Failed to query Route 53 change status: %v", err)
|
|
}
|
|
if *resp.ChangeInfo.Status == route53.ChangeStatusInsync {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
|
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// .DNSName should not have a trailing dot
|
|
reqParams := &route53.ListHostedZonesByNameInput{
|
|
DNSName: aws.String(acme.UnFqdn(authZone)),
|
|
}
|
|
resp, err := r.client.ListHostedZonesByName(reqParams)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var hostedZoneID string
|
|
for _, hostedZone := range resp.HostedZones {
|
|
// .Name has a trailing dot
|
|
if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone {
|
|
hostedZoneID = *hostedZone.Id
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(hostedZoneID) == 0 {
|
|
return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn)
|
|
}
|
|
|
|
if strings.HasPrefix(hostedZoneID, "/hostedzone/") {
|
|
hostedZoneID = strings.TrimPrefix(hostedZoneID, "/hostedzone/")
|
|
}
|
|
|
|
return hostedZoneID, nil
|
|
}
|
|
|
|
func newTXTRecordSet(fqdn, value string, ttl int) *route53.ResourceRecordSet {
|
|
return &route53.ResourceRecordSet{
|
|
Name: aws.String(fqdn),
|
|
Type: aws.String("TXT"),
|
|
TTL: aws.Int64(int64(ttl)),
|
|
ResourceRecords: []*route53.ResourceRecord{
|
|
{Value: aws.String(value)},
|
|
},
|
|
}
|
|
}
|