lego/providers/dns/route53/route53.go
JanB 4d9e4f1487 Make sure we don't provision to a private hosted zone
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.
2016-04-21 15:47:43 +02:00

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)},
},
}
}