From 74dc5b1c5818175800e68340ba73b163de0c318a Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 25 Oct 2018 17:38:04 +0200 Subject: [PATCH] Support custom DNS resolvers for Let's Encrypt. --- acme/acme.go | 3 +++ cmd/traefik/traefik.go | 1 + docs/configuration/acme.md | 21 +++++++++++++++++ provider/acme/provider.go | 48 ++++++++++++++++++++++++++++++++++---- types/dns_resolvers.go | 44 ++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 types/dns_resolvers.go diff --git a/acme/acme.go b/acme/acme.go index 73f20b8ae..a740edacd 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -451,6 +451,9 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { return nil, err } + acmeprovider.SetRecursiveNameServers(a.DNSChallenge.Resolvers) + acmeprovider.SetPropagationCheck(a.DNSChallenge.DisablePropagationCheck) + var provider acme.ChallengeProvider provider, err = dns.NewDNSChallengeProviderByName(a.DNSChallenge.Provider) if err != nil { diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index b2bbfc415..5f851af5f 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -71,6 +71,7 @@ Complete documentation is available at https://traefik.io`, f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{}) f.AddParser(reflect.TypeOf(ecs.Clusters{}), &ecs.Clusters{}) f.AddParser(reflect.TypeOf([]types.Domain{}), &types.Domains{}) + f.AddParser(reflect.TypeOf(types.DNSResolvers{}), &types.DNSResolvers{}) f.AddParser(reflect.TypeOf(types.Buckets{}), &types.Buckets{}) f.AddParser(reflect.TypeOf(types.StatusCodes{}), &types.StatusCodes{}) f.AddParser(reflect.TypeOf(types.FieldNames{}), &types.FieldNames{}) diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index cd2d6bf78..b70c36936 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -142,6 +142,23 @@ entryPoint = "https" # # delayBeforeCheck = 0 + # Use following DNS servers to resolve the FQDN authority. + # + # Optional + # Default: empty + # + # resolvers = ["1.1.1.1:53", "8.8.8.8:53"] + + # Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. + # + # NOT RECOMMENDED: + # Increase the risk of reaching Let's Encrypt's rate limits. + # + # Optional + # Default: false + # + # disablePropagationCheck = true + # Domains list. # Only domains defined here can generate wildcard certificates. # The certificates for these domains are negotiated at traefik startup only. @@ -302,6 +319,10 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | +#### `resolvers` + +Use custom DNS servers to resolve the FQDN authority. + ### `domains` You can provide SANs (alternative domains) to each main domain. diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 349ae2619..99fddeaa0 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" fmtlog "log" + "net" "net/url" "reflect" "strings" @@ -74,10 +75,12 @@ type Certificate struct { // DNSChallenge contains DNS challenge Configuration type DNSChallenge struct { - Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` - DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` - preCheckTimeout time.Duration - preCheckInterval time.Duration + Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` + DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` + Resolvers types.DNSResolvers `description:"Use following DNS servers to resolve the FQDN authority."` + DisablePropagationCheck bool `description:"Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended]"` + preCheckTimeout time.Duration + preCheckInterval time.Duration } // HTTPChallenge contains HTTP challenge Configuration @@ -252,6 +255,9 @@ func (p *Provider) getClient() (*acme.Client, error) { if p.DNSChallenge != nil && len(p.DNSChallenge.Provider) > 0 { log.Debugf("Using DNS Challenge provider: %s", p.DNSChallenge.Provider) + SetRecursiveNameServers(p.DNSChallenge.Resolvers) + SetPropagationCheck(p.DNSChallenge.DisablePropagationCheck) + err = dnsOverrideDelay(p.DNSChallenge.DelayBeforeCheck) if err != nil { return nil, err @@ -784,3 +790,37 @@ func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool } return false } + +// SetPropagationCheck to disable the Lego PreCheck. +func SetPropagationCheck(disable bool) { + if disable { + acme.PreCheckDNS = func(_, _ string) (bool, error) { + return true, nil + } + } +} + +// SetRecursiveNameServers to provide a custom DNS resolver. +func SetRecursiveNameServers(dnsResolvers []string) { + resolvers := normaliseDNSResolvers(dnsResolvers) + if len(resolvers) > 0 { + acme.RecursiveNameservers = resolvers + log.Infof("Validating FQDN authority with DNS using %+v", resolvers) + } +} + +// ensure all servers have a port number +func normaliseDNSResolvers(dnsResolvers []string) []string { + var normalisedResolvers []string + for _, server := range dnsResolvers { + srv := strings.TrimSpace(server) + if len(srv) > 0 { + if host, port, err := net.SplitHostPort(srv); err != nil { + normalisedResolvers = append(normalisedResolvers, net.JoinHostPort(srv, "53")) + } else { + normalisedResolvers = append(normalisedResolvers, net.JoinHostPort(host, port)) + } + } + } + return normalisedResolvers +} diff --git a/types/dns_resolvers.go b/types/dns_resolvers.go new file mode 100644 index 000000000..dd96f7895 --- /dev/null +++ b/types/dns_resolvers.go @@ -0,0 +1,44 @@ +package types + +import ( + "fmt" + "strings" +) + +// DNSResolvers is a list of DNSes that we will try to resolve the challenged FQDN against +type DNSResolvers []string + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (r *DNSResolvers) String() string { + return strings.Join(*r, ",") +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (r *DNSResolvers) Set(value string) error { + entryPoints := strings.Split(value, ",") + if len(entryPoints) == 0 { + return fmt.Errorf("wrong DNSResolvers format: %s", value) + } + for _, entryPoint := range entryPoints { + *r = append(*r, entryPoint) + } + return nil +} + +// Get return the DNSResolvers list +func (r *DNSResolvers) Get() interface{} { + return *r +} + +// SetValue sets the DNSResolvers list +func (r *DNSResolvers) SetValue(val interface{}) { + *r = val.(DNSResolvers) +} + +// Type is type of the struct +func (r *DNSResolvers) Type() string { + return "dnsresolvers" +}