Update lego

This commit is contained in:
Michael 2018-07-23 17:30:03 +02:00 committed by Traefiker Bot
parent c8ae97fd38
commit aabebb2185
13 changed files with 696 additions and 112 deletions

View file

@ -0,0 +1,170 @@
// Package acmedns implements a DNS provider for solving DNS-01 challenges using
// Joohoi's acme-dns project. For more information see the ACME-DNS homepage:
// https://github.com/joohoi/acme-dns
package acmedns
import (
"errors"
"fmt"
"github.com/cpu/goacmedns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const (
// envNamespace is the prefix for ACME-DNS environment variables.
envNamespace = "ACME_DNS_"
// apiBaseEnvVar is the environment variable name for the ACME-DNS API address
// (e.g. https://acmedns.your-domain.com).
apiBaseEnvVar = envNamespace + "API_BASE"
// storagePathEnvVar is the environment variable name for the ACME-DNS JSON
// account data file. A per-domain account will be registered/persisted to
// this file and used for TXT updates.
storagePathEnvVar = envNamespace + "STORAGE_PATH"
)
// acmeDNSClient is an interface describing the goacmedns.Client functions
// the DNSProvider uses. It makes it easier for tests to shim a mock Client into
// the DNSProvider.
type acmeDNSClient interface {
// UpdateTXTRecord updates the provided account's TXT record to the given
// value or returns an error.
UpdateTXTRecord(goacmedns.Account, string) error
// RegisterAccount registers and returns a new account with the given
// allowFrom restriction or returns an error.
RegisterAccount([]string) (goacmedns.Account, error)
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface for
// an ACME-DNS server.
type DNSProvider struct {
client acmeDNSClient
storage goacmedns.Storage
}
// NewDNSProvider creates an ACME-DNS provider using file based account storage.
// Its configuration is loaded from the environment by reading apiBaseEnvVar and
// storagePathEnvVar.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(apiBaseEnvVar, storagePathEnvVar)
if err != nil {
return nil, fmt.Errorf("acme-dns: %v", err)
}
client := goacmedns.NewClient(values[apiBaseEnvVar])
storage := goacmedns.NewFileStorage(values[storagePathEnvVar], 0600)
return NewDNSProviderClient(client, storage)
}
// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given
// acmeDNSClient and goacmedns.Storage.
func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) {
if client == nil {
return nil, errors.New("ACME-DNS Client must be not nil")
}
if storage == nil {
return nil, errors.New("ACME-DNS Storage must be not nil")
}
return &DNSProvider{
client: client,
storage: storage,
}, nil
}
// ErrCNAMERequired is returned by Present when the Domain indicated had no
// existing ACME-DNS account in the Storage and additional setup is required.
// The user must create a CNAME in the DNS zone for Domain that aliases FQDN
// to Target in order to complete setup for the ACME-DNS account that was
// created.
type ErrCNAMERequired struct {
// The Domain that is being issued for.
Domain string
// The alias of the CNAME (left hand DNS label).
FQDN string
// The RDATA of the CNAME (right hand side, canonical name).
Target string
}
// Error returns a descriptive message for the ErrCNAMERequired instance telling
// the user that a CNAME needs to be added to the DNS zone of c.Domain before
// the ACME-DNS hook will work. The CNAME to be created should be of the form:
// {{ c.FQDN }} CNAME {{ c.Target }}
func (e ErrCNAMERequired) Error() string {
return fmt.Sprintf("acme-dns: new account created for %q. "+
"To complete setup for %q you must provision the following "+
"CNAME in your DNS zone and re-run this provider when it is "+
"in place:\n"+
"%s CNAME %s.",
e.Domain, e.Domain, e.FQDN, e.Target)
}
// Present creates a TXT record to fulfil the DNS-01 challenge. If there is an
// existing account for the domain in the provider's storage then it will be
// used to set the challenge response TXT record with the ACME-DNS server and
// issuance will continue. If there is not an account for the given domain
// present in the DNSProvider storage one will be created and registered with
// the ACME DNS server and an ErrCNAMERequired error is returned. This will halt
// issuance and indicate to the user that a one-time manual setup is required
// for the domain.
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
// Compute the challenge response FQDN and TXT value for the domain based
// on the keyAuth.
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
// Check if credentials were previously saved for this domain.
account, err := d.storage.Fetch(domain)
// Errors other than goacmeDNS.ErrDomainNotFound are unexpected.
if err != nil && err != goacmedns.ErrDomainNotFound {
return err
}
if err == goacmedns.ErrDomainNotFound {
// The account did not exist. Create a new one and return an error
// indicating the required one-time manual CNAME setup.
return d.register(domain, fqdn)
}
// Update the acme-dns TXT record.
return d.client.UpdateTXTRecord(account, value)
}
// CleanUp removes the record matching the specified parameters. It is not
// implemented for the ACME-DNS provider.
func (d *DNSProvider) CleanUp(_, _, _ string) error {
// ACME-DNS doesn't support the notion of removing a record. For users of
// ACME-DNS it is expected the stale records remain in-place.
return nil
}
// register creates a new ACME-DNS account for the given domain. If account
// creation works as expected a ErrCNAMERequired error is returned describing
// the one-time manual CNAME setup required to complete setup of the ACME-DNS
// hook for the domain. If any other error occurs it is returned as-is.
func (d *DNSProvider) register(domain, fqdn string) error {
// TODO(@cpu): Read CIDR whitelists from the environment
newAcct, err := d.client.RegisterAccount(nil)
if err != nil {
return err
}
// Store the new account in the storage and call save to persist the data.
err = d.storage.Put(domain, newAcct)
if err != nil {
return err
}
err = d.storage.Save()
if err != nil {
return err
}
// Stop issuance by returning an error. The user needs to perform a manual
// one-time CNAME setup in their DNS zone to complete the setup of the new
// account we created.
return ErrCNAMERequired{
Domain: domain,
FQDN: fqdn,
Target: newAcct.FullDomain,
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns/acmedns"
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"github.com/xenolf/lego/providers/dns/bluecat"
@ -43,6 +44,8 @@ import (
// NewDNSChallengeProviderByName Factory for DNS providers
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
switch name {
case "acme-dns":
return acmedns.NewDNSProvider()
case "azure":
return azure.NewDNSProvider()
case "auroradns":

View file

@ -30,26 +30,32 @@ func NewDNSProvider() (*DNSProvider, error) {
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for http://duckdns.org .
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
if duckdnsToken == "" {
func NewDNSProviderCredentials(token string) (*DNSProvider, error) {
if token == "" {
return nil, errors.New("DuckDNS: credentials missing")
}
return &DNSProvider{token: duckdnsToken}, nil
return &DNSProvider{token: token}, nil
}
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
// txt == "" will clear the TXT record.
func makeDuckdnsURL(domain, token, txt string) string {
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
if txt == "" {
return requestBase + "&clear=true"
}
return requestBase + "&txt=" + txt
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
return updateTxtRecord(domain, d.token, txtRecord, false)
}
func issueDuckdnsRequest(url string) error {
response, err := acme.HTTPClient.Get(url)
// CleanUp clears DuckDNS TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return updateTxtRecord(domain, d.token, "", true)
}
// updateTxtRecord Update the domains TXT record
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all sub domains.
func updateTxtRecord(domain, token, txt string, clear bool) error {
u := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s&clear=%t&txt=%s", domain, token, clear, txt)
response, err := acme.HTTPClient.Get(u)
if err != nil {
return err
}
@ -59,26 +65,10 @@ func issueDuckdnsRequest(url string) error {
if err != nil {
return err
}
body := string(bodyBytes)
if body != "OK" {
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, u)
}
return nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// In duckdns you only have one TXT record shared with
// the domain and all sub domains.
//
// To update the TXT record we just need to make one simple get request.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
url := makeDuckdnsURL(domain, d.token, txtRecord)
return issueDuckdnsRequest(url)
}
// CleanUp clears duckdns TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
url := makeDuckdnsURL(domain, d.token, "")
return issueDuckdnsRequest(url)
}

View file

@ -0,0 +1,42 @@
/*
Package exec implements a manual DNS provider which runs a program for adding/removing the DNS record.
The file name of the external program is specified in the environment variable `EXEC_PATH`.
When it is run by lego, three command-line parameters are passed to it:
The action ("present" or "cleanup"), the fully-qualified domain name, the value for the record and the TTL.
For example, requesting a certificate for the domain 'foo.example.com' can be achieved by calling lego as follows:
EXEC_PATH=./update-dns.sh \
lego --dns exec \
--domains foo.example.com \
--email invalid@example.com run
It will then call the program './update-dns.sh' with like this:
./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
The program then needs to make sure the record is inserted.
When it returns an error via a non-zero exit code, lego aborts.
When the record is to be removed again,
the program is called with the first command-line parameter set to "cleanup" instead of "present".
If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`:
EXEC_MODE=RAW \
EXEC_PATH=./update-dns.sh \
lego --dns exec \
--domains foo.example.com \
--email invalid@example.com run
It will then call the program './update-dns.sh' like this:
./update-dns.sh "present" "foo.example.com." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8"
NOTE:
The `--` is because the token MAY start with a `-`, and the called program may try and interpret a - as indicating a flag.
In the case of urfave, which is commonly used,
you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely.
*/
package exec

View file

@ -1,78 +1,100 @@
// Package exec implements a manual DNS provider which runs a program for
// adding/removing the DNS record.
//
// The file name of the external program is specified in the environment
// variable EXEC_PATH. When it is run by lego, three command-line parameters
// are passed to it: The action ("present" or "cleanup"), the fully-qualified domain
// name, the value for the record and the TTL.
//
// For example, requesting a certificate for the domain 'foo.example.com' can
// be achieved by calling lego as follows:
//
// EXEC_PATH=./update-dns.sh \
// lego --dns exec \
// --domains foo.example.com \
// --email invalid@example.com run
//
// It will then call the program './update-dns.sh' with like this:
//
// ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
//
// The program then needs to make sure the record is inserted. When it returns
// an error via a non-zero exit code, lego aborts.
//
// When the record is to be removed again, the program is called with the first
// command-line parameter set to "cleanup" instead of "present".
package exec
import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/log"
"github.com/xenolf/lego/platform/config/env"
)
// Config Provider configuration.
type Config struct {
Program string
Mode string
}
// DNSProvider adds and removes the record for the DNS challenge by calling a
// program with command-line parameters.
type DNSProvider struct {
program string
config *Config
}
// NewDNSProvider returns a new DNS provider which runs the program in the
// environment variable EXEC_PATH for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
s := os.Getenv("EXEC_PATH")
if s == "" {
return nil, errors.New("environment variable EXEC_PATH not set")
values, err := env.Get("EXEC_PATH")
if err != nil {
return nil, fmt.Errorf("exec: %v", err)
}
return NewDNSProviderProgram(s)
return NewDNSProviderConfig(&Config{
Program: values["EXEC_PATH"],
Mode: os.Getenv("EXEC_MODE"),
})
}
// NewDNSProviderConfig returns a new DNS provider which runs the given configuration
// for adding and removing the DNS record.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration is nil")
}
return &DNSProvider{config: config}, nil
}
// NewDNSProviderProgram returns a new DNS provider which runs the given program
// for adding and removing the DNS record.
// Deprecated: use NewDNSProviderConfig instead
func NewDNSProviderProgram(program string) (*DNSProvider, error) {
return &DNSProvider{program: program}, nil
if len(program) == 0 {
return nil, errors.New("the program is undefined")
}
return NewDNSProviderConfig(&Config{Program: program})
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "present", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var args []string
if d.config.Mode == "RAW" {
args = []string{"present", "--", domain, token, keyAuth}
} else {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
args = []string{"present", fqdn, value, strconv.Itoa(ttl)}
}
return cmd.Run()
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "cleanup", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var args []string
if d.config.Mode == "RAW" {
args = []string{"cleanup", "--", domain, token, keyAuth}
} else {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
args = []string{"cleanup", fqdn, value, strconv.Itoa(ttl)}
}
return cmd.Run()
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}

View file

@ -115,13 +115,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
}
// Look for existing records.
list, err := d.client.ResourceRecordSets.List(d.project, zone).Name(fqdn).Type("TXT").Do()
existing, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return err
}
if len(list.Rrsets) > 0 {
if len(existing) > 0 {
// Attempt to delete the existing records when adding our new one.
change.Deletions = list.Rrsets
change.Deletions = existing
}
chg, err := d.client.Changes.Create(d.project, zone, change).Do()
@ -156,16 +156,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return err
}
for _, rec := range records {
change := &dns.Change{
Deletions: []*dns.ResourceRecordSet{rec},
}
_, err = d.client.Changes.Create(d.project, zone, change).Do()
if err != nil {
return err
}
if len(records) == 0 {
return nil
}
return nil
_, err = d.client.Changes.Create(d.project, zone, &dns.Change{Deletions: records}).Do()
return err
}
// Timeout customizes the timeout values used by the ACME package for checking
@ -198,17 +194,10 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
recs, err := d.client.ResourceRecordSets.List(d.project, zone).Do()
recs, err := d.client.ResourceRecordSets.List(d.project, zone).Name(fqdn).Type("TXT").Do()
if err != nil {
return nil, err
}
var found []*dns.ResourceRecordSet
for _, r := range recs.Rrsets {
if r.Type == "TXT" && r.Name == fqdn {
found = append(found, r)
}
}
return found, nil
return recs.Rrsets, nil
}

View file

@ -5,6 +5,7 @@ package ns1
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/xenolf/lego/acme"
@ -75,7 +76,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (d *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
zone, _, err := d.client.Zones.Get(domain)
authZone, err := getAuthZone(domain)
if err != nil {
return nil, err
}
zone, _, err := d.client.Zones.Get(authZone)
if err != nil {
return nil, err
}
@ -83,6 +89,19 @@ func (d *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
return zone, nil
}
func getAuthZone(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
if strings.HasSuffix(authZone, ".") {
authZone = authZone[:len(authZone)-len(".")]
}
return authZone, err
}
func (d *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record {
name := acme.UnFqdn(fqdn)

View file

@ -3,6 +3,7 @@
package route53
import (
"errors"
"fmt"
"math/rand"
"os"
@ -17,15 +18,30 @@ import (
"github.com/xenolf/lego/acme"
)
const (
maxRetries = 5
route53TTL = 10
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
MaxRetries int
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HostedZoneID string
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
MaxRetries: 5,
TTL: 10,
PropagationTimeout: time.Minute * 2,
PollingInterval: time.Second * 4,
HostedZoneID: os.Getenv("AWS_HOSTED_ZONE_ID"),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *route53.Route53
hostedZoneID string
client *route53.Route53
config *Config
}
// customRetryer implements the client.Retryer interface by composing the
@ -65,35 +81,49 @@ func (d customRetryer) RetryRules(r *request.Request) time.Duration {
//
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
func NewDNSProvider() (*DNSProvider, error) {
hostedZoneID := os.Getenv("AWS_HOSTED_ZONE_ID")
return NewDNSProviderConfig(NewDefaultConfig())
}
// NewDNSProviderConfig takes a given config ans returns a custom configured
// DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the Route53 DNS provider is nil")
}
r := customRetryer{}
r.NumMaxRetries = maxRetries
config := request.WithRetryer(aws.NewConfig(), r)
session, err := session.NewSessionWithOptions(session.Options{Config: *config})
r.NumMaxRetries = config.MaxRetries
sessionCfg := request.WithRetryer(aws.NewConfig(), r)
session, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
if err != nil {
return nil, err
}
client := route53.New(session)
return &DNSProvider{
client: client,
hostedZoneID: hostedZoneID,
client: client,
config: config,
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation.
func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
return r.config.PropagationTimeout, r.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("UPSERT", fqdn, value, route53TTL)
return r.changeRecord("UPSERT", fqdn, value, r.config.TTL)
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("DELETE", fqdn, value, route53TTL)
return r.changeRecord("DELETE", fqdn, value, r.config.TTL)
}
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
@ -123,7 +153,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
statusID := resp.ChangeInfo.Id
return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
return acme.WaitFor(r.config.PropagationTimeout, r.config.PollingInterval, func() (bool, error) {
reqParams := &route53.GetChangeInput{
Id: statusID,
}
@ -139,8 +169,8 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
}
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
if r.hostedZoneID != "" {
return r.hostedZoneID, nil
if r.config.HostedZoneID != "" {
return r.config.HostedZoneID, nil
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)