Generate wildcard certificate with SANs in ACME
This commit is contained in:
parent
8168d2fdc1
commit
7109910f46
88
acme/acme.go
88
acme/acme.go
|
@ -46,9 +46,9 @@ type ACME struct {
|
||||||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
||||||
CAServer string `description:"CA server to use."`
|
CAServer string `description:"CA server to use."`
|
||||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
||||||
DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-02 Challenge"`
|
DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"`
|
||||||
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
||||||
DNSProvider string `description:"Activate DNS-02 Challenge (Deprecated)"` // deprecated
|
DNSProvider string `description:"Activate DNS-01 Challenge (Deprecated)"` // deprecated
|
||||||
DelayDontCheckDNS flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // deprecated
|
DelayDontCheckDNS flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // deprecated
|
||||||
ACMELogging bool `description:"Enable debug logging of ACME actions."`
|
ACMELogging bool `description:"Enable debug logging of ACME actions."`
|
||||||
client *acme.Client
|
client *acme.Client
|
||||||
|
@ -62,20 +62,6 @@ type ACME struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACME) init() error {
|
func (a *ACME) init() error {
|
||||||
// FIXME temporary fix, waiting for https://github.com/xenolf/lego/pull/478
|
|
||||||
acme.HTTPClient = http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).Dial,
|
|
||||||
TLSHandshakeTimeout: 15 * time.Second,
|
|
||||||
ResponseHeaderTimeout: 15 * time.Second,
|
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.ACMELogging {
|
if a.ACMELogging {
|
||||||
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
|
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
|
||||||
} else {
|
} else {
|
||||||
|
@ -651,6 +637,7 @@ func (a *ACME) runJobs() {
|
||||||
|
|
||||||
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it
|
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it
|
||||||
func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string, error) {
|
func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string, error) {
|
||||||
|
// Check if the domains array is empty or contains only one empty value
|
||||||
if len(domains) == 0 || (len(domains) == 1 && len(domains[0]) == 0) {
|
if len(domains) == 0 || (len(domains) == 1 && len(domains[0]) == 0) {
|
||||||
return nil, errors.New("unable to generate a certificate when no domain is given")
|
return nil, errors.New("unable to generate a certificate when no domain is given")
|
||||||
}
|
}
|
||||||
|
@ -663,15 +650,14 @@ func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string
|
||||||
if a.DNSChallenge == nil && len(a.DNSProvider) == 0 {
|
if a.DNSChallenge == nil && len(a.DNSProvider) == 0 {
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(domains[0], "*.*") {
|
||||||
if len(domains) > 1 {
|
return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : SANs are not allowed", strings.Join(domains, ","))
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
for _, san := range domains[1:] {
|
for _, san := range domains[1:] {
|
||||||
if strings.HasPrefix(san, "*") {
|
if strings.HasPrefix(san, "*") {
|
||||||
return nil, fmt.Errorf("unable to generate a certificate in ACME provider for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a certificate for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ","))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,31 +696,37 @@ func (a *ACME) deleteUnnecessaryDomains() {
|
||||||
keepDomain = false
|
keepDomain = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
} else if strings.HasPrefix(domain.Main, "*") && domain.SANs == nil {
|
|
||||||
// Check if domains can be validated by the wildcard domain
|
|
||||||
|
|
||||||
var newDomainsToCheck []string
|
|
||||||
|
|
||||||
// Check if domains can be validated by the wildcard domain
|
|
||||||
domainsMap := make(map[string]*tls.Certificate)
|
|
||||||
domainsMap[domain.Main] = &tls.Certificate{}
|
|
||||||
|
|
||||||
for _, domainProcessed := range domainToCheck.ToStrArray() {
|
|
||||||
if isDomainAlreadyChecked(domainProcessed, domainsMap) {
|
|
||||||
log.Warnf("Domain %q will not be processed by ACME because it is validated by the wildcard %q", domainProcessed, domain.Main)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newDomainsToCheck = append(newDomainsToCheck, domainProcessed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the domain if both Main and SANs can be validated by the wildcard domain
|
|
||||||
// otherwise keep the unchecked values
|
|
||||||
if newDomainsToCheck == nil {
|
|
||||||
keepDomain = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
domainToCheck.Set(newDomainsToCheck)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newDomainsToCheck []string
|
||||||
|
|
||||||
|
// Check if domains can be validated by the wildcard domain
|
||||||
|
domainsMap := make(map[string]*tls.Certificate)
|
||||||
|
domainsMap[domain.Main] = &tls.Certificate{}
|
||||||
|
if len(domain.SANs) > 0 {
|
||||||
|
domainsMap[strings.Join(domain.SANs, ",")] = &tls.Certificate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domainProcessed := range domainToCheck.ToStrArray() {
|
||||||
|
if idxDomain < idxDomainToCheck && isDomainAlreadyChecked(domainProcessed, domainsMap) {
|
||||||
|
// The domain is duplicated in a CN
|
||||||
|
log.Warnf("Domain %q is duplicated in the configuration or validated by the domain %v. It will be processed once.", domainProcessed, domain)
|
||||||
|
continue
|
||||||
|
} else if domain.Main != domainProcessed && strings.HasPrefix(domain.Main, "*") && types.MatchDomain(domainProcessed, domain.Main) {
|
||||||
|
// Check if a wildcard can validate the domain
|
||||||
|
log.Warnf("Domain %q will not be processed by ACME provider because it is validated by the wildcard %q", domainProcessed, domain.Main)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newDomainsToCheck = append(newDomainsToCheck, domainProcessed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the domain if both Main and SANs can be validated by the wildcard domain
|
||||||
|
// otherwise keep the unchecked values
|
||||||
|
if newDomainsToCheck == nil {
|
||||||
|
keepDomain = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
domainToCheck.Set(newDomainsToCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
if keepDomain {
|
if keepDomain {
|
||||||
|
|
|
@ -417,11 +417,27 @@ func TestAcme_getValidDomain(t *testing.T) {
|
||||||
expectedDomains: nil,
|
expectedDomains: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "unexpected SANs",
|
desc: "unauthorized wildcard with SAN",
|
||||||
domains: []string{"*.traefik.wtf", "foo.traefik.wtf"},
|
domains: []string{"*.*.traefik.wtf", "foo.traefik.wtf"},
|
||||||
dnsChallenge: &acmeprovider.DNSChallenge{},
|
dnsChallenge: &acmeprovider.DNSChallenge{},
|
||||||
wildcardAllowed: true,
|
wildcardAllowed: true,
|
||||||
expectedErr: "unable to generate a wildcard certificate for domain \"*.traefik.wtf,foo.traefik.wtf\" : SANs are not allowed",
|
expectedErr: "unable to generate a wildcard certificate for domain \"*.*.traefik.wtf,foo.traefik.wtf\" : ACME does not allow '*.*' wildcard domain",
|
||||||
|
expectedDomains: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard with SANs",
|
||||||
|
domains: []string{"*.traefik.wtf", "traefik.wtf"},
|
||||||
|
dnsChallenge: &acmeprovider.DNSChallenge{},
|
||||||
|
wildcardAllowed: true,
|
||||||
|
expectedErr: "",
|
||||||
|
expectedDomains: []string{"*.traefik.wtf", "traefik.wtf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unexpected SANs",
|
||||||
|
domains: []string{"*.traefik.wtf", "*.acme.wtf"},
|
||||||
|
dnsChallenge: &acmeprovider.DNSChallenge{},
|
||||||
|
wildcardAllowed: true,
|
||||||
|
expectedErr: "unable to generate a certificate for domains \"*.traefik.wtf,*.acme.wtf\": SANs can not be a wildcard domain",
|
||||||
expectedDomains: nil,
|
expectedDomains: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ entryPoint = "https"
|
||||||
#
|
#
|
||||||
entryPoint = "http"
|
entryPoint = "http"
|
||||||
|
|
||||||
# Use a DNS-01/DNS-02 acme challenge rather than HTTP-01 challenge.
|
# Use a DNS-01/DNS-01 acme challenge rather than HTTP-01 challenge.
|
||||||
# Note : Mandatory for wildcard certificates generation.
|
# Note : Mandatory for wildcard certificates generation.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
|
@ -264,7 +264,7 @@ defaultEntryPoints = ["http", "https"]
|
||||||
|
|
||||||
### `dnsChallenge`
|
### `dnsChallenge`
|
||||||
|
|
||||||
Use `DNS-01/DNS-02` challenge to generate/renew ACME certificates.
|
Use `DNS-01/DNS-01` challenge to generate/renew ACME certificates.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[acme]
|
[acme]
|
||||||
|
@ -276,7 +276,7 @@ Use `DNS-01/DNS-02` challenge to generate/renew ACME certificates.
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
ACME wildcard certificates can only be generated thanks to a `DNS-02` challenge.
|
ACME wildcard certificates can only be generated thanks to a `DNS-01` challenge.
|
||||||
|
|
||||||
#### `provider`
|
#### `provider`
|
||||||
|
|
||||||
|
@ -397,14 +397,18 @@ CA server to use.
|
||||||
main = "local3.com"
|
main = "local3.com"
|
||||||
[[acme.domains]]
|
[[acme.domains]]
|
||||||
main = "*.local4.com"
|
main = "*.local4.com"
|
||||||
|
sans = ["local4.com", "test1.test1.local4.com"]
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Wildcard domains
|
#### Wildcard domains
|
||||||
|
|
||||||
Wildcard domain has to be defined as a main domain **with no SANs** (alternative domains).
|
Wildcard domain has to be defined as a main domain.
|
||||||
All domains must have A/AAAA records pointing to Træfik.
|
All domains must have A/AAAA records pointing to Træfik.
|
||||||
|
|
||||||
|
Due to ACME limitation, it's not possible to define a wildcard as a SAN (alternative domains).
|
||||||
|
It's neither possible to define a wildcard on a wildcard domain (for example `*.*.local.com`).
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Note that Let's Encrypt has [rate limiting](https://letsencrypt.org/docs/rate-limits).
|
Note that Let's Encrypt has [rate limiting](https://letsencrypt.org/docs/rate-limits).
|
||||||
|
|
||||||
|
@ -435,9 +439,9 @@ Each domain & SANs will lead to a certificate request.
|
||||||
[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) allows wildcard certificate support.
|
[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) allows wildcard certificate support.
|
||||||
However, this feature needs a specific configuration.
|
However, this feature needs a specific configuration.
|
||||||
|
|
||||||
### DNS-02 Challenge
|
### DNS-01 Challenge
|
||||||
|
|
||||||
As described in [Let's Encrypt post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605), wildcard certificates can only be generated through a `DNS-02`Challenge.
|
As described in [Let's Encrypt post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605), wildcard certificates can only be generated through a `DNS-01` Challenge.
|
||||||
This challenge is linked to the Træfik option `acme.dnsChallenge`.
|
This challenge is linked to the Træfik option `acme.dnsChallenge`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
@ -454,16 +458,88 @@ For more information about this option, please refer to the [dnsChallenge sectio
|
||||||
### Wildcard domain
|
### Wildcard domain
|
||||||
|
|
||||||
Wildcard domains can currently be provided only by to the `acme.domains` option.
|
Wildcard domains can currently be provided only by to the `acme.domains` option.
|
||||||
Theses domains can not have SANs.
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[acme]
|
[acme]
|
||||||
# ...
|
# ...
|
||||||
[[acme.domains]]
|
[[acme.domains]]
|
||||||
main = "*local1.com"
|
main = "*.local1.com"
|
||||||
|
sans = ["local1.com"]
|
||||||
[[acme.domains]]
|
[[acme.domains]]
|
||||||
main = "*.local2.com"
|
main = "*.local2.com"
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information about this option, please refer to the [domains section](/configuration/acme/#domains).
|
For more information about this option, please refer to the [domains section](/configuration/acme/#domains).
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
Let's Encrypt wildcard support have some limitations to take into account :
|
||||||
|
|
||||||
|
- Wildcard domain can not be a SAN (alternative domain),
|
||||||
|
- Wildcard domain on a wildcard domain is forbidden (for example `*.*.local.com`),
|
||||||
|
- A DNS-01 Challenge is executed for each domain (CN and SANs), DNS provider can not manage correctly this behavior as explained in the [DNS provider support section](/configuration/acme/#dns-provider-support)
|
||||||
|
|
||||||
|
|
||||||
|
### DNS provider support
|
||||||
|
|
||||||
|
All DNS providers allow creating ACME wildcard certificates.
|
||||||
|
However, many troubles can appear for wildcard domains with SANs.
|
||||||
|
|
||||||
|
If a wildcard domain is defined with it root domain as SAN, as described below, 2 DNS-01 Challenges will be executed.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[acme]
|
||||||
|
# ...
|
||||||
|
[[acme.domains]]
|
||||||
|
main = "*.local1.com"
|
||||||
|
sans = ["local1.com"]
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
When a DNS-01 Challenge is done, Let's Encrypt checks if a TXT record is created with a given name and a given value.
|
||||||
|
When a certificate is generated for a wildcard domain is defined with it root domain as SAN, the requested TXT record name for both the wildcard domain and the root domain is the same.
|
||||||
|
|
||||||
|
The [DNS RFC](https://community.letsencrypt.org/t/wildcard-issuance-two-txt-records-for-the-same-name/54528/2) allows this behavior.
|
||||||
|
But all DNS providers keep TXT records values in a cache with a TTL.
|
||||||
|
In function of the parameters given by the Træfik ACME client library ([LEGO](https://github.com/xenolf/lego)), the TXT record TTL can be superior to challenge Timeout.
|
||||||
|
In that event, the DNS-01 Challenge will not work correctly.
|
||||||
|
|
||||||
|
[LEGO](https://github.com/xenolf/lego) will involve in the way to be adapted to all of DNS providers.
|
||||||
|
Meanwhile, the table described below contains all the DNS providers supported by Træfik and indicates if they allow generating certificates for a wildcard domain and its root domain.
|
||||||
|
Do not hesitate to complete it.
|
||||||
|
|
||||||
|
| Provider Name | Provider code | Wildcard and Root Domain Support |
|
||||||
|
|--------------------------------------------------------|----------------|----------------------------------|
|
||||||
|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | Not tested yet |
|
||||||
|
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | Not tested yet |
|
||||||
|
| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | Not tested yet |
|
||||||
|
| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | YES |
|
||||||
|
| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | Not tested yet |
|
||||||
|
| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | YES |
|
||||||
|
| [DNSimple](https://dnsimple.com) | `dnsimple` | Not tested yet |
|
||||||
|
| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | Not tested yet |
|
||||||
|
| [DNSPod](http://www.dnspod.net/) | `dnspod` | Not tested yet |
|
||||||
|
| [Duck DNS](https://www.duckdns.org/) | `duckdns` | Not tested yet |
|
||||||
|
| [Dyn](https://dyn.com) | `dyn` | Not tested yet |
|
||||||
|
| External Program | `exec` | Not tested yet |
|
||||||
|
| [Exoscale](https://www.exoscale.ch) | `exoscale` | Not tested yet |
|
||||||
|
| [Fast DNS](https://www.akamai.com/) | `fastdns` | Not tested yet |
|
||||||
|
| [Gandi](https://www.gandi.net) | `gandi` | Not tested yet |
|
||||||
|
| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | Not tested yet |
|
||||||
|
| [Glesys](https://glesys.com/) | `glesys` | Not tested yet |
|
||||||
|
| [GoDaddy](https://godaddy.com/domains) | `godaddy` | Not tested yet |
|
||||||
|
| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | YES |
|
||||||
|
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | Not tested yet |
|
||||||
|
| [Linode](https://www.linode.com) | `linode` | Not tested yet |
|
||||||
|
| manual | - | YES |
|
||||||
|
| [Namecheap](https://www.namecheap.com) | `namecheap` | Not tested yet |
|
||||||
|
| [name.com](https://www.name.com/) | `namedotcom` | Not tested yet |
|
||||||
|
| [Ns1](https://ns1.com/) | `ns1` | Not tested yet |
|
||||||
|
| [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | Not tested yet |
|
||||||
|
| [OVH](https://www.ovh.com) | `ovh` | YES |
|
||||||
|
| [PowerDNS](https://www.powerdns.com) | `pdns` | Not tested yet |
|
||||||
|
| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | Not tested yet |
|
||||||
|
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | Not tested yet |
|
||||||
|
| [Route 53](https://aws.amazon.com/route53/) | `route53` | YES |
|
||||||
|
| [VULTR](https://www.vultr.com) | `vultr` | Not tested yet |
|
||||||
|
|
|
@ -42,7 +42,7 @@ type Configuration struct {
|
||||||
EntryPoint string `description:"EntryPoint to use."`
|
EntryPoint string `description:"EntryPoint to use."`
|
||||||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
||||||
OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated
|
OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated
|
||||||
DNSChallenge *DNSChallenge `description:"Activate DNS-02 Challenge"`
|
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"`
|
||||||
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
||||||
Domains []types.Domain `description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge"`
|
Domains []types.Domain `description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge"`
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ type Certificate struct {
|
||||||
|
|
||||||
// DNSChallenge contains DNS challenge Configuration
|
// DNSChallenge contains DNS challenge Configuration
|
||||||
type DNSChallenge struct {
|
type DNSChallenge struct {
|
||||||
Provider string `description:"Use a DNS-02 based challenge provider rather than HTTPS."`
|
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."`
|
DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,16 +565,16 @@ func (p *Provider) getValidDomains(domain types.Domain, wildcardAllowed bool) ([
|
||||||
if p.DNSChallenge == nil {
|
if p.DNSChallenge == nil {
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
||||||
}
|
}
|
||||||
if len(domain.SANs) > 0 {
|
if strings.HasPrefix(domain.Main, "*.*") {
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : SANs are not allowed", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, san := range domain.SANs {
|
|
||||||
if strings.HasPrefix(san, "*") {
|
|
||||||
return nil, fmt.Errorf("unable to generate a certificate in ACME provider for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ","))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, san := range domain.SANs {
|
||||||
|
if strings.HasPrefix(san, "*") {
|
||||||
|
return nil, fmt.Errorf("unable to generate a certificate in ACME provider for domains %q: SAN %q can not be a wildcard domain", strings.Join(domains, ","), san)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
||||||
return domains, nil
|
return domains, nil
|
||||||
}
|
}
|
||||||
|
@ -610,26 +610,31 @@ func (p *Provider) deleteUnnecessaryDomains() {
|
||||||
keepDomain = false
|
keepDomain = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
} else if strings.HasPrefix(domain.Main, "*") && domain.SANs == nil {
|
|
||||||
|
|
||||||
// Check if domains can be validated by the wildcard domain
|
|
||||||
var newDomainsToCheck []string
|
|
||||||
for _, domainProcessed := range domainToCheck.ToStrArray() {
|
|
||||||
if isDomainAlreadyChecked(domainProcessed, domain.ToStrArray()) {
|
|
||||||
log.Warnf("Domain %q will not be processed by ACME provider because it is validated by the wildcard %q", domainProcessed, domain.Main)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newDomainsToCheck = append(newDomainsToCheck, domainProcessed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the domain if both Main and SANs can be validated by the wildcard domain
|
|
||||||
// otherwise keep the unchecked values
|
|
||||||
if newDomainsToCheck == nil {
|
|
||||||
keepDomain = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
domainToCheck.Set(newDomainsToCheck)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if CN or SANS to check already exists
|
||||||
|
// or can not be checked by a wildcard
|
||||||
|
var newDomainsToCheck []string
|
||||||
|
for _, domainProcessed := range domainToCheck.ToStrArray() {
|
||||||
|
if idxDomain < idxDomainToCheck && isDomainAlreadyChecked(domainProcessed, domain.ToStrArray()) {
|
||||||
|
// The domain is duplicated in a CN
|
||||||
|
log.Warnf("Domain %q is duplicated in the configuration or validated by the domain %v. It will be processed once.", domainProcessed, domain)
|
||||||
|
continue
|
||||||
|
} else if domain.Main != domainProcessed && strings.HasPrefix(domain.Main, "*") && isDomainAlreadyChecked(domainProcessed, []string{domain.Main}) {
|
||||||
|
// Check if a wildcard can validate the domain
|
||||||
|
log.Warnf("Domain %q will not be processed by ACME provider because it is validated by the wildcard %q", domainProcessed, domain.Main)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newDomainsToCheck = append(newDomainsToCheck, domainProcessed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the domain if both Main and SANs can be validated by the wildcard domain
|
||||||
|
// otherwise keep the unchecked values
|
||||||
|
if newDomainsToCheck == nil {
|
||||||
|
keepDomain = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
domainToCheck.Set(newDomainsToCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
if keepDomain {
|
if keepDomain {
|
||||||
|
|
|
@ -207,11 +207,27 @@ func TestGetValidDomain(t *testing.T) {
|
||||||
expectedDomains: nil,
|
expectedDomains: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "unexpected SANs",
|
desc: "unauthorized wildcard with SAN",
|
||||||
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
|
domains: types.Domain{Main: "*.*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
|
||||||
dnsChallenge: &DNSChallenge{},
|
dnsChallenge: &DNSChallenge{},
|
||||||
wildcardAllowed: true,
|
wildcardAllowed: true,
|
||||||
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : SANs are not allowed",
|
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.*.traefik.wtf,foo.traefik.wtf\" : ACME does not allow '*.*' wildcard domain",
|
||||||
|
expectedDomains: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard and SANs",
|
||||||
|
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"traefik.wtf"}},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
wildcardAllowed: true,
|
||||||
|
expectedErr: "",
|
||||||
|
expectedDomains: []string{"*.traefik.wtf", "traefik.wtf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unexpected SANs",
|
||||||
|
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"*.acme.wtf"}},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
wildcardAllowed: true,
|
||||||
|
expectedErr: "unable to generate a certificate in ACME provider for domains \"*.traefik.wtf,*.acme.wtf\": SAN \"*.acme.wtf\" can not be a wildcard domain",
|
||||||
expectedDomains: nil,
|
expectedDomains: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -251,8 +267,8 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
Main: "*.foo.acme.wtf",
|
Main: "*.foo.acme.wtf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Main: "acme.wtf",
|
Main: "acme02.wtf",
|
||||||
SANs: []string{"traefik.acme.wtf", "bar.foo"},
|
SANs: []string{"traefik.acme02.wtf", "bar.foo"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedDomains: []types.Domain{
|
expectedDomains: []types.Domain{
|
||||||
|
@ -262,15 +278,38 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Main: "*.foo.acme.wtf",
|
Main: "*.foo.acme.wtf",
|
||||||
|
SANs: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Main: "acme.wtf",
|
Main: "acme02.wtf",
|
||||||
SANs: []string{"traefik.acme.wtf", "bar.foo"},
|
SANs: []string{"traefik.acme02.wtf", "bar.foo"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "2 domains with same values",
|
desc: "wildcard and root domain",
|
||||||
|
domains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "acme.wtf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{"acme.wtf"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedDomains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "acme.wtf",
|
||||||
|
SANs: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 equals domains",
|
||||||
domains: []types.Domain{
|
domains: []types.Domain{
|
||||||
{
|
{
|
||||||
Main: "acme.wtf",
|
Main: "acme.wtf",
|
||||||
|
@ -288,6 +327,29 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "2 domains with same values",
|
||||||
|
domains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "acme.wtf",
|
||||||
|
SANs: []string{"traefik.acme.wtf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Main: "acme.wtf",
|
||||||
|
SANs: []string{"traefik.acme.wtf", "foo.bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedDomains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "acme.wtf",
|
||||||
|
SANs: []string{"traefik.acme.wtf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Main: "foo.bar",
|
||||||
|
SANs: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "domain totally checked by wildcard",
|
desc: "domain totally checked by wildcard",
|
||||||
domains: []types.Domain{
|
domains: []types.Domain{
|
||||||
|
@ -302,6 +364,25 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
expectedDomains: []types.Domain{
|
expectedDomains: []types.Domain{
|
||||||
{
|
{
|
||||||
Main: "*.acme.wtf",
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "duplicated wildcard",
|
||||||
|
domains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{"acme.wtf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Main: "*.acme.wtf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedDomains: []types.Domain{
|
||||||
|
{
|
||||||
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{"acme.wtf"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -315,6 +396,10 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
{
|
{
|
||||||
Main: "*.acme.wtf",
|
Main: "*.acme.wtf",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Main: "who.acme.wtf",
|
||||||
|
SANs: []string{"traefik.acme.wtf", "bar.acme.wtf"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectedDomains: []types.Domain{
|
expectedDomains: []types.Domain{
|
||||||
{
|
{
|
||||||
|
@ -323,6 +408,7 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Main: "*.acme.wtf",
|
Main: "*.acme.wtf",
|
||||||
|
SANs: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue