diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go
index 1c4410681..f41b74ffd 100644
--- a/cmd/traefik/traefik.go
+++ b/cmd/traefik/traefik.go
@@ -20,6 +20,7 @@ import (
"github.com/containous/traefik/pkg/config/dynamic"
"github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/log"
+ "github.com/containous/traefik/pkg/provider/acme"
"github.com/containous/traefik/pkg/provider/aggregator"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/server"
@@ -88,7 +89,9 @@ func runCmd(staticConfiguration *static.Configuration) error {
}
staticConfiguration.SetEffectiveConfiguration()
- staticConfiguration.ValidateConfiguration()
+ if err := staticConfiguration.ValidateConfiguration(); err != nil {
+ return err
+ }
log.WithoutContext().Infof("Traefik version %s built on %s", version.Version, version.BuildDate)
@@ -112,15 +115,9 @@ func runCmd(staticConfiguration *static.Configuration) error {
providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers)
- acmeProvider, err := staticConfiguration.InitACMEProvider()
- if err != nil {
- log.WithoutContext().Errorf("Unable to initialize ACME provider: %v", err)
- } else if acmeProvider != nil {
- if err := providerAggregator.AddProvider(acmeProvider); err != nil {
- log.WithoutContext().Errorf("Unable to add ACME provider to the providers list: %v", err)
- acmeProvider = nil
- }
- }
+ tlsManager := traefiktls.NewManager()
+
+ acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
serverEntryPointsTCP := make(server.TCPEntryPoints)
for entryPointName, config := range staticConfiguration.EntryPoints {
@@ -129,27 +126,31 @@ func runCmd(staticConfiguration *static.Configuration) error {
if err != nil {
return fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err)
}
- serverEntryPointsTCP[entryPointName].RouteAppenderFactory = router.NewRouteAppenderFactory(*staticConfiguration, entryPointName, acmeProvider)
+ serverEntryPointsTCP[entryPointName].RouteAppenderFactory = router.NewRouteAppenderFactory(*staticConfiguration, entryPointName, acmeProviders)
}
- tlsManager := traefiktls.NewManager()
-
- if acmeProvider != nil {
- acmeProvider.SetTLSManager(tlsManager)
- if acmeProvider.TLSChallenge != nil &&
- acmeProvider.HTTPChallenge == nil &&
- acmeProvider.DNSChallenge == nil {
- tlsManager.TLSAlpnGetter = acmeProvider.GetTLSALPNCertificate
- }
- }
-
svr := server.NewServer(*staticConfiguration, providerAggregator, serverEntryPointsTCP, tlsManager)
- if acmeProvider != nil && acmeProvider.OnHostRule {
- acmeProvider.SetConfigListenerChan(make(chan dynamic.Configuration))
- svr.AddListener(acmeProvider.ListenConfiguration)
+ resolverNames := map[string]struct{}{}
+
+ for _, p := range acmeProviders {
+ resolverNames[p.ResolverName] = struct{}{}
+ svr.AddListener(p.ListenConfiguration)
}
+
+ svr.AddListener(func(config dynamic.Configuration) {
+ for rtName, rt := range config.HTTP.Routers {
+ if rt.TLS == nil || rt.TLS.CertResolver == "" {
+ continue
+ }
+
+ if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
+ log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
+ }
+ }
+ })
+
ctx := cmd.ContextWithSignal(context.Background())
if staticConfiguration.Ping != nil {
@@ -196,6 +197,40 @@ func runCmd(staticConfiguration *static.Configuration) error {
return nil
}
+// initACMEProvider creates an acme provider from the ACME part of globalConfiguration
+func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider {
+ challengeStore := acme.NewLocalChallengeStore()
+ localStores := map[string]*acme.LocalStore{}
+
+ var resolvers []*acme.Provider
+ for name, resolver := range c.CertificatesResolvers {
+ if resolver.ACME != nil {
+ if localStores[resolver.ACME.Storage] == nil {
+ localStores[resolver.ACME.Storage] = acme.NewLocalStore(resolver.ACME.Storage)
+ }
+
+ p := &acme.Provider{
+ Configuration: resolver.ACME,
+ Store: localStores[resolver.ACME.Storage],
+ ChallengeStore: challengeStore,
+ ResolverName: name,
+ }
+
+ if err := providerAggregator.AddProvider(p); err != nil {
+ log.WithoutContext().Errorf("Unable to add ACME provider to the providers list: %v", err)
+ continue
+ }
+ p.SetTLSManager(tlsManager)
+ if p.TLSChallenge != nil {
+ tlsManager.TLSAlpnGetter = p.GetTLSALPNCertificate
+ }
+ p.SetConfigListenerChan(make(chan dynamic.Configuration))
+ resolvers = append(resolvers, p)
+ }
+ }
+ return resolvers
+}
+
func configureLogging(staticConfiguration *static.Configuration) {
// configure default log flags
stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md
index 648304076..e60b6d759 100644
--- a/docs/content/https/acme.md
+++ b/docs/content/https/acme.md
@@ -11,8 +11,8 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom
## Configuration Examples
??? example "Enabling ACME"
-
- ```toml tab="TOML"
+
+ ```toml tab="File (TOML)"
[entryPoints]
[entryPoints.web]
address = ":80"
@@ -20,18 +20,15 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom
[entryPoints.web-secure]
address = ":443"
- # every router with TLS enabled will now be able to use ACME for its certificates
- [acme]
+ [certificatesResolvers.sample.acme]
email = "your-email@your-domain.org"
storage = "acme.json"
- # dynamic generation based on the Host() & HostSNI() matchers
- onHostRule = true
[acme.httpChallenge]
# used during the challenge
entryPoint = "web"
```
- ```yaml tab="YAML"
+ ```yaml tab="File (YAML)"
entryPoints:
web:
address: ":80"
@@ -39,50 +36,24 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom
web-secure:
address: ":443"
- # every router with TLS enabled will now be able to use ACME for its certificates
- acme:
- email: your-email@your-domain.org
- storage: acme.json
- # dynamic generation based on the Host() & HostSNI() matchers
- onHostRule: true
- httpChallenge:
- # used during the challenge
- entryPoint: web
- ```
-
-??? example "Configuring Wildcard Certificates"
-
- ```toml tab="TOML"
- [entryPoints]
- [entryPoints.web-secure]
- address = ":443"
-
- [acme]
- email = "your-email@your-domain.org"
- storage = "acme.json"
- [acme.dnsChallenge]
- provider = "xxx"
-
- [[acme.domains]]
- main = "*.mydomain.com"
- sans = ["mydomain.com"]
+ certificatesResolvers:
+ sample:
+ acme:
+ email: your-email@your-domain.org
+ storage: acme.json
+ httpChallenge:
+ # used during the challenge
+ entryPoint: web
```
- ```yaml tab="YAML"
- entryPoints:
- web-secure:
- address: ":443"
-
- acme:
- email: your-email@your-domain.org
- storage: acme.json
- dnsChallenge:
- provide: xxx
-
- domains:
- - main: "*.mydomain.com"
- sans:
- - mydomain.com
+ ```bash tab="CLI"
+ --entryPoints.web.address=":80"
+ --entryPoints.websecure.address=":443"
+ # ...
+ --certificatesResolvers.sample.acme.email: your-email@your-domain.org
+ --certificatesResolvers.sample.acme.storage: acme.json
+ # used during the challenge
+ --certificatesResolvers.sample.acme.httpChallenge.entryPoint: web
```
??? note "Configuration Reference"
@@ -90,13 +61,17 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom
There are many available options for ACME.
For a quick glance at what's possible, browse the configuration reference:
- ```toml tab="TOML"
+ ```toml tab="File (TOML)"
--8<-- "content/https/ref-acme.toml"
```
- ```yaml tab="YAML"
+ ```yaml tab="File (YAML)"
--8<-- "content/https/ref-acme.yaml"
```
+
+ ```bash tab="CLI"
+ --8<-- "content/https/ref-acme.txt"
+ ```
## Automatic Renewals
@@ -118,16 +93,25 @@ when using the `TLS-ALPN-01` challenge, Traefik must be reachable by Let's Encry
??? example "Configuring the `tlsChallenge`"
- ```toml tab="TOML"
- [acme]
- [acme.tlsChallenge]
+ ```toml tab="File (TOML)"
+ [certificatesResolvers.sample.acme]
+ # ...
+ [certificatesResolvers.sample.acme.tlsChallenge]
```
- ```yaml tab="YAML"
- acme:
- tlsChallenge: {}
+ ```yaml tab="File (YAML)"
+ certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ tlsChallenge: {}
```
+ ```bash tab="CLI"
+ # ...
+ --certificatesResolvers.sample.acme.tlsChallenge
+ ```
+
### `httpChallenge`
Use the `HTTP-01` challenge to generate and renew ACME certificates by provisioning an HTTP resource under a well-known URI.
@@ -137,7 +121,7 @@ when using the `HTTP-01` challenge, `acme.httpChallenge.entryPoint` must be reac
??? example "Using an EntryPoint Called http for the `httpChallenge`"
- ```toml tab="TOML"
+ ```toml tab="File (TOML)"
[entryPoints]
[entryPoints.web]
address = ":80"
@@ -145,13 +129,13 @@ when using the `HTTP-01` challenge, `acme.httpChallenge.entryPoint` must be reac
[entryPoints.web-secure]
address = ":443"
- [acme]
+ [certificatesResolvers.sample.acme]
# ...
- [acme.httpChallenge]
+ [certificatesResolvers.sample.acme.httpChallenge]
entryPoint = "web"
```
- ```yaml tab="YAML"
+ ```yaml tab="File (YAML)"
entryPoints:
web:
address: ":80"
@@ -159,10 +143,19 @@ when using the `HTTP-01` challenge, `acme.httpChallenge.entryPoint` must be reac
web-secure:
address: ":443"
- acme:
- # ...
- httpChallenge:
- entryPoint: web
+ certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ httpChallenge:
+ entryPoint: web
+ ```
+
+ ```bash tab="CLI"
+ --entryPoints.web.address=":80"
+ --entryPoints.websecure.address=":443"
+ # ...
+ --certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
```
!!! note
@@ -174,21 +167,30 @@ Use the `DNS-01` challenge to generate and renew ACME certificates by provisioni
??? example "Configuring a `dnsChallenge` with the DigitalOcean Provider"
- ```toml tab="TOML"
- [acme]
+ ```toml tab="File (TOML)"
+ [certificatesResolvers.sample.acme]
# ...
- [acme.dnsChallenge]
+ [certificatesResolvers.sample.acme.dnsChallenge]
provider = "digitalocean"
delayBeforeCheck = 0
# ...
```
- ```yaml tab="YAML"
- acme:
- # ...
- dnsChallenge:
- provider: digitalocean
- delayBeforeCheck: 0
+ ```yaml tab="File (YAML)"
+ certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ dnsChallenge:
+ provider: digitalocean
+ delayBeforeCheck: 0
+ # ...
+ ```
+
+ ```bash tab="CLI"
+ # ...
+ --certificatesResolvers.sample.acme.dnsChallenge.provider=digitalocean
+ --certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0
# ...
```
@@ -238,7 +240,7 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | [Additional configuration](https://go-acme.github.io/lego/dns/lightsail) |
| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/linode) |
| [Linode v4](https://www.linode.com) | `linodev4` | `LINODE_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/linodev4) |
-| manual | - | none, but you need to run Traefik interactively [^4], turn on `acmeLogging` to see instructions and press Enter. | |
+| manual | - | none, but you need to run Traefik interactively [^4], turn on debug log to see instructions and press Enter. | |
| [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mydnsjp) |
| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/namecheap) |
| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/namedotcom) |
@@ -276,22 +278,29 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
Use custom DNS servers to resolve the FQDN authority.
-```toml tab="TOML"
-[acme]
+```toml tab="File (TOML)"
+[certificatesResolvers.sample.acme]
# ...
- [acme.dnsChallenge]
+ [certificatesResolvers.sample.acme.dnsChallenge]
# ...
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
```
-```yaml tab="YAML"
-acme:
- # ...
- dnsChallenge:
- # ...
- resolvers:
- - "1.1.1.1:53"
- - "8.8.8.8:53"
+```yaml tab="File (YAML)"
+certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ dnsChallenge:
+ # ...
+ resolvers:
+ - "1.1.1.1:53"
+ - "8.8.8.8:53"
+```
+
+```bash tab="CLI"
+# ...
+--certificatesResolvers.sample.acme.dnsChallenge.resolvers:="1.1.1.1:53,8.8.8.8:53"
```
#### Wildcard Domains
@@ -299,140 +308,56 @@ acme:
[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) supports wildcard certificates.
As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605) wildcard certificates can only be generated through a [`DNS-01` challenge](#dnschallenge).
-```toml tab="TOML"
-[acme]
- # ...
- [[acme.domains]]
- main = "*.local1.com"
- sans = ["local1.com"]
-
-# ...
-```
-
-```yaml tab="YAML"
-acme:
- # ...
- domains:
- - main: "*.local1.com"
- sans:
- - local1.com
-
-# ...
-```
-
-!!! note "Double Wildcard Certificates"
- It is not possible to request a double wildcard certificate for a domain (for example `*.*.local.com`).
-
-Most likely the root domain should receive a certificate too, so it needs to be specified as SAN and 2 `DNS-01` challenges are executed.
-In this case the generated DNS TXT record for both domains is the same.
-Even though this behavior is [DNS RFC](https://community.letsencrypt.org/t/wildcard-issuance-two-txt-records-for-the-same-name/54528/2) compliant,
-it can lead to problems as all DNS providers keep DNS records cached for a given time (TTL) and this TTL can be greater than the challenge timeout making the `DNS-01` challenge fail.
-
-The Traefik ACME client library [LEGO](https://github.com/go-acme/lego) supports some but not all DNS providers to work around this issue.
-The [Supported `provider` table](#providers) indicates if they allow generating certificates for a wildcard domain and its root domain.
-
-## Known Domains, SANs
-
-You can set SANs (alternative domains) for each main domain.
-Every domain must have A/AAAA records pointing to Traefik.
-Each domain & SAN will lead to a certificate request.
-
-```toml tab="TOML"
-[acme]
- # ...
- [[acme.domains]]
- main = "local1.com"
- sans = ["test1.local1.com", "test2.local1.com"]
- [[acme.domains]]
- main = "local2.com"
- [[acme.domains]]
- main = "*.local3.com"
- sans = ["local3.com", "test1.test1.local3.com"]
-# ...
-```
-
-```yaml tab="YAML"
-acme:
- # ...
- domains:
- - main: "local1.com"
- sans:
- - "test1.local1.com"
- - "test2.local1.com"
- - main: "local2.com"
- - main: "*.local3.com"
- sans:
- - "local3.com"
- - "test1.test1.local3.com"
-# ...
-```
-
-!!! important
- The certificates for the domains listed in `acme.domains` are negotiated at Traefik startup only.
-
-!!! note
- Wildcard certificates can only be verified through a `DNS-01` challenge.
-
## `caServer`
??? example "Using the Let's Encrypt staging server"
- ```toml tab="TOML"
- [acme]
+ ```toml tab="File (TOML)"
+ [certificatesResolvers.sample.acme]
# ...
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
# ...
```
- ```yaml tab="YAML"
- acme:
- # ...
- caServer: https://acme-staging-v02.api.letsencrypt.org/directory
- # ...
+ ```yaml tab="File (YAML)"
+ certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ caServer: https://acme-staging-v02.api.letsencrypt.org/directory
+ # ...
```
-## `onHostRule`
-
-Enable certificate generation on [routers](../routing/routers/index.md) `Host` & `HostSNI` rules.
-
-This will request a certificate from Let's Encrypt for each router with a Host rule.
-
-```toml tab="TOML"
-[acme]
- # ...
- onHostRule = true
- # ...
-```
-
-```yaml tab="YAML"
-acme:
- # ...
- onHostRule: true
- # ...
-```
-
-!!! note "Multiple Hosts in a Rule"
- The rule `Host(test1.traefik.io,test2.traefik.io)` will request a certificate with the main domain `test1.traefik.io` and SAN `test2.traefik.io`.
-
-!!! warning
- `onHostRule` option can not be used to generate wildcard certificates. Refer to [wildcard generation](#wildcard-domains) for further information.
+ ```bash tab="CLI"
+ # ...
+ --certificatesResolvers.sample.acme.caServer="https://acme-staging-v02.api.letsencrypt.org/directory"
+ # ...
+ ```
## `storage`
The `storage` option sets the location where your ACME certificates are saved to.
-```toml tab="TOML"
-[acme]
+```toml tab="File (TOML)"
+[certificatesResolvers.sample.acme]
# ...
storage = "acme.json"
# ...
```
-```yaml tab="YAML"
-acme
- # ...
- storage: acme.json
- # ...
+```toml tab="File (TOML)"
+certificatesResolvers:
+ sample:
+ acme:
+ # ...
+ storage: acme.json
+ # ...
+```
+
+```bash tab="CLI"
+# ...
+--certificatesResolvers.sample.acme.storage=acme.json
+# ...
```
The value can refer to some kinds of storage:
diff --git a/docs/content/https/ref-acme.toml b/docs/content/https/ref-acme.toml
index b3a1fc031..7567470f9 100644
--- a/docs/content/https/ref-acme.toml
+++ b/docs/content/https/ref-acme.toml
@@ -1,123 +1,89 @@
# Enable ACME (Let's Encrypt): automatic SSL.
-[acme]
+[certificatesResolvers.sample.acme]
-# Email address used for registration.
-#
-# Required
-#
-email = "test@traefik.io"
-
-# File or key used for certificates storage.
-#
-# Required
-#
-storage = "acme.json"
-
-# If true, display debug log messages from the acme client library.
-#
-# Optional
-# Default: false
-#
-# acmeLogging = true
-
-# If true, override certificates in key-value store when using storeconfig.
-#
-# Optional
-# Default: false
-#
-# overrideCertificates = true
-
-# Enable certificate generation on routers host rules.
-#
-# Optional
-# Default: false
-#
-# onHostRule = true
-
-# CA server to use.
-# Uncomment the line to use Let's Encrypt's staging server,
-# leave commented to go to prod.
-#
-# Optional
-# Default: "https://acme-v02.api.letsencrypt.org/directory"
-#
-# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
-
-# KeyType to use.
-#
-# Optional
-# Default: "RSA4096"
-#
-# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
-#
-# KeyType = "RSA4096"
-
-# Use a TLS-ALPN-01 ACME challenge.
-#
-# Optional (but recommended)
-#
-[acme.tlsChallenge]
-
-# Use a HTTP-01 ACME challenge.
-#
-# Optional
-#
-# [acme.httpChallenge]
-
- # EntryPoint to use for the HTTP-01 challenges.
+ # Email address used for registration.
#
# Required
#
- # entryPoint = "web"
+ email = "test@traefik.io"
-# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
-# Note: mandatory for wildcard certificate generation.
-#
-# Optional
-#
-# [acme.dnsChallenge]
-
- # DNS provider used.
+ # File or key used for certificates storage.
#
# Required
#
- # provider = "digitalocean"
+ storage = "acme.json"
- # By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
- # If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
- # Useful if internal networks block external DNS queries.
+ # CA server to use.
+ # Uncomment the line to use Let's Encrypt's staging server,
+ # leave commented to go to prod.
#
# Optional
- # Default: 0
+ # Default: "https://acme-v02.api.letsencrypt.org/directory"
#
- # delayBeforeCheck = 0
+ # caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
- # Use following DNS servers to resolve the FQDN authority.
+ # KeyType to use.
#
# Optional
- # Default: empty
+ # Default: "RSA4096"
#
- # resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
+ # Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
+ #
+ # keyType = "RSA4096"
- # Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready.
+ # Use a TLS-ALPN-01 ACME challenge.
#
- # NOT RECOMMENDED:
- # Increase the risk of reaching Let's Encrypt's rate limits.
+ # Optional (but recommended)
+ #
+ [certificatesResolvers.sample.acme.tlsChallenge]
+
+ # Use a HTTP-01 ACME challenge.
#
# Optional
- # Default: false
#
- # disablePropagationCheck = true
+ # [certificatesResolvers.sample.acme.httpChallenge]
-# Domains list.
-# Only domains defined here can generate wildcard certificates.
-# The certificates for these domains are negotiated at traefik startup only.
-#
-# [[acme.domains]]
-# main = "local1.com"
-# sans = ["test1.local1.com", "test2.local1.com"]
-# [[acme.domains]]
-# main = "local2.com"
-# [[acme.domains]]
-# main = "*.local3.com"
-# sans = ["local3.com", "test1.test1.local3.com"]
\ No newline at end of file
+ # EntryPoint to use for the HTTP-01 challenges.
+ #
+ # Required
+ #
+ # entryPoint = "web"
+
+ # Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
+ # Note: mandatory for wildcard certificate generation.
+ #
+ # Optional
+ #
+ # [certificatesResolvers.sample.acme.dnsChallenge]
+
+ # DNS provider used.
+ #
+ # Required
+ #
+ # provider = "digitalocean"
+
+ # By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
+ # If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
+ # Useful if internal networks block external DNS queries.
+ #
+ # Optional
+ # Default: 0
+ #
+ # 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
diff --git a/docs/content/https/ref-acme.txt b/docs/content/https/ref-acme.txt
new file mode 100644
index 000000000..4e9fefc3a
--- /dev/null
+++ b/docs/content/https/ref-acme.txt
@@ -0,0 +1,89 @@
+# Enable ACME (Let's Encrypt): automatic SSL.
+--certificatesResolvers.sample.acme
+
+# Email address used for registration.
+#
+# Required
+#
+--certificatesResolvers.sample.acme.email="test@traefik.io"
+
+# File or key used for certificates storage.
+#
+# Required
+#
+--certificatesResolvers.sample.acme.storage="acme.json"
+
+# CA server to use.
+# Uncomment the line to use Let's Encrypt's staging server,
+# leave commented to go to prod.
+#
+# Optional
+# Default: "https://acme-v02.api.letsencrypt.org/directory"
+#
+--certificatesResolvers.sample.acme.caServer="https://acme-staging-v02.api.letsencrypt.org/directory"
+
+# KeyType to use.
+#
+# Optional
+# Default: "RSA4096"
+#
+# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
+#
+--certificatesResolvers.sample.acme.keyType=RSA4096
+
+# Use a TLS-ALPN-01 ACME challenge.
+#
+# Optional (but recommended)
+#
+--certificatesResolvers.sample.acme.tlsChallenge
+
+# Use a HTTP-01 ACME challenge.
+#
+# Optional
+#
+--certificatesResolvers.sample.acme.httpChallenge
+
+# EntryPoint to use for the HTTP-01 challenges.
+#
+# Required
+#
+--certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
+
+# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
+# Note: mandatory for wildcard certificate generation.
+#
+# Optional
+#
+--certificatesResolvers.sample.acme.dnsChallenge
+
+# DNS provider used.
+#
+# Required
+#
+--certificatesResolvers.sample.acme.dnsChallenge.provider=digitalocean
+
+# By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
+# If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
+# Useful if internal networks block external DNS queries.
+#
+# Optional
+# Default: 0
+#
+--certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0
+
+# Use following DNS servers to resolve the FQDN authority.
+#
+# Optional
+# Default: empty
+#
+--certificatesResolvers.sample.acme.dnsChallenge.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
+#
+--certificatesResolvers.sample.acme.dnsChallenge.disablePropagationCheck=true
diff --git a/docs/content/https/ref-acme.yaml b/docs/content/https/ref-acme.yaml
index 23cd9b7a6..b827e6f06 100644
--- a/docs/content/https/ref-acme.yaml
+++ b/docs/content/https/ref-acme.yaml
@@ -1,127 +1,93 @@
-# Enable ACME (Let's Encrypt): automatic SSL.
-acme:
+certificatesResolvers:
+ sample:
+ # Enable ACME (Let's Encrypt): automatic SSL.
+ acme:
- # Email address used for registration.
- #
- # Required
- #
- email: "test@traefik.io"
+ # Email address used for registration.
+ #
+ # Required
+ #
+ email: "test@traefik.io"
- # File or key used for certificates storage.
- #
- # Required
- #
- storage: "acme.json"
+ # File or key used for certificates storage.
+ #
+ # Required
+ #
+ storage: "acme.json"
- # If true, display debug log messages from the acme client library.
- #
- # Optional
- # Default: false
- #
- # acmeLogging: true
+ # CA server to use.
+ # Uncomment the line to use Let's Encrypt's staging server,
+ # leave commented to go to prod.
+ #
+ # Optional
+ # Default: "https://acme-v02.api.letsencrypt.org/directory"
+ #
+ # caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
- # If true, override certificates in key-value store when using storeconfig.
- #
- # Optional
- # Default: false
- #
- # overrideCertificates: true
+ # KeyType to use.
+ #
+ # Optional
+ # Default: "RSA4096"
+ #
+ # Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
+ #
+ # keyType: RSA4096
- # Enable certificate generation on routers host rules.
- #
- # Optional
- # Default: false
- #
- # onHostRule: true
+ # Use a TLS-ALPN-01 ACME challenge.
+ #
+ # Optional (but recommended)
+ #
+ tlsChallenge:
- # CA server to use.
- # Uncomment the line to use Let's Encrypt's staging server,
- # leave commented to go to prod.
- #
- # Optional
- # Default: "https://acme-v02.api.letsencrypt.org/directory"
- #
- # caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
+ # Use a HTTP-01 ACME challenge.
+ #
+ # Optional
+ #
+ # httpChallenge:
- # KeyType to use.
- #
- # Optional
- # Default: "RSA4096"
- #
- # Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
- #
- # KeyType: RSA4096
+ # EntryPoint to use for the HTTP-01 challenges.
+ #
+ # Required
+ #
+ # entryPoint: web
- # Use a TLS-ALPN-01 ACME challenge.
- #
- # Optional (but recommended)
- #
- tlsChallenge:
+ # Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
+ # Note: mandatory for wildcard certificate generation.
+ #
+ # Optional
+ #
+ # dnsChallenge:
- # Use a HTTP-01 ACME challenge.
- #
- # Optional
- #
- # httpChallenge:
+ # DNS provider used.
+ #
+ # Required
+ #
+ # provider: digitalocean
- # EntryPoint to use for the HTTP-01 challenges.
- #
- # Required
- #
- # entryPoint: web
+ # By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
+ # If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
+ # Useful if internal networks block external DNS queries.
+ #
+ # Optional
+ # Default: 0
+ #
+ # delayBeforeCheck: 0
- # Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
- # Note: mandatory for wildcard certificate generation.
- #
- # Optional
- #
- # dnsChallenge:
+ # Use following DNS servers to resolve the FQDN authority.
+ #
+ # Optional
+ # Default: empty
+ #
+ # resolvers
+ # - "1.1.1.1:53"
+ # - "8.8.8.8:53"
- # DNS provider used.
- #
- # Required
- #
- # provider: digitalocean
-
- # By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
- # If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
- # Useful if internal networks block external DNS queries.
- #
- # Optional
- # Default: 0
- #
- # 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.
- #
- # domains:
- # - main: "local1.com"
- # sans:
- # - "test1.local1.com"
- # - "test2.local1.com"
- # - main: "local2.com"
- # - main: "*.local3.com"
- # sans:
- # - "local3.com"
- # - "test1.test1.local3.com"
+ # 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
diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md
index e33c5e4ca..2e9069e2f 100644
--- a/docs/content/reference/static-configuration/cli-ref.md
+++ b/docs/content/reference/static-configuration/cli-ref.md
@@ -36,60 +36,6 @@ Keep access logs with status codes in the specified range.
`--accesslog.format`:
Access log format: json | common (Default: ```common```)
-`--acme.acmelogging`:
-Enable debug logging of ACME actions. (Default: ```false```)
-
-`--acme.caserver`:
-CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
-
-`--acme.dnschallenge`:
-Activate DNS-01 Challenge. (Default: ```false```)
-
-`--acme.dnschallenge.delaybeforecheck`:
-Assume DNS propagates after a delay in seconds rather than finding and querying nameservers. (Default: ```0```)
-
-`--acme.dnschallenge.disablepropagationcheck`:
-Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended] (Default: ```false```)
-
-`--acme.dnschallenge.provider`:
-Use a DNS-01 based challenge provider rather than HTTPS.
-
-`--acme.dnschallenge.resolvers`:
-Use following DNS servers to resolve the FQDN authority.
-
-`--acme.domains`:
-The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge.
-
-`--acme.domains[n].main`:
-Default subject name.
-
-`--acme.domains[n].sans`:
-Subject alternative names.
-
-`--acme.email`:
-Email address used for registration.
-
-`--acme.entrypoint`:
-EntryPoint to use.
-
-`--acme.httpchallenge`:
-Activate HTTP-01 Challenge. (Default: ```false```)
-
-`--acme.httpchallenge.entrypoint`:
-HTTP challenge EntryPoint
-
-`--acme.keytype`:
-KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. (Default: ```RSA4096```)
-
-`--acme.onhostrule`:
-Enable certificate generation on router Host rules. (Default: ```false```)
-
-`--acme.storage`:
-Storage to use. (Default: ```acme.json```)
-
-`--acme.tlschallenge`:
-Activate TLS-ALPN-01 Challenge. (Default: ```true```)
-
`--api`:
Enable api/dashboard. (Default: ```false```)
@@ -111,6 +57,45 @@ Enable more detailed statistics. (Default: ```false```)
`--api.statistics.recenterrors`:
Number of recent errors logged. (Default: ```10```)
+`--certificatesresolvers.`:
+Certificates resolvers configuration. (Default: ```false```)
+
+`--certificatesresolvers..acme.caserver`:
+CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
+
+`--certificatesresolvers..acme.dnschallenge`:
+Activate DNS-01 Challenge. (Default: ```false```)
+
+`--certificatesresolvers..acme.dnschallenge.delaybeforecheck`:
+Assume DNS propagates after a delay in seconds rather than finding and querying nameservers. (Default: ```0```)
+
+`--certificatesresolvers..acme.dnschallenge.disablepropagationcheck`:
+Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended] (Default: ```false```)
+
+`--certificatesresolvers..acme.dnschallenge.provider`:
+Use a DNS-01 based challenge provider rather than HTTPS.
+
+`--certificatesresolvers..acme.dnschallenge.resolvers`:
+Use following DNS servers to resolve the FQDN authority.
+
+`--certificatesresolvers..acme.email`:
+Email address used for registration.
+
+`--certificatesresolvers..acme.httpchallenge`:
+Activate HTTP-01 Challenge. (Default: ```false```)
+
+`--certificatesresolvers..acme.httpchallenge.entrypoint`:
+HTTP challenge EntryPoint
+
+`--certificatesresolvers..acme.keytype`:
+KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. (Default: ```RSA4096```)
+
+`--certificatesresolvers..acme.storage`:
+Storage to use. (Default: ```acme.json```)
+
+`--certificatesresolvers..acme.tlschallenge`:
+Activate TLS-ALPN-01 Challenge. (Default: ```true```)
+
`--entrypoints.`:
Entry points definition. (Default: ```false```)
diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md
index 52f921d1b..c1d8a8397 100644
--- a/docs/content/reference/static-configuration/env-ref.md
+++ b/docs/content/reference/static-configuration/env-ref.md
@@ -36,60 +36,6 @@ Keep access logs with status codes in the specified range.
`TRAEFIK_ACCESSLOG_FORMAT`:
Access log format: json | common (Default: ```common```)
-`TRAEFIK_ACME_ACMELOGGING`:
-Enable debug logging of ACME actions. (Default: ```false```)
-
-`TRAEFIK_ACME_CASERVER`:
-CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
-
-`TRAEFIK_ACME_DNSCHALLENGE`:
-Activate DNS-01 Challenge. (Default: ```false```)
-
-`TRAEFIK_ACME_DNSCHALLENGE_DELAYBEFORECHECK`:
-Assume DNS propagates after a delay in seconds rather than finding and querying nameservers. (Default: ```0```)
-
-`TRAEFIK_ACME_DNSCHALLENGE_DISABLEPROPAGATIONCHECK`:
-Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended] (Default: ```false```)
-
-`TRAEFIK_ACME_DNSCHALLENGE_PROVIDER`:
-Use a DNS-01 based challenge provider rather than HTTPS.
-
-`TRAEFIK_ACME_DNSCHALLENGE_RESOLVERS`:
-Use following DNS servers to resolve the FQDN authority.
-
-`TRAEFIK_ACME_DOMAINS`:
-The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge.
-
-`TRAEFIK_ACME_DOMAINS[n]_MAIN`:
-Default subject name.
-
-`TRAEFIK_ACME_DOMAINS[n]_SANS`:
-Subject alternative names.
-
-`TRAEFIK_ACME_EMAIL`:
-Email address used for registration.
-
-`TRAEFIK_ACME_ENTRYPOINT`:
-EntryPoint to use.
-
-`TRAEFIK_ACME_HTTPCHALLENGE`:
-Activate HTTP-01 Challenge. (Default: ```false```)
-
-`TRAEFIK_ACME_HTTPCHALLENGE_ENTRYPOINT`:
-HTTP challenge EntryPoint
-
-`TRAEFIK_ACME_KEYTYPE`:
-KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. (Default: ```RSA4096```)
-
-`TRAEFIK_ACME_ONHOSTRULE`:
-Enable certificate generation on router Host rules. (Default: ```false```)
-
-`TRAEFIK_ACME_STORAGE`:
-Storage to use. (Default: ```acme.json```)
-
-`TRAEFIK_ACME_TLSCHALLENGE`:
-Activate TLS-ALPN-01 Challenge. (Default: ```true```)
-
`TRAEFIK_API`:
Enable api/dashboard. (Default: ```false```)
@@ -111,6 +57,45 @@ Enable more detailed statistics. (Default: ```false```)
`TRAEFIK_API_STATISTICS_RECENTERRORS`:
Number of recent errors logged. (Default: ```10```)
+`TRAEFIK_CERTIFICATESRESOLVERS_`:
+Certificates resolvers configuration. (Default: ```false```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_CASERVER`:
+CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_DNSCHALLENGE`:
+Activate DNS-01 Challenge. (Default: ```false```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_DNSCHALLENGE_DELAYBEFORECHECK`:
+Assume DNS propagates after a delay in seconds rather than finding and querying nameservers. (Default: ```0```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_DNSCHALLENGE_DISABLEPROPAGATIONCHECK`:
+Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended] (Default: ```false```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_DNSCHALLENGE_PROVIDER`:
+Use a DNS-01 based challenge provider rather than HTTPS.
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_DNSCHALLENGE_RESOLVERS`:
+Use following DNS servers to resolve the FQDN authority.
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_EMAIL`:
+Email address used for registration.
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_HTTPCHALLENGE`:
+Activate HTTP-01 Challenge. (Default: ```false```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_HTTPCHALLENGE_ENTRYPOINT`:
+HTTP challenge EntryPoint
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_KEYTYPE`:
+KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. (Default: ```RSA4096```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_STORAGE`:
+Storage to use. (Default: ```acme.json```)
+
+`TRAEFIK_CERTIFICATESRESOLVERS__ACME_TLSCHALLENGE`:
+Activate TLS-ALPN-01 Challenge. (Default: ```true```)
+
`TRAEFIK_ENTRYPOINTS_`:
Entry points definition. (Default: ```false```)
diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml
index 4301d8d56..62f07e451 100644
--- a/docs/content/reference/static-configuration/file.toml
+++ b/docs/content/reference/static-configuration/file.toml
@@ -221,12 +221,10 @@
[acme]
email = "foobar"
- acmeLogging = true
caServer = "foobar"
storage = "foobar"
entryPoint = "foobar"
keyType = "foobar"
- onHostRule = true
[acme.dnsChallenge]
provider = "foobar"
delayBeforeCheck = 42
diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml
index 0f415c348..bfb44c68e 100644
--- a/docs/content/reference/static-configuration/file.yaml
+++ b/docs/content/reference/static-configuration/file.yaml
@@ -230,12 +230,10 @@ hostResolver:
resolvDepth: 42
acme:
email: foobar
- acmeLogging: true
caServer: foobar
storage: foobar
entryPoint: foobar
keyType: foobar
- onHostRule: true
dnsChallenge:
provider: foobar
delayBeforeCheck: 42
diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md
index 5a3d09987..2f23af18b 100644
--- a/docs/content/routing/routers/index.md
+++ b/docs/content/routing/routers/index.md
@@ -325,9 +325,9 @@ Traefik will terminate the SSL connections (meaning that it will send decrypted
service: service-id
```
-#### `Options`
+#### `options`
-The `Options` field enables fine-grained control of the TLS parameters.
+The `options` field enables fine-grained control of the TLS parameters.
It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied only if a `Host` rule is defined.
!!! note "Server Name Association"
@@ -384,13 +384,13 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied
[http.routers.routerfoo]
rule = "Host(`snitest.com`) && Path(`/foo`)"
[http.routers.routerfoo.tls]
- options="foo"
+ options = "foo"
[http.routers]
[http.routers.routerbar]
rule = "Host(`snitest.com`) && Path(`/bar`)"
[http.routers.routerbar.tls]
- options="bar"
+ options = "bar"
```
```yaml tab="YAML"
@@ -409,6 +409,76 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied
If that happens, both mappings are discarded, and the host name (`snitest.com` in this case) for these routers gets associated with the default TLS options instead.
+#### `certResolver`
+
+If `certResolver` is defined, Traefik will try to generate certificates based on routers `Host` & `HostSNI` rules.
+
+```toml tab="TOML"
+[http.routers]
+ [http.routers.routerfoo]
+ rule = "Host(`snitest.com`) && Path(`/foo`)"
+ [http.routers.routerfoo.tls]
+ certResolver = "foo"
+```
+
+```yaml tab="YAML"
+http:
+ routers:
+ routerfoo:
+ rule: "Host(`snitest.com`) && Path(`/foo`)"
+ tls:
+ certResolver: foo
+```
+
+!!! note "Multiple Hosts in a Rule"
+ The rule `Host(test1.traefik.io,test2.traefik.io)` will request a certificate with the main domain `test1.traefik.io` and SAN `test2.traefik.io`.
+
+#### `domains`
+
+You can set SANs (alternative domains) for each main domain.
+Every domain must have A/AAAA records pointing to Traefik.
+Each domain & SAN will lead to a certificate request.
+
+```toml tab="TOML"
+[http.routers]
+ [http.routers.routerbar]
+ rule = "Host(`snitest.com`) && Path(`/bar`)"
+ [http.routers.routerbar.tls]
+ certResolver = "bar"
+ [[http.routers.routerbar.tls.domains]]
+ main = "snitest.com"
+ sans = "*.snitest.com"
+```
+
+```yaml tab="YAML"
+http:
+ routers:
+ routerbar:
+ rule: "Host(`snitest.com`) && Path(`/bar`)"
+ tls:
+ certResolver: "bar"
+ domains:
+ - main: "snitest.com"
+ sans: "*.snitest.com"
+```
+
+[ACME v2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) supports wildcard certificates.
+As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605) wildcard certificates can only be generated through a [`DNS-01` challenge](./../../https/acme.md#dnschallenge).
+
+Most likely the root domain should receive a certificate too, so it needs to be specified as SAN and 2 `DNS-01` challenges are executed.
+In this case the generated DNS TXT record for both domains is the same.
+Even though this behavior is [DNS RFC](https://community.letsencrypt.org/t/wildcard-issuance-two-txt-records-for-the-same-name/54528/2) compliant,
+it can lead to problems as all DNS providers keep DNS records cached for a given time (TTL) and this TTL can be greater than the challenge timeout making the `DNS-01` challenge fail.
+
+The Traefik ACME client library [LEGO](https://github.com/go-acme/lego) supports some but not all DNS providers to work around this issue.
+The [Supported `provider` table](./../../https/acme.md#providers) indicates if they allow generating certificates for a wildcard domain and its root domain.
+
+!!! note
+ Wildcard certificates can only be verified through a `DNS-01` challenge.
+
+!!! note "Double Wildcard Certificates"
+ It is not possible to request a double wildcard certificate for a domain (for example `*.*.local.com`).
+
## Configuring TCP Routers
### General
@@ -593,9 +663,9 @@ Services are the target for the router.
In the current version, with [ACME](../../https/acme.md) enabled, automatic certificate generation will apply to every router declaring a TLS section.
-#### `Options`
+#### `options`
-The `Options` field enables fine-grained control of the TLS parameters.
+The `options` field enables fine-grained control of the TLS parameters.
It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied only if a `HostSNI` rule is defined.
??? example "Configuring the tls options"
@@ -636,3 +706,51 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied
- "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
- "TLS_RSA_WITH_AES_256_GCM_SHA384"
```
+
+#### `certResolver`
+
+See [`certResolver` for HTTP router](./index.md#certresolver) for more information.
+
+```toml tab="TOML"
+[tcp.routers]
+ [tcp.routers.routerfoo]
+ rule = "HostSNI(`snitest.com`)"
+ [tcp.routers.routerfoo.tls]
+ certResolver = "foo"
+```
+
+```yaml tab="YAML"
+tcp:
+ routers:
+ routerfoo:
+ rule: "HostSNI(`snitest.com`)"
+ tls:
+ certResolver: foo
+```
+
+#### `domains`
+
+See [`domains` for HTTP router](./index.md#domains) for more information.
+
+```toml tab="TOML"
+[tcp.routers]
+ [tcp.routers.routerbar]
+ rule = "HostSNI(`snitest.com`)"
+ [tcp.routers.routerbar.tls]
+ certResolver = "bar"
+ [[tcp.routers.routerbar.tls.domains]]
+ main = "snitest.com"
+ sans = "*.snitest.com"
+```
+
+```yaml tab="YAML"
+tcp:
+ routers:
+ routerbar:
+ rule: "HostSNI(`snitest.com`)"
+ tls:
+ certResolver: "bar"
+ domains:
+ - main: "snitest.com"
+ sans: "*.snitest.com"
+```
diff --git a/docs/content/user-guides/crd-acme/03-deployments.yml b/docs/content/user-guides/crd-acme/03-deployments.yml
index 822d2bf41..d3b436bcd 100644
--- a/docs/content/user-guides/crd-acme/03-deployments.yml
+++ b/docs/content/user-guides/crd-acme/03-deployments.yml
@@ -33,16 +33,13 @@ spec:
- --entrypoints.web.Address=:8000
- --entrypoints.websecure.Address=:4443
- --providers.kubernetescrd
- - --acme
- - --acme.acmelogging
- - --acme.tlschallenge
- - --acme.onhostrule
- - --acme.email=foo@you.com
- - --acme.entrypoint=websecure
- - --acme.storage=acme.json
+ - --certificatesresolvers.default.acme.tlschallenge
+ - --certificatesresolvers.default.acme.email=foo@you.com
+ - --certificatesresolvers.default.acme.entrypoint=websecure
+ - --certificatesresolvers.default.acme.storage=acme.json
# Please note that this is the staging Let's Encrypt server.
# Once you get things working, you should remove that whole line altogether.
- - --acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
+ - --certificatesresolvers.default.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
ports:
- name: web
containerPort: 8000
diff --git a/docs/content/user-guides/crd-acme/04-ingressroutes.yml b/docs/content/user-guides/crd-acme/04-ingressroutes.yml
index 04baed826..dae1bce30 100644
--- a/docs/content/user-guides/crd-acme/04-ingressroutes.yml
+++ b/docs/content/user-guides/crd-acme/04-ingressroutes.yml
@@ -26,5 +26,5 @@ spec:
services:
- name: whoami
port: 80
- # Please note the use of an empty TLS object to enable TLS with Let's Encrypt.
- tls: {}
+ tls:
+ certResolver: default
diff --git a/integration/acme_test.go b/integration/acme_test.go
index 6b72a95f8..23c774b6b 100644
--- a/integration/acme_test.go
+++ b/integration/acme_test.go
@@ -11,6 +11,7 @@ import (
"time"
"github.com/containous/traefik/integration/try"
+ "github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/provider/acme"
"github.com/containous/traefik/pkg/testhelpers"
"github.com/containous/traefik/pkg/types"
@@ -26,17 +27,23 @@ type AcmeSuite struct {
fakeDNSServer *dns.Server
}
+type subCases struct {
+ host string
+ expectedCommonName string
+ expectedAlgorithm x509.PublicKeyAlgorithm
+}
+
type acmeTestCase struct {
template templateModel
traefikConfFilePath string
- expectedCommonName string
- expectedAlgorithm x509.PublicKeyAlgorithm
+ subCases []subCases
}
type templateModel struct {
+ Domains []types.Domain
PortHTTP string
PortHTTPS string
- Acme acme.Configuration
+ Acme map[string]static.CertificateResolver
}
const (
@@ -120,40 +127,48 @@ func (s *AcmeSuite) TearDownSuite(c *check.C) {
}
}
-func (s *AcmeSuite) TestHTTP01DomainsAtStart(c *check.C) {
- c.Skip("We need to fix DefaultCertificate at start")
+func (s *AcmeSuite) TestHTTP01Domains(c *check.C) {
testCase := acmeTestCase{
- traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ traefikConfFilePath: "fixtures/acme/acme_domains.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- Domains: types.Domains{types.Domain{
- Main: "traefik.acme.wtf",
+ Domains: []types.Domain{{
+ Main: "traefik.acme.wtf",
+ }},
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
}},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
}
-func (s *AcmeSuite) TestHTTP01DomainsInSANAtStart(c *check.C) {
- c.Skip("We need to fix DefaultCertificate at start")
+func (s *AcmeSuite) TestHTTP01DomainsInSAN(c *check.C) {
testCase := acmeTestCase{
- traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ traefikConfFilePath: "fixtures/acme/acme_domains.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: "acme.wtf",
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- Domains: types.Domains{types.Domain{
- Main: "acme.wtf",
- SANs: []string{"traefik.acme.wtf"},
+ Domains: []types.Domain{{
+ Main: "acme.wtf",
+ SANs: []string{"traefik.acme.wtf"},
+ }},
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
}},
},
},
- expectedCommonName: "acme.wtf",
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -162,14 +177,49 @@ func (s *AcmeSuite) TestHTTP01DomainsInSANAtStart(c *check.C) {
func (s *AcmeSuite) TestHTTP01OnHostRule(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ }},
+ },
+ },
+ }
+
+ s.retrieveAcmeCertificate(c, testCase)
+}
+
+func (s *AcmeSuite) TestMultipleResolver(c *check.C) {
+ testCase := acmeTestCase{
+ traefikConfFilePath: "fixtures/acme/acme_multiple_resolvers.toml",
+ subCases: []subCases{
+ {
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ },
+ {
+ host: "tchouk.acme.wtf",
+ expectedCommonName: "tchouk.acme.wtf",
+ expectedAlgorithm: x509.ECDSA,
+ },
+ },
+ template: templateModel{
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ }},
+ "tchouk": {ACME: &acme.Configuration{
+ TLSChallenge: &acme.TLSChallenge{},
+ KeyType: "EC256",
+ }},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -178,15 +228,19 @@ func (s *AcmeSuite) TestHTTP01OnHostRule(c *check.C) {
func (s *AcmeSuite) TestHTTP01OnHostRuleECDSA(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.ECDSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
- KeyType: "EC384",
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ KeyType: "EC384",
+ }},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.ECDSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -195,31 +249,39 @@ func (s *AcmeSuite) TestHTTP01OnHostRuleECDSA(c *check.C) {
func (s *AcmeSuite) TestHTTP01OnHostRuleInvalidAlgo(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
- KeyType: "INVALID",
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ KeyType: "INVALID",
+ }},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
}
-func (s *AcmeSuite) TestHTTP01OnHostRuleStaticCertificatesWithWildcard(c *check.C) {
+func (s *AcmeSuite) TestHTTP01OnHostRuleDefaultDynamicCertificatesWithWildcard(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_tls.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: wildcardDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ }},
},
},
- expectedCommonName: wildcardDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -228,14 +290,38 @@ func (s *AcmeSuite) TestHTTP01OnHostRuleStaticCertificatesWithWildcard(c *check.
func (s *AcmeSuite) TestHTTP01OnHostRuleDynamicCertificatesWithWildcard(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_tls_dynamic.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: wildcardDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ }},
+ },
+ },
+ }
+
+ s.retrieveAcmeCertificate(c, testCase)
+}
+
+func (s *AcmeSuite) TestTLSALPN01OnHostRuleTCP(c *check.C) {
+ testCase := acmeTestCase{
+ traefikConfFilePath: "fixtures/acme/acme_tcp.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
+ template: templateModel{
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ TLSChallenge: &acme.TLSChallenge{},
+ }},
},
},
- expectedCommonName: wildcardDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -244,72 +330,65 @@ func (s *AcmeSuite) TestHTTP01OnHostRuleDynamicCertificatesWithWildcard(c *check
func (s *AcmeSuite) TestTLSALPN01OnHostRule(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- TLSChallenge: &acme.TLSChallenge{},
- OnHostRule: true,
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ TLSChallenge: &acme.TLSChallenge{},
+ }},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
}
-func (s *AcmeSuite) TestTLSALPN01DomainsAtStart(c *check.C) {
- c.Skip("We need to fix DefaultCertificate at start")
+func (s *AcmeSuite) TestTLSALPN01Domains(c *check.C) {
testCase := acmeTestCase{
- traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ traefikConfFilePath: "fixtures/acme/acme_domains.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: acmeDomain,
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- TLSChallenge: &acme.TLSChallenge{},
- Domains: types.Domains{types.Domain{
- Main: "traefik.acme.wtf",
+ Domains: []types.Domain{{
+ Main: "traefik.acme.wtf",
+ }},
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ TLSChallenge: &acme.TLSChallenge{},
}},
},
},
- expectedCommonName: acmeDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
}
-func (s *AcmeSuite) TestTLSALPN01DomainsInSANAtStart(c *check.C) {
- c.Skip("We need to fix DefaultCertificate at start")
+func (s *AcmeSuite) TestTLSALPN01DomainsInSAN(c *check.C) {
testCase := acmeTestCase{
- traefikConfFilePath: "fixtures/acme/acme_base.toml",
+ traefikConfFilePath: "fixtures/acme/acme_domains.toml",
+ subCases: []subCases{{
+ host: acmeDomain,
+ expectedCommonName: "acme.wtf",
+ expectedAlgorithm: x509.RSA,
+ }},
template: templateModel{
- Acme: acme.Configuration{
- TLSChallenge: &acme.TLSChallenge{},
- Domains: types.Domains{types.Domain{
- Main: "acme.wtf",
- SANs: []string{"traefik.acme.wtf"},
+ Domains: []types.Domain{{
+ Main: "acme.wtf",
+ SANs: []string{"traefik.acme.wtf"},
+ }},
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ TLSChallenge: &acme.TLSChallenge{},
}},
},
},
- expectedCommonName: "acme.wtf",
- expectedAlgorithm: x509.RSA,
- }
-
- s.retrieveAcmeCertificate(c, testCase)
-}
-
-func (s *AcmeSuite) TestTLSALPN01DomainsWithProvidedWildcardDomainAtStart(c *check.C) {
- c.Skip("We need to fix DefaultCertificate at start")
- testCase := acmeTestCase{
- traefikConfFilePath: "fixtures/acme/acme_tls.toml",
- template: templateModel{
- Acme: acme.Configuration{
- TLSChallenge: &acme.TLSChallenge{},
- Domains: types.Domains{types.Domain{
- Main: acmeDomain,
- }},
- },
- },
- expectedCommonName: wildcardDomain,
- expectedAlgorithm: x509.RSA,
}
s.retrieveAcmeCertificate(c, testCase)
@@ -318,10 +397,11 @@ func (s *AcmeSuite) TestTLSALPN01DomainsWithProvidedWildcardDomainAtStart(c *che
// Test Let's encrypt down
func (s *AcmeSuite) TestNoValidLetsEncryptServer(c *check.C) {
file := s.adaptFile(c, "fixtures/acme/acme_base.toml", templateModel{
- Acme: acme.Configuration{
- CAServer: "http://wrongurl:4001/directory",
- HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
- OnHostRule: true,
+ Acme: map[string]static.CertificateResolver{
+ "default": {ACME: &acme.Configuration{
+ CAServer: "http://wrongurl:4001/directory",
+ HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
+ }},
},
})
defer os.Remove(file)
@@ -347,8 +427,10 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) {
testCase.template.PortHTTPS = ":5001"
}
- if len(testCase.template.Acme.CAServer) == 0 {
- testCase.template.Acme.CAServer = s.getAcmeURL()
+ for _, value := range testCase.template.Acme {
+ if len(value.ACME.CAServer) == 0 {
+ value.ACME.CAServer = s.getAcmeURL()
+ }
}
file := s.adaptFile(c, testCase.traefikConfFilePath, testCase.template)
@@ -365,57 +447,59 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) {
backend := startTestServer("9010", http.StatusOK)
defer backend.Close()
- client := &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- },
- }
-
- // wait for traefik (generating acme account take some seconds)
- err = try.Do(90*time.Second, func() error {
- _, errGet := client.Get("https://127.0.0.1:5001")
- return errGet
- })
- c.Assert(err, checker.IsNil)
-
- client = &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- ServerName: acmeDomain,
+ for _, sub := range testCase.subCases {
+ client := &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
- },
+ }
+
+ // wait for traefik (generating acme account take some seconds)
+ err = try.Do(60*time.Second, func() error {
+ _, errGet := client.Get("https://127.0.0.1:5001")
+ return errGet
+ })
+ c.Assert(err, checker.IsNil)
+
+ client = &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ ServerName: sub.host,
+ },
+ },
+ }
+
+ req := testhelpers.MustNewRequest(http.MethodGet, "https://127.0.0.1:5001/", nil)
+ req.Host = sub.host
+ req.Header.Set("Host", sub.host)
+ req.Header.Set("Accept", "*/*")
+
+ var resp *http.Response
+
+ // Retry to send a Request which uses the LE generated certificate
+ err = try.Do(60*time.Second, func() error {
+ resp, err = client.Do(req)
+
+ // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\
+ req.Close = true
+
+ if err != nil {
+ return err
+ }
+
+ cn := resp.TLS.PeerCertificates[0].Subject.CommonName
+ if cn != sub.expectedCommonName {
+ return fmt.Errorf("domain %s found instead of %s", cn, sub.expectedCommonName)
+ }
+
+ return nil
+ })
+
+ c.Assert(err, checker.IsNil)
+ c.Assert(resp.StatusCode, checker.Equals, http.StatusOK)
+ // Check Domain into response certificate
+ c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, sub.expectedCommonName)
+ c.Assert(resp.TLS.PeerCertificates[0].PublicKeyAlgorithm, checker.Equals, sub.expectedAlgorithm)
}
-
- req := testhelpers.MustNewRequest(http.MethodGet, "https://127.0.0.1:5001/", nil)
- req.Host = acmeDomain
- req.Header.Set("Host", acmeDomain)
- req.Header.Set("Accept", "*/*")
-
- var resp *http.Response
-
- // Retry to send a Request which uses the LE generated certificate
- err = try.Do(60*time.Second, func() error {
- resp, err = client.Do(req)
-
- // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\
- req.Close = true
-
- if err != nil {
- return err
- }
-
- cn := resp.TLS.PeerCertificates[0].Subject.CommonName
- if cn != testCase.expectedCommonName {
- return fmt.Errorf("domain %s found instead of %s", cn, testCase.expectedCommonName)
- }
-
- return nil
- })
-
- c.Assert(err, checker.IsNil)
- c.Assert(resp.StatusCode, checker.Equals, http.StatusOK)
- // Check Domain into response certificate
- c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.expectedCommonName)
- c.Assert(resp.TLS.PeerCertificates[0].PublicKeyAlgorithm, checker.Equals, testCase.expectedAlgorithm)
}
diff --git a/integration/fixtures/acme/acme_base.toml b/integration/fixtures/acme/acme_base.toml
index ee2755545..ae8b2be83 100644
--- a/integration/fixtures/acme/acme_base.toml
+++ b/integration/fixtures/acme/acme_base.toml
@@ -11,31 +11,24 @@
[entryPoints.web-secure]
address = "{{ .PortHTTPS }}"
-[acme]
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
- # entryPoint = "https"
- acmeLogging = true
- onHostRule = {{ .Acme.OnHostRule }}
- keyType = "{{ .Acme.KeyType }}"
- caServer = "{{ .Acme.CAServer }}"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
- {{if .Acme.HTTPChallenge }}
- [acme.httpChallenge]
- entryPoint = "{{ .Acme.HTTPChallenge.EntryPoint }}"
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
{{end}}
- {{if .Acme.TLSChallenge }}
- [acme.tlsChallenge]
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
{{end}}
- {{range .Acme.Domains}}
- [[acme.domains]]
- main = "{{ .Main }}"
- sans = [{{range .SANs }}
- "{{.}}",
- {{end}}]
- {{end}}
+{{end}}
[api]
@@ -55,3 +48,4 @@
rule = "Host(`traefik.acme.wtf`)"
service = "test"
[http.routers.test.tls]
+ certResolver = "default"
diff --git a/integration/fixtures/acme/acme_domains.toml b/integration/fixtures/acme/acme_domains.toml
new file mode 100644
index 000000000..72f047acf
--- /dev/null
+++ b/integration/fixtures/acme/acme_domains.toml
@@ -0,0 +1,58 @@
+[global]
+ checkNewVersion = false
+ sendAnonymousUsage = false
+
+[log]
+ level = "DEBUG"
+
+[entryPoints]
+ [entryPoints.web]
+ address = "{{ .PortHTTP }}"
+ [entryPoints.web-secure]
+ address = "{{ .PortHTTPS }}"
+
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
+ email = "test@traefik.io"
+ storage = "/tmp/acme.json"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
+
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
+ {{end}}
+
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
+ {{end}}
+
+{{end}}
+
+[api]
+
+[providers.file]
+ filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[http.services]
+ [http.services.test.loadBalancer]
+ [[http.services.test.loadBalancer.servers]]
+ url = "http://127.0.0.1:9010"
+
+[http.routers]
+ [http.routers.test]
+ entryPoints = ["web-secure"]
+ rule = "PathPrefix(`/`)"
+ service = "test"
+ [http.routers.test.tls]
+ certResolver = "default"
+{{range .Domains}}
+ [[http.routers.test.tls.domains]]
+ main = "{{ .Main }}"
+ sans = [{{range .SANs }}
+ "{{.}}",
+ {{end}}]
+{{end}}
diff --git a/integration/fixtures/acme/acme_multiple_resolvers.toml b/integration/fixtures/acme/acme_multiple_resolvers.toml
new file mode 100644
index 000000000..73313d3f3
--- /dev/null
+++ b/integration/fixtures/acme/acme_multiple_resolvers.toml
@@ -0,0 +1,58 @@
+[global]
+ checkNewVersion = false
+ sendAnonymousUsage = false
+
+[log]
+ level = "DEBUG"
+
+[entryPoints]
+ [entryPoints.web]
+ address = "{{ .PortHTTP }}"
+ [entryPoints.web-secure]
+ address = "{{ .PortHTTPS }}"
+
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
+ email = "test@traefik.io"
+ storage = "/tmp/acme.json"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
+
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
+ {{end}}
+
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
+ {{end}}
+
+{{end}}
+
+[api]
+
+[providers.file]
+ filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[http.services]
+ [http.services.test.loadBalancer]
+ [[http.services.test.loadBalancer.servers]]
+ url = "http://127.0.0.1:9010"
+
+[http.routers]
+ [http.routers.test]
+ entryPoints = ["web-secure"]
+ rule = "Host(`traefik.acme.wtf`)"
+ service = "test"
+ [http.routers.test.tls]
+ certResolver = "default"
+
+ [http.routers.tchouk]
+ entryPoints = ["web-secure"]
+ rule = "Host(`tchouk.acme.wtf`)"
+ service = "test"
+ [http.routers.tchouk.tls]
+ certResolver = "tchouk"
diff --git a/integration/fixtures/acme/acme_tcp.toml b/integration/fixtures/acme/acme_tcp.toml
new file mode 100644
index 000000000..c016a4139
--- /dev/null
+++ b/integration/fixtures/acme/acme_tcp.toml
@@ -0,0 +1,51 @@
+[global]
+ checkNewVersion = false
+ sendAnonymousUsage = false
+
+[log]
+ level = "DEBUG"
+
+[entryPoints]
+ [entryPoints.web]
+ address = "{{ .PortHTTP }}"
+ [entryPoints.web-secure]
+ address = "{{ .PortHTTPS }}"
+
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
+ email = "test@traefik.io"
+ storage = "/tmp/acme.json"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
+
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
+ {{end}}
+
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
+ {{end}}
+
+{{end}}
+
+[api]
+
+[providers.file]
+ filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[tcp.services]
+ [tcp.services.test.loadBalancer]
+ [[tcp.services.test.loadBalancer.servers]]
+ address = "127.0.0.1:9010"
+
+[tcp.routers]
+ [tcp.routers.test]
+ entryPoints = ["web-secure"]
+ rule = "HostSNI(`traefik.acme.wtf`)"
+ service = "test"
+ [tcp.routers.test.tls]
+ certResolver = "default"
diff --git a/integration/fixtures/acme/acme_tls.toml b/integration/fixtures/acme/acme_tls.toml
index 7addd0731..2319974bd 100644
--- a/integration/fixtures/acme/acme_tls.toml
+++ b/integration/fixtures/acme/acme_tls.toml
@@ -11,31 +11,24 @@
[entryPoints.web-secure]
address = "{{ .PortHTTPS }}"
-[acme]
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
-# entryPoint = "https"
- acmeLogging = true
- onHostRule = {{ .Acme.OnHostRule }}
- keyType = "{{ .Acme.KeyType }}"
- caServer = "{{ .Acme.CAServer }}"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
- {{if .Acme.HTTPChallenge }}
- [acme.httpChallenge]
- entryPoint = "{{ .Acme.HTTPChallenge.EntryPoint }}"
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
{{end}}
- {{if .Acme.TLSChallenge }}
- [acme.tlsChallenge]
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
{{end}}
- {{range .Acme.Domains}}
- [[acme.domains]]
- main = "{{ .Main }}"
- sans = [{{range .SANs }}
- "{{.}}",
- {{end}}]
- {{end}}
+{{end}}
[api]
diff --git a/integration/fixtures/acme/acme_tls_dynamic.toml b/integration/fixtures/acme/acme_tls_dynamic.toml
index a538796ba..eac99adc1 100644
--- a/integration/fixtures/acme/acme_tls_dynamic.toml
+++ b/integration/fixtures/acme/acme_tls_dynamic.toml
@@ -7,32 +7,29 @@
[entryPoints]
[entryPoints.web]
- address = "{{ .PortHTTP }}"
+ address = "{{ .PortHTTP }}"
[entryPoints.web-secure]
- address = "{{ .PortHTTPS }}"
+ address = "{{ .PortHTTPS }}"
-[acme]
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
-# entryPoint = "https"
- acmeLogging = true
- onHostRule = {{ .Acme.OnHostRule }}
- keyType = "{{ .Acme.KeyType }}"
- caServer = "{{ .Acme.CAServer }}"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
- {{if .Acme.HTTPChallenge }}
- [acme.httpChallenge]
- entryPoint = "{{ .Acme.HTTPChallenge.EntryPoint }}"
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
{{end}}
- {{range .Acme.Domains}}
- [[acme.domains]]
- main = "{{ .Main }}"
- sans = [{{range .SANs }}
- "{{.}}",
- {{end}}]
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
{{end}}
+{{end}}
+
[api]
[providers]
diff --git a/integration/fixtures/acme/acme_tls_multiple_entrypoints.toml b/integration/fixtures/acme/acme_tls_multiple_entrypoints.toml
index 757414ee4..f4601b695 100644
--- a/integration/fixtures/acme/acme_tls_multiple_entrypoints.toml
+++ b/integration/fixtures/acme/acme_tls_multiple_entrypoints.toml
@@ -8,42 +8,29 @@
[entryPoints]
[entryPoints.web]
address = "{{ .PortHTTP }}"
-
[entryPoints.web-secure]
address = "{{ .PortHTTPS }}"
[entryPoints.traefik]
address = ":9000"
-# FIXME
-# [entryPoints.traefik.tls]
-# [entryPoints.traefik.tls.defaultCertificate]
-# certFile = "fixtures/acme/ssl/wildcard.crt"
-# keyFile = "fixtures/acme/ssl/wildcard.key"
-[acme]
+{{range $name, $resolvers := .Acme }}
+
+[certificatesResolvers.{{ $name }}.acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
-# entryPoint = "https"
- acmeLogging = true
- onHostRule = {{ .Acme.OnHostRule }}
- keyType = "{{ .Acme.KeyType }}"
- caServer = "{{ .Acme.CAServer }}"
+ keyType = "{{ $resolvers.ACME.KeyType }}"
+ caServer = "{{ $resolvers.ACME.CAServer }}"
- {{if .Acme.HTTPChallenge }}
- [acme.httpChallenge]
- entryPoint = "{{ .Acme.HTTPChallenge.EntryPoint }}"
+ {{if $resolvers.ACME.HTTPChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.httpChallenge]
+ entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
{{end}}
- {{if .Acme.TLSChallenge }}
- [acme.tlsChallenge]
+ {{if $resolvers.ACME.TLSChallenge }}
+ [certificatesResolvers.{{ $name }}.acme.tlsChallenge]
{{end}}
- {{range .Acme.Domains}}
- [[acme.domains]]
- main = "{{ .Main }}"
- sans = [{{range .SANs }}
- "{{.}}",
- {{end}}]
- {{end}}
+{{end}}
[api]
diff --git a/integration/https_test.go b/integration/https_test.go
index 07c74f229..5da57cd61 100644
--- a/integration/https_test.go
+++ b/integration/https_test.go
@@ -265,7 +265,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) {
c.Assert(err.Error(), checker.Contains, "protocol version not supported")
// with unknown tls option
- err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS option instead", tr4.TLSClientConfig.ServerName)))
+ err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS options instead", tr4.TLSClientConfig.ServerName)))
c.Assert(err, checker.IsNil)
}
diff --git a/integration/simple_test.go b/integration/simple_test.go
index b7309d809..985e76eb5 100644
--- a/integration/simple_test.go
+++ b/integration/simple_test.go
@@ -537,7 +537,7 @@ func (s *SimpleSuite) TestRouterConfigErrors(c *check.C) {
defer cmd.Process.Kill()
// All errors
- err = try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS option instead"]`))
+ err = try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS options instead"]`))
c.Assert(err, checker.IsNil)
// router4 is enabled, but in warning state because its tls options conf was messed up
diff --git a/pkg/anonymize/anonymize_config_test.go b/pkg/anonymize/anonymize_config_test.go
index 14b61dbd7..823217c70 100644
--- a/pkg/anonymize/anonymize_config_test.go
+++ b/pkg/anonymize/anonymize_config_test.go
@@ -89,23 +89,18 @@ func TestDo_globalConfiguration(t *testing.T) {
},
},
}
- config.ACME = &acme.Configuration{
- Email: "acme Email",
- ACMELogging: true,
- CAServer: "CAServer",
- Storage: "Storage",
- EntryPoint: "EntryPoint",
- KeyType: "MyKeyType",
- OnHostRule: true,
- DNSChallenge: &acmeprovider.DNSChallenge{Provider: "DNSProvider"},
- HTTPChallenge: &acmeprovider.HTTPChallenge{
- EntryPoint: "MyEntryPoint",
- },
- TLSChallenge: &acmeprovider.TLSChallenge{},
- Domains: []types.Domain{
- {
- Main: "Domains Main",
- SANs: []string{"Domains acme SANs 1", "Domains acme SANs 2", "Domains acme SANs 3"},
+ config.CertificatesResolvers = map[string]static.CertificateResolver{
+ "default": {
+ ACME: &acme.Configuration{
+ Email: "acme Email",
+ CAServer: "CAServer",
+ Storage: "Storage",
+ KeyType: "MyKeyType",
+ DNSChallenge: &acmeprovider.DNSChallenge{Provider: "DNSProvider"},
+ HTTPChallenge: &acmeprovider.HTTPChallenge{
+ EntryPoint: "MyEntryPoint",
+ },
+ TLSChallenge: &acmeprovider.TLSChallenge{},
},
},
}
@@ -126,9 +121,6 @@ func TestDo_globalConfiguration(t *testing.T) {
config.API = &static.API{
EntryPoint: "traefik",
Dashboard: true,
- Statistics: &types.Statistics{
- RecentErrors: 111,
- },
DashboardAssets: &assetfs.AssetFS{
Asset: func(path string) ([]byte, error) {
return nil, nil
diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml
index 89e9672f7..e177d32bf 100644
--- a/pkg/config/dynamic/fixtures/sample.toml
+++ b/pkg/config/dynamic/fixtures/sample.toml
@@ -212,7 +212,6 @@
storage = "foobar"
entryPoint = "foobar"
keyType = "foobar"
- onHostRule = true
[acme.dnsChallenge]
provider = "foobar"
delayBeforeCheck = 42
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index f58bd0753..5356a5503 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -1,6 +1,10 @@
package dynamic
-import "reflect"
+import (
+ "reflect"
+
+ "github.com/containous/traefik/pkg/types"
+)
// +k8s:deepcopy-gen=true
@@ -34,7 +38,9 @@ type Router struct {
// RouterTLSConfig holds the TLS configuration for a router
type RouterTLSConfig struct {
- Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
+ Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
+ CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty"`
+ Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go
index be2ca9b1b..e72c2fd76 100644
--- a/pkg/config/dynamic/tcp_config.go
+++ b/pkg/config/dynamic/tcp_config.go
@@ -1,6 +1,10 @@
package dynamic
-import "reflect"
+import (
+ "reflect"
+
+ "github.com/containous/traefik/pkg/types"
+)
// +k8s:deepcopy-gen=true
@@ -31,8 +35,10 @@ type TCPRouter struct {
// RouterTCPTLSConfig holds the TLS configuration for a router
type RouterTCPTLSConfig struct {
- Passthrough bool `json:"passthrough" toml:"passthrough" yaml:"passthrough"`
- Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
+ Passthrough bool `json:"passthrough" toml:"passthrough" yaml:"passthrough"`
+ Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
+ CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty"`
+ Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go
index 416865453..46b64d071 100644
--- a/pkg/config/dynamic/zz_generated.deepcopy.go
+++ b/pkg/config/dynamic/zz_generated.deepcopy.go
@@ -30,6 +30,7 @@ package dynamic
import (
tls "github.com/containous/traefik/pkg/tls"
+ types "github.com/containous/traefik/pkg/types"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@@ -876,7 +877,7 @@ func (in *Router) DeepCopyInto(out *Router) {
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(RouterTLSConfig)
- **out = **in
+ (*in).DeepCopyInto(*out)
}
return
}
@@ -894,6 +895,13 @@ func (in *Router) DeepCopy() *Router {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterTCPTLSConfig) DeepCopyInto(out *RouterTCPTLSConfig) {
*out = *in
+ if in.Domains != nil {
+ in, out := &in.Domains, &out.Domains
+ *out = make([]types.Domain, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
@@ -910,6 +918,13 @@ func (in *RouterTCPTLSConfig) DeepCopy() *RouterTCPTLSConfig {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterTLSConfig) DeepCopyInto(out *RouterTLSConfig) {
*out = *in
+ if in.Domains != nil {
+ in, out := &in.Domains, &out.Domains
+ *out = make([]types.Domain, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
@@ -1096,7 +1111,7 @@ func (in *TCPRouter) DeepCopyInto(out *TCPRouter) {
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(RouterTCPTLSConfig)
- **out = **in
+ (*in).DeepCopyInto(*out)
}
return
}
diff --git a/pkg/config/file/file_node_test.go b/pkg/config/file/file_node_test.go
index 086af94c1..affb8513b 100644
--- a/pkg/config/file/file_node_test.go
+++ b/pkg/config/file/file_node_test.go
@@ -44,7 +44,7 @@ func Test_getRootFieldNames(t *testing.T) {
func Test_decodeFileToNode_compare(t *testing.T) {
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
- "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
+ "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
@@ -59,7 +59,7 @@ func Test_decodeFileToNode_compare(t *testing.T) {
func Test_decodeFileToNode_Toml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.toml",
- "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
+ "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
@@ -85,42 +85,35 @@ func Test_decodeFileToNode_Toml(t *testing.T) {
{Name: "retryAttempts", Value: "true"},
{Name: "statusCodes", Value: "foobar,foobar"}}},
{Name: "format", Value: "foobar"}}},
- {Name: "acme",
- Children: []*parser.Node{
- {Name: "acmeLogging", Value: "true"},
- {Name: "caServer", Value: "foobar"},
- {Name: "dnsChallenge", Children: []*parser.Node{
- {Name: "delayBeforeCheck", Value: "42"},
- {Name: "disablePropagationCheck", Value: "true"},
- {Name: "provider", Value: "foobar"},
- {Name: "resolvers", Value: "foobar,foobar"},
- }},
- {Name: "domains", Children: []*parser.Node{
- {Name: "[0]", Children: []*parser.Node{
- {Name: "main", Value: "foobar"},
- {Name: "sans", Value: "foobar,foobar"},
- }},
- {Name: "[1]", Children: []*parser.Node{
- {Name: "main", Value: "foobar"},
- {Name: "sans", Value: "foobar,foobar"},
- }},
- }},
- {Name: "email", Value: "foobar"},
- {Name: "entryPoint", Value: "foobar"},
- {Name: "httpChallenge", Children: []*parser.Node{
- {Name: "entryPoint", Value: "foobar"}}},
- {Name: "keyType", Value: "foobar"},
- {Name: "onHostRule", Value: "true"},
- {Name: "storage", Value: "foobar"},
- {Name: "tlsChallenge"},
- },
- },
{Name: "api", Children: []*parser.Node{
{Name: "dashboard", Value: "true"},
{Name: "entryPoint", Value: "foobar"},
{Name: "middlewares", Value: "foobar,foobar"},
{Name: "statistics", Children: []*parser.Node{
{Name: "recentErrors", Value: "42"}}}}},
+ {Name: "certificatesResolvers", Children: []*parser.Node{
+ {Name: "default", Children: []*parser.Node{
+ {Name: "acme",
+ Children: []*parser.Node{
+ {Name: "acmeLogging", Value: "true"},
+ {Name: "caServer", Value: "foobar"},
+ {Name: "dnsChallenge", Children: []*parser.Node{
+ {Name: "delayBeforeCheck", Value: "42"},
+ {Name: "disablePropagationCheck", Value: "true"},
+ {Name: "provider", Value: "foobar"},
+ {Name: "resolvers", Value: "foobar,foobar"},
+ }},
+ {Name: "email", Value: "foobar"},
+ {Name: "entryPoint", Value: "foobar"},
+ {Name: "httpChallenge", Children: []*parser.Node{
+ {Name: "entryPoint", Value: "foobar"}}},
+ {Name: "keyType", Value: "foobar"},
+ {Name: "storage", Value: "foobar"},
+ {Name: "tlsChallenge"},
+ },
+ },
+ }},
+ }},
{Name: "entryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "address", Value: "foobar"},
@@ -327,42 +320,35 @@ func Test_decodeFileToNode_Yaml(t *testing.T) {
{Name: "retryAttempts", Value: "true"},
{Name: "statusCodes", Value: "foobar,foobar"}}},
{Name: "format", Value: "foobar"}}},
- {Name: "acme",
- Children: []*parser.Node{
- {Name: "acmeLogging", Value: "true"},
- {Name: "caServer", Value: "foobar"},
- {Name: "dnsChallenge", Children: []*parser.Node{
- {Name: "delayBeforeCheck", Value: "42"},
- {Name: "disablePropagationCheck", Value: "true"},
- {Name: "provider", Value: "foobar"},
- {Name: "resolvers", Value: "foobar,foobar"},
- }},
- {Name: "domains", Children: []*parser.Node{
- {Name: "[0]", Children: []*parser.Node{
- {Name: "main", Value: "foobar"},
- {Name: "sans", Value: "foobar,foobar"},
- }},
- {Name: "[1]", Children: []*parser.Node{
- {Name: "main", Value: "foobar"},
- {Name: "sans", Value: "foobar,foobar"},
- }},
- }},
- {Name: "email", Value: "foobar"},
- {Name: "entryPoint", Value: "foobar"},
- {Name: "httpChallenge", Children: []*parser.Node{
- {Name: "entryPoint", Value: "foobar"}}},
- {Name: "keyType", Value: "foobar"},
- {Name: "onHostRule", Value: "true"},
- {Name: "storage", Value: "foobar"},
- {Name: "tlsChallenge"},
- },
- },
{Name: "api", Children: []*parser.Node{
{Name: "dashboard", Value: "true"},
{Name: "entryPoint", Value: "foobar"},
{Name: "middlewares", Value: "foobar,foobar"},
{Name: "statistics", Children: []*parser.Node{
{Name: "recentErrors", Value: "42"}}}}},
+ {Name: "certificatesResolvers", Children: []*parser.Node{
+ {Name: "default", Children: []*parser.Node{
+ {Name: "acme",
+ Children: []*parser.Node{
+ {Name: "acmeLogging", Value: "true"},
+ {Name: "caServer", Value: "foobar"},
+ {Name: "dnsChallenge", Children: []*parser.Node{
+ {Name: "delayBeforeCheck", Value: "42"},
+ {Name: "disablePropagationCheck", Value: "true"},
+ {Name: "provider", Value: "foobar"},
+ {Name: "resolvers", Value: "foobar,foobar"},
+ }},
+ {Name: "email", Value: "foobar"},
+ {Name: "entryPoint", Value: "foobar"},
+ {Name: "httpChallenge", Children: []*parser.Node{
+ {Name: "entryPoint", Value: "foobar"}}},
+ {Name: "keyType", Value: "foobar"},
+ {Name: "storage", Value: "foobar"},
+ {Name: "tlsChallenge"},
+ },
+ },
+ }},
+ }},
{Name: "entryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "address", Value: "foobar"},
diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml
index e964e12ca..2ecf138d2 100644
--- a/pkg/config/file/fixtures/sample.toml
+++ b/pkg/config/file/fixtures/sample.toml
@@ -205,30 +205,21 @@
resolvConfig = "foobar"
resolvDepth = 42
-[acme]
+[certificatesResolvers.default.acme]
email = "foobar"
acmeLogging = true
caServer = "foobar"
storage = "foobar"
entryPoint = "foobar"
keyType = "foobar"
- onHostRule = true
- [acme.dnsChallenge]
+ [certificatesResolvers.default.acme.dnsChallenge]
provider = "foobar"
delayBeforeCheck = 42
resolvers = ["foobar", "foobar"]
disablePropagationCheck = true
- [acme.httpChallenge]
+ [certificatesResolvers.default.acme.httpChallenge]
entryPoint = "foobar"
- [acme.tlsChallenge]
-
- [[acme.domains]]
- main = "foobar"
- sans = ["foobar", "foobar"]
-
- [[acme.domains]]
- main = "foobar"
- sans = ["foobar", "foobar"]
+ [certificatesResolvers.default.acme.tlsChallenge]
## Dynamic configuration
diff --git a/pkg/config/file/fixtures/sample.yml b/pkg/config/file/fixtures/sample.yml
index 40a8c55da..1cbd3e724 100644
--- a/pkg/config/file/fixtures/sample.yml
+++ b/pkg/config/file/fixtures/sample.yml
@@ -214,30 +214,23 @@ hostResolver:
cnameFlattening: true
resolvConfig: foobar
resolvDepth: 42
-acme:
- email: foobar
- acmeLogging: true
- caServer: foobar
- storage: foobar
- entryPoint: foobar
- keyType: foobar
- onHostRule: true
- dnsChallenge:
- provider: foobar
- delayBeforeCheck: 42
- resolvers:
- - foobar
- - foobar
- disablePropagationCheck: true
- httpChallenge:
- entryPoint: foobar
- tlsChallenge: {}
- domains:
- - main: foobar
- sans:
- - foobar
- - foobar
- - main: foobar
- sans:
- - foobar
- - foobar
+
+certificatesResolvers:
+ default:
+ acme:
+ email: foobar
+ acmeLogging: true
+ caServer: foobar
+ storage: foobar
+ entryPoint: foobar
+ keyType: foobar
+ dnsChallenge:
+ provider: foobar
+ delayBeforeCheck: 42
+ resolvers:
+ - foobar
+ - foobar
+ disablePropagationCheck: true
+ httpChallenge:
+ entryPoint: foobar
+ tlsChallenge: {}
diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go
index e284e1337..e297e422e 100644
--- a/pkg/config/static/static_config.go
+++ b/pkg/config/static/static_config.go
@@ -1,7 +1,8 @@
package static
import (
- "errors"
+ "fmt"
+ stdlog "log"
"strings"
"time"
@@ -23,7 +24,8 @@ import (
"github.com/containous/traefik/pkg/tracing/zipkin"
"github.com/containous/traefik/pkg/types"
assetfs "github.com/elazarl/go-bindata-assetfs"
- "github.com/go-acme/lego/challenge/dns01"
+ legolog "github.com/go-acme/lego/log"
+ "github.com/sirupsen/logrus"
)
const (
@@ -59,6 +61,11 @@ type Configuration struct {
HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." json:"hostResolver,omitempty" toml:"hostResolver,omitempty" yaml:"hostResolver,omitempty" label:"allowEmpty" export:"true"`
+ CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
+}
+
+// CertificateResolver contains the configuration for the different types of certificates resolver.
+type CertificateResolver struct {
ACME *acmeprovider.Configuration `description:"Enable ACME (Let's Encrypt): automatic SSL." json:"acme,omitempty" toml:"acme,omitempty" yaml:"acme,omitempty" export:"true"`
}
@@ -194,64 +201,35 @@ func (c *Configuration) SetEffectiveConfiguration() {
c.initACMEProvider()
}
-// FIXME handle on new configuration ACME struct
func (c *Configuration) initACMEProvider() {
- if c.ACME != nil {
- c.ACME.CAServer = getSafeACMECAServer(c.ACME.CAServer)
-
- if c.ACME.DNSChallenge != nil && c.ACME.HTTPChallenge != nil {
- log.Warn("Unable to use DNS challenge and HTTP challenge at the same time. Fallback to DNS challenge.")
- c.ACME.HTTPChallenge = nil
- }
-
- if c.ACME.DNSChallenge != nil && c.ACME.TLSChallenge != nil {
- log.Warn("Unable to use DNS challenge and TLS challenge at the same time. Fallback to DNS challenge.")
- c.ACME.TLSChallenge = nil
- }
-
- if c.ACME.HTTPChallenge != nil && c.ACME.TLSChallenge != nil {
- log.Warn("Unable to use HTTP challenge and TLS challenge at the same time. Fallback to TLS challenge.")
- c.ACME.HTTPChallenge = nil
+ for _, resolver := range c.CertificatesResolvers {
+ if resolver.ACME != nil {
+ resolver.ACME.CAServer = getSafeACMECAServer(resolver.ACME.CAServer)
}
}
-}
-// InitACMEProvider create an acme provider from the ACME part of globalConfiguration
-func (c *Configuration) InitACMEProvider() (*acmeprovider.Provider, error) {
- if c.ACME != nil {
- if len(c.ACME.Storage) == 0 {
- return nil, errors.New("unable to initialize ACME provider with no storage location for the certificates")
- }
- return &acmeprovider.Provider{
- Configuration: c.ACME,
- }, nil
- }
- return nil, nil
+ legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
}
// ValidateConfiguration validate that configuration is coherent
-func (c *Configuration) ValidateConfiguration() {
- if c.ACME != nil {
- for _, domain := range c.ACME.Domains {
- if domain.Main != dns01.UnFqdn(domain.Main) {
- log.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
- }
- for _, san := range domain.SANs {
- if san != dns01.UnFqdn(san) {
- log.Warnf("FQDN detected, please remove the trailing dot: %s", san)
- }
- }
+func (c *Configuration) ValidateConfiguration() error {
+ var acmeEmail string
+ for name, resolver := range c.CertificatesResolvers {
+ if resolver.ACME == nil {
+ continue
}
+
+ if len(resolver.ACME.Storage) == 0 {
+ return fmt.Errorf("unable to initialize certificates resolver %q with no storage location for the certificates", name)
+ }
+
+ if acmeEmail != "" && resolver.ACME.Email != acmeEmail {
+ return fmt.Errorf("unable to initialize certificates resolver %q, all the acme resolvers must use the same email", name)
+ }
+ acmeEmail = resolver.ACME.Email
}
- // FIXME Validate store config?
- // if c.ACME != nil {
- // if _, ok := c.EntryPoints[c.ACME.EntryPoint]; !ok {
- // log.Fatalf("Unknown entrypoint %q for ACME configuration", c.ACME.EntryPoint)
- // }
- // else if c.EntryPoints[c.ACME.EntryPoint].TLS == nil {
- // log.Fatalf("Entrypoint %q has no TLS configuration for ACME configuration", c.ACME.EntryPoint)
- // }
- // }
+
+ return nil
}
func getSafeACMECAServer(caServerSrc string) string {
diff --git a/pkg/provider/acme/challenge_http.go b/pkg/provider/acme/challenge_http.go
index 755662fd7..669e3473a 100644
--- a/pkg/provider/acme/challenge_http.go
+++ b/pkg/provider/acme/challenge_http.go
@@ -17,7 +17,7 @@ import (
var _ challenge.ProviderTimeout = (*challengeHTTP)(nil)
type challengeHTTP struct {
- Store Store
+ Store ChallengeStore
}
// Present presents a challenge to obtain new ACME certificate.
@@ -52,7 +52,7 @@ func (p *Provider) Append(router *mux.Router) {
domain = req.Host
}
- tokenValue := getTokenValue(ctx, token, domain, p.Store)
+ tokenValue := getTokenValue(ctx, token, domain, p.ChallengeStore)
if len(tokenValue) > 0 {
rw.WriteHeader(http.StatusOK)
_, err = rw.Write(tokenValue)
@@ -66,7 +66,7 @@ func (p *Provider) Append(router *mux.Router) {
}))
}
-func getTokenValue(ctx context.Context, token, domain string, store Store) []byte {
+func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte {
logger := log.FromContext(ctx)
logger.Debugf("Retrieving the ACME challenge for token %v...", token)
diff --git a/pkg/provider/acme/challenge_tls.go b/pkg/provider/acme/challenge_tls.go
index c0697df89..196d2d256 100644
--- a/pkg/provider/acme/challenge_tls.go
+++ b/pkg/provider/acme/challenge_tls.go
@@ -12,7 +12,7 @@ import (
var _ challenge.Provider = (*challengeTLSALPN)(nil)
type challengeTLSALPN struct {
- Store Store
+ Store ChallengeStore
}
func (c *challengeTLSALPN) Present(domain, token, keyAuth string) error {
@@ -37,7 +37,7 @@ func (c *challengeTLSALPN) CleanUp(domain, token, keyAuth string) error {
// GetTLSALPNCertificate Get the temp certificate for ACME TLS-ALPN-O1 challenge.
func (p *Provider) GetTLSALPNCertificate(domain string) (*tls.Certificate, error) {
- cert, err := p.Store.GetTLSChallenge(domain)
+ cert, err := p.ChallengeStore.GetTLSChallenge(domain)
if err != nil {
return nil, err
}
diff --git a/pkg/provider/acme/local_store.go b/pkg/provider/acme/local_store.go
index fa5c00b13..717f30c2c 100644
--- a/pkg/provider/acme/local_store.go
+++ b/pkg/provider/acme/local_store.go
@@ -5,7 +5,6 @@ import (
"fmt"
"io/ioutil"
"os"
- "regexp"
"sync"
"github.com/containous/traefik/pkg/log"
@@ -16,25 +15,34 @@ var _ Store = (*LocalStore)(nil)
// LocalStore Stores implementation for local file
type LocalStore struct {
+ saveDataChan chan map[string]*StoredData
filename string
- storedData *StoredData
- SaveDataChan chan *StoredData `json:"-"`
- lock sync.RWMutex
+
+ lock sync.RWMutex
+ storedData map[string]*StoredData
}
// NewLocalStore initializes a new LocalStore with a file name
func NewLocalStore(filename string) *LocalStore {
- store := &LocalStore{filename: filename, SaveDataChan: make(chan *StoredData)}
+ store := &LocalStore{filename: filename, saveDataChan: make(chan map[string]*StoredData)}
store.listenSaveAction()
return store
}
-func (s *LocalStore) get() (*StoredData, error) {
+func (s *LocalStore) save(resolverName string, storedData *StoredData) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.storedData[resolverName] = storedData
+ s.saveDataChan <- s.storedData
+}
+
+func (s *LocalStore) get(resolverName string) (*StoredData, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
if s.storedData == nil {
- s.storedData = &StoredData{
- HTTPChallenges: make(map[string]map[string][]byte),
- TLSChallenges: make(map[string]*Certificate),
- }
+ s.storedData = map[string]*StoredData{}
hasData, err := CheckFile(s.filename)
if err != nil {
@@ -56,49 +64,40 @@ func (s *LocalStore) get() (*StoredData, error) {
}
if len(file) > 0 {
- if err := json.Unmarshal(file, s.storedData); err != nil {
+ if err := json.Unmarshal(file, &s.storedData); err != nil {
return nil, err
}
}
- // Check if ACME Account is in ACME V1 format
- if s.storedData.Account != nil && s.storedData.Account.Registration != nil {
- isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI)
- if err != nil {
- return nil, err
- }
- if isOldRegistration {
- logger.Debug("Reseting ACME account.")
- s.storedData.Account = nil
- s.SaveDataChan <- s.storedData
- }
- }
-
// Delete all certificates with no value
- var certificates []*Certificate
- for _, certificate := range s.storedData.Certificates {
- if len(certificate.Certificate) == 0 || len(certificate.Key) == 0 {
- logger.Debugf("Deleting empty certificate %v for %v", certificate, certificate.Domain.ToStrArray())
- continue
+ var certificates []*CertAndStore
+ for _, storedData := range s.storedData {
+ for _, certificate := range storedData.Certificates {
+ if len(certificate.Certificate.Certificate) == 0 || len(certificate.Key) == 0 {
+ logger.Debugf("Deleting empty certificate %v for %v", certificate, certificate.Domain.ToStrArray())
+ continue
+ }
+ certificates = append(certificates, certificate)
+ }
+ if len(certificates) < len(storedData.Certificates) {
+ storedData.Certificates = certificates
+ s.saveDataChan <- s.storedData
}
- certificates = append(certificates, certificate)
- }
-
- if len(certificates) < len(s.storedData.Certificates) {
- s.storedData.Certificates = certificates
- s.SaveDataChan <- s.storedData
}
}
}
- return s.storedData, nil
+ if s.storedData[resolverName] == nil {
+ s.storedData[resolverName] = &StoredData{}
+ }
+ return s.storedData[resolverName], nil
}
// listenSaveAction listens to a chan to store ACME data in json format into LocalStore.filename
func (s *LocalStore) listenSaveAction() {
safe.Go(func() {
logger := log.WithoutContext().WithField(log.ProviderName, "acme")
- for object := range s.SaveDataChan {
+ for object := range s.saveDataChan {
data, err := json.MarshalIndent(object, "", " ")
if err != nil {
logger.Error(err)
@@ -113,8 +112,8 @@ func (s *LocalStore) listenSaveAction() {
}
// GetAccount returns ACME Account
-func (s *LocalStore) GetAccount() (*Account, error) {
- storedData, err := s.get()
+func (s *LocalStore) GetAccount(resolverName string) (*Account, error) {
+ storedData, err := s.get(resolverName)
if err != nil {
return nil, err
}
@@ -123,21 +122,21 @@ func (s *LocalStore) GetAccount() (*Account, error) {
}
// SaveAccount stores ACME Account
-func (s *LocalStore) SaveAccount(account *Account) error {
- storedData, err := s.get()
+func (s *LocalStore) SaveAccount(resolverName string, account *Account) error {
+ storedData, err := s.get(resolverName)
if err != nil {
return err
}
storedData.Account = account
- s.SaveDataChan <- storedData
+ s.save(resolverName, storedData)
return nil
}
// GetCertificates returns ACME Certificates list
-func (s *LocalStore) GetCertificates() ([]*Certificate, error) {
- storedData, err := s.get()
+func (s *LocalStore) GetCertificates(resolverName string) ([]*CertAndStore, error) {
+ storedData, err := s.get(resolverName)
if err != nil {
return nil, err
}
@@ -146,20 +145,37 @@ func (s *LocalStore) GetCertificates() ([]*Certificate, error) {
}
// SaveCertificates stores ACME Certificates list
-func (s *LocalStore) SaveCertificates(certificates []*Certificate) error {
- storedData, err := s.get()
+func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertAndStore) error {
+ storedData, err := s.get(resolverName)
if err != nil {
return err
}
storedData.Certificates = certificates
- s.SaveDataChan <- storedData
+ s.save(resolverName, storedData)
return nil
}
+// LocalChallengeStore is an implementation of the ChallengeStore in memory.
+type LocalChallengeStore struct {
+ storedData *StoredChallengeData
+ lock sync.RWMutex
+}
+
+// NewLocalChallengeStore initializes a new LocalChallengeStore.
+func NewLocalChallengeStore() *LocalChallengeStore {
+ return &LocalChallengeStore{
+ storedData: &StoredChallengeData{
+ HTTPChallenges: make(map[string]map[string][]byte),
+ TLSChallenges: make(map[string]*Certificate),
+ },
+ }
+
+}
+
// GetHTTPChallengeToken Get the http challenge token from the store
-func (s *LocalStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) {
+func (s *LocalChallengeStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@@ -179,7 +195,7 @@ func (s *LocalStore) GetHTTPChallengeToken(token, domain string) ([]byte, error)
}
// SetHTTPChallengeToken Set the http challenge token in the store
-func (s *LocalStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error {
+func (s *LocalChallengeStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error {
s.lock.Lock()
defer s.lock.Unlock()
@@ -196,7 +212,7 @@ func (s *LocalStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte)
}
// RemoveHTTPChallengeToken Remove the http challenge token in the store
-func (s *LocalStore) RemoveHTTPChallengeToken(token, domain string) error {
+func (s *LocalChallengeStore) RemoveHTTPChallengeToken(token, domain string) error {
s.lock.Lock()
defer s.lock.Unlock()
@@ -214,7 +230,7 @@ func (s *LocalStore) RemoveHTTPChallengeToken(token, domain string) error {
}
// AddTLSChallenge Add a certificate to the ACME TLS-ALPN-01 certificates storage
-func (s *LocalStore) AddTLSChallenge(domain string, cert *Certificate) error {
+func (s *LocalChallengeStore) AddTLSChallenge(domain string, cert *Certificate) error {
s.lock.Lock()
defer s.lock.Unlock()
@@ -227,7 +243,7 @@ func (s *LocalStore) AddTLSChallenge(domain string, cert *Certificate) error {
}
// GetTLSChallenge Get a certificate from the ACME TLS-ALPN-01 certificates storage
-func (s *LocalStore) GetTLSChallenge(domain string) (*Certificate, error) {
+func (s *LocalChallengeStore) GetTLSChallenge(domain string) (*Certificate, error) {
s.lock.Lock()
defer s.lock.Unlock()
@@ -239,7 +255,7 @@ func (s *LocalStore) GetTLSChallenge(domain string) (*Certificate, error) {
}
// RemoveTLSChallenge Remove a certificate from the ACME TLS-ALPN-01 certificates storage
-func (s *LocalStore) RemoveTLSChallenge(domain string) error {
+func (s *LocalChallengeStore) RemoveTLSChallenge(domain string) error {
s.lock.Lock()
defer s.lock.Unlock()
diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go
index 9d6130481..2fc2678bc 100644
--- a/pkg/provider/acme/provider.go
+++ b/pkg/provider/acme/provider.go
@@ -6,8 +6,6 @@ import (
"crypto/x509"
"errors"
"fmt"
- "io/ioutil"
- fmtlog "log"
"net/url"
"reflect"
"strings"
@@ -25,10 +23,8 @@ import (
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/lego"
- legolog "github.com/go-acme/lego/log"
"github.com/go-acme/lego/providers/dns"
"github.com/go-acme/lego/registration"
- "github.com/sirupsen/logrus"
)
var (
@@ -39,16 +35,12 @@ var (
// Configuration holds ACME configuration provided by users
type Configuration struct {
Email string `description:"Email address used for registration." json:"email,omitempty" toml:"email,omitempty" yaml:"email,omitempty"`
- ACMELogging bool `description:"Enable debug logging of ACME actions." json:"acmeLogging,omitempty" toml:"acmeLogging,omitempty" yaml:"acmeLogging,omitempty"`
CAServer string `description:"CA server to use." json:"caServer,omitempty" toml:"caServer,omitempty" yaml:"caServer,omitempty"`
Storage string `description:"Storage to use." json:"storage,omitempty" toml:"storage,omitempty" yaml:"storage,omitempty"`
- EntryPoint string `description:"EntryPoint to use." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'." json:"keyType,omitempty" toml:"keyType,omitempty" yaml:"keyType,omitempty"`
- OnHostRule bool `description:"Enable certificate generation on router Host rules." json:"onHostRule,omitempty" toml:"onHostRule,omitempty" yaml:"onHostRule,omitempty"`
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty"`
TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty"`
- Domains []types.Domain `description:"The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge." json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// SetDefaults sets the default values.
@@ -58,6 +50,12 @@ func (a *Configuration) SetDefaults() {
a.KeyType = "RSA4096"
}
+// CertAndStore allows mapping a TLS certificate to a TLS store.
+type CertAndStore struct {
+ Certificate
+ Store string
+}
+
// Certificate is a struct which contains all data needed from an ACME certificate
type Certificate struct {
Domain types.Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty"`
@@ -84,11 +82,13 @@ type TLSChallenge struct{}
// Provider holds configurations of the provider.
type Provider struct {
*Configuration
+ ResolverName string
Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"`
- certificates []*Certificate
+ ChallengeStore ChallengeStore
+ certificates []*CertAndStore
account *Account
client *lego.Client
- certsChan chan *Certificate
+ certsChan chan *CertAndStore
configurationChan chan<- dynamic.Message
tlsManager *traefiktls.Manager
clientMutex sync.Mutex
@@ -113,41 +113,20 @@ func (p *Provider) ListenConfiguration(config dynamic.Configuration) {
p.configFromListenerChan <- config
}
-// ListenRequest resolves new certificates for a domain from an incoming request and return a valid Certificate to serve (onDemand option)
-func (p *Provider) ListenRequest(domain string) (*tls.Certificate, error) {
- ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
-
- acmeCert, err := p.resolveCertificate(ctx, types.Domain{Main: domain}, false)
- if acmeCert == nil || err != nil {
- return nil, err
- }
-
- cert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
-
- return &cert, err
-}
-
// Init for compatibility reason the BaseProvider implements an empty Init
func (p *Provider) Init() error {
ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
logger := log.FromContext(ctx)
- if p.ACMELogging {
- legolog.Logger = fmtlog.New(logger.WriterLevel(logrus.InfoLevel), "legolog: ", 0)
- } else {
- legolog.Logger = fmtlog.New(ioutil.Discard, "", 0)
- }
-
if len(p.Configuration.Storage) == 0 {
return errors.New("unable to initialize ACME provider with no storage location for the certificates")
}
- p.Store = NewLocalStore(p.Configuration.Storage)
var err error
- p.account, err = p.Store.GetAccount()
+ p.account, err = p.Store.GetAccount(p.ResolverName)
if err != nil {
- return fmt.Errorf("unable to get ACME account : %v", err)
+ return fmt.Errorf("unable to get ACME account: %v", err)
}
// Reset Account if caServer changed, thus registration URI can be updated
@@ -156,7 +135,7 @@ func (p *Provider) Init() error {
p.account = nil
}
- p.certificates, err = p.Store.GetCertificates()
+ p.certificates, err = p.Store.GetCertificates(p.ResolverName)
if err != nil {
return fmt.Errorf("unable to get ACME certificates : %v", err)
}
@@ -188,7 +167,7 @@ func isAccountMatchingCaServer(ctx context.Context, accountURI string, serverURI
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
- ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
+ ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme."+p.ResolverName))
p.pool = pool
@@ -198,17 +177,6 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
p.configurationChan = configurationChan
p.refreshCertificates()
- p.deleteUnnecessaryDomains(ctx)
- for i := 0; i < len(p.Domains); i++ {
- domain := p.Domains[i]
- safe.Go(func() {
- if _, err := p.resolveCertificate(ctx, domain, true); err != nil {
- log.WithoutContext().WithField(log.ProviderName, "acme").
- Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
- }
- })
- }
-
p.renewCertificates(ctx)
ticker := time.NewTicker(24 * time.Hour)
@@ -275,13 +243,18 @@ func (p *Provider) getClient() (*lego.Client, error) {
// Save the account once before all the certificates generation/storing
// No certificate can be generated if account is not initialized
- err = p.Store.SaveAccount(account)
+ err = p.Store.SaveAccount(p.ResolverName, account)
if err != nil {
return nil, err
}
- switch {
- case p.DNSChallenge != nil && len(p.DNSChallenge.Provider) > 0:
+ if (p.DNSChallenge == nil || len(p.DNSChallenge.Provider) == 0) &&
+ (p.HTTPChallenge == nil || len(p.HTTPChallenge.EntryPoint) == 0) &&
+ p.TLSChallenge == nil {
+ return nil, errors.New("ACME challenge not specified, please select TLS or HTTP or DNS Challenge")
+ }
+
+ if p.DNSChallenge != nil && len(p.DNSChallenge.Provider) > 0 {
logger.Debugf("Using DNS Challenge provider: %s", p.DNSChallenge.Provider)
var provider challenge.Provider
@@ -304,25 +277,24 @@ func (p *Provider) getClient() (*lego.Client, error) {
if err != nil {
return nil, err
}
+ }
- case p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0:
+ if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
logger.Debug("Using HTTP Challenge provider.")
- err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.Store})
+ err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.ChallengeStore})
if err != nil {
return nil, err
}
+ }
- case p.TLSChallenge != nil:
+ if p.TLSChallenge != nil {
logger.Debug("Using TLS Challenge provider.")
- err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.Store})
+ err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.ChallengeStore})
if err != nil {
return nil, err
}
-
- default:
- return nil, errors.New("ACME challenge not specified, please select TLS or HTTP or DNS Challenge")
}
p.client = client
@@ -346,7 +318,7 @@ func (p *Provider) initAccount(ctx context.Context) (*Account, error) {
return p.account, nil
}
-func (p *Provider) resolveDomains(ctx context.Context, domains []string) {
+func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
if len(domains) == 0 {
log.FromContext(ctx).Debug("No domain parsed in provider ACME")
return
@@ -362,7 +334,7 @@ func (p *Provider) resolveDomains(ctx context.Context, domains []string) {
}
safe.Go(func() {
- if _, err := p.resolveCertificate(ctx, domain, false); err != nil {
+ if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
log.FromContext(ctx).Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
}
})
@@ -376,32 +348,72 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
case config := <-p.configFromListenerChan:
if config.TCP != nil {
for routerName, route := range config.TCP.Routers {
- if route.TLS == nil {
+ if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
- domains, err := rules.ParseHostSNI(route.Rule)
- if err != nil {
- log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
- continue
+ tlsStore := "default"
+ if len(route.TLS.Domains) > 0 {
+ for _, domain := range route.TLS.Domains {
+ if domain.Main != dns01.UnFqdn(domain.Main) {
+ log.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
+ }
+ for _, san := range domain.SANs {
+ if san != dns01.UnFqdn(san) {
+ log.Warnf("FQDN detected, please remove the trailing dot: %s", san)
+ }
+ }
+ }
+
+ domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
+ for i := 0; i < len(domains); i++ {
+ domain := domains[i]
+ safe.Go(func() {
+ if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
+ log.WithoutContext().WithField(log.ProviderName, "acme."+p.ResolverName).
+ Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
+ }
+ })
+ }
+ } else {
+ domains, err := rules.ParseHostSNI(route.Rule)
+ if err != nil {
+ log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
+ continue
+ }
+ p.resolveDomains(ctxRouter, domains, tlsStore)
}
- p.resolveDomains(ctxRouter, domains)
}
}
for routerName, route := range config.HTTP.Routers {
- if route.TLS == nil {
+ if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
- domains, err := rules.ParseDomains(route.Rule)
- if err != nil {
- log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
- continue
+ tlsStore := "default"
+ if len(route.TLS.Domains) > 0 {
+ domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
+ for i := 0; i < len(domains); i++ {
+ domain := domains[i]
+ safe.Go(func() {
+ if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
+ log.WithoutContext().WithField(log.ProviderName, "acme."+p.ResolverName).
+ Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
+ }
+ })
+ }
+ } else {
+ domains, err := rules.ParseDomains(route.Rule)
+ if err != nil {
+ log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
+ continue
+ }
+ p.resolveDomains(ctxRouter, domains, tlsStore)
}
- p.resolveDomains(ctxRouter, domains)
+
}
case <-stop:
return
@@ -410,14 +422,14 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
-func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, domainFromConfigurationFile bool) (*certificate.Resource, error) {
- domains, err := p.getValidDomains(ctx, domain, domainFromConfigurationFile)
+func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (*certificate.Resource, error) {
+ domains, err := p.getValidDomains(ctx, domain)
if err != nil {
return nil, err
}
// Check provided certificates
- uncheckedDomains := p.getUncheckedDomains(ctx, domains, !domainFromConfigurationFile)
+ uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
if len(uncheckedDomains) == 0 {
return nil, nil
}
@@ -457,7 +469,7 @@ func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain,
} else {
domain = types.Domain{Main: uncheckedDomains[0]}
}
- p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey)
+ p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, tlsStore)
return cert, nil
}
@@ -480,22 +492,22 @@ func (p *Provider) addResolvingDomains(resolvingDomains []string) {
}
}
-func (p *Provider) addCertificateForDomain(domain types.Domain, certificate []byte, key []byte) {
- p.certsChan <- &Certificate{Certificate: certificate, Key: key, Domain: domain}
+func (p *Provider) addCertificateForDomain(domain types.Domain, certificate []byte, key []byte, tlsStore string) {
+ p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore}
}
// deleteUnnecessaryDomains deletes from the configuration :
// - Duplicated domains
// - Domains which are checked by wildcard domain
-func (p *Provider) deleteUnnecessaryDomains(ctx context.Context) {
+func deleteUnnecessaryDomains(ctx context.Context, domains []types.Domain) []types.Domain {
var newDomains []types.Domain
logger := log.FromContext(ctx)
- for idxDomainToCheck, domainToCheck := range p.Domains {
+ for idxDomainToCheck, domainToCheck := range domains {
keepDomain := true
- for idxDomain, domain := range p.Domains {
+ for idxDomain, domain := range domains {
if idxDomainToCheck == idxDomain {
continue
}
@@ -538,11 +550,11 @@ func (p *Provider) deleteUnnecessaryDomains(ctx context.Context) {
}
}
- p.Domains = newDomains
+ return newDomains
}
func (p *Provider) watchCertificate(ctx context.Context) {
- p.certsChan = make(chan *Certificate)
+ p.certsChan = make(chan *CertAndStore)
p.pool.Go(func(stop chan bool) {
for {
@@ -550,9 +562,8 @@ func (p *Provider) watchCertificate(ctx context.Context) {
case cert := <-p.certsChan:
certUpdated := false
for _, domainsCertificate := range p.certificates {
- if reflect.DeepEqual(cert.Domain, domainsCertificate.Domain) {
+ if reflect.DeepEqual(cert.Domain, domainsCertificate.Certificate.Domain) {
domainsCertificate.Certificate = cert.Certificate
- domainsCertificate.Key = cert.Key
certUpdated = true
break
}
@@ -573,7 +584,7 @@ func (p *Provider) watchCertificate(ctx context.Context) {
}
func (p *Provider) saveCertificates() error {
- err := p.Store.SaveCertificates(p.certificates)
+ err := p.Store.SaveCertificates(p.ResolverName, p.certificates)
p.refreshCertificates()
@@ -582,7 +593,7 @@ func (p *Provider) saveCertificates() error {
func (p *Provider) refreshCertificates() {
conf := dynamic.Message{
- ProviderName: "ACME",
+ ProviderName: "acme." + p.ResolverName,
Configuration: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@@ -596,9 +607,10 @@ func (p *Provider) refreshCertificates() {
for _, cert := range p.certificates {
certConf := &traefiktls.CertAndStores{
Certificate: traefiktls.Certificate{
- CertFile: traefiktls.FileOrContent(cert.Certificate),
+ CertFile: traefiktls.FileOrContent(cert.Certificate.Certificate),
KeyFile: traefiktls.FileOrContent(cert.Key),
},
+ Stores: []string{cert.Store},
}
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
}
@@ -611,7 +623,7 @@ func (p *Provider) renewCertificates(ctx context.Context) {
logger.Info("Testing certificate renew...")
for _, cert := range p.certificates {
- crt, err := getX509Certificate(ctx, cert)
+ crt, err := getX509Certificate(ctx, &cert.Certificate)
// If there's an error, we assume the cert is broken, and needs update
// <= 30 days left, renew certificate
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(24*30*time.Hour)) {
@@ -626,7 +638,7 @@ func (p *Provider) renewCertificates(ctx context.Context) {
renewedCert, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain.Main,
PrivateKey: cert.Key,
- Certificate: cert.Certificate,
+ Certificate: cert.Certificate.Certificate,
}, true, oscpMustStaple)
if err != nil {
@@ -639,20 +651,20 @@ func (p *Provider) renewCertificates(ctx context.Context) {
continue
}
- p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey)
+ p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
}
}
}
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
-func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, checkConfigurationDomains bool) []string {
+func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string {
p.resolvingDomainsMutex.RLock()
defer p.resolvingDomainsMutex.RUnlock()
log.FromContext(ctx).Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
- allDomains := p.tlsManager.GetStore("default").GetAllDomains()
+ allDomains := p.tlsManager.GetStore(tlsStore).GetAllDomains()
// Get ACME certificates
for _, cert := range p.certificates {
@@ -664,13 +676,6 @@ func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []str
allDomains = append(allDomains, domain)
}
- // Get Configuration Domains
- if checkConfigurationDomains {
- for i := 0; i < len(p.Domains); i++ {
- allDomains = append(allDomains, strings.Join(p.Domains[i].ToStrArray(), ","))
- }
- }
-
return searchUncheckedDomains(ctx, domainsToCheck, allDomains)
}
@@ -712,17 +717,13 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
}
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it
-func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain, wildcardAllowed bool) ([]string, error) {
+func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain) ([]string, error) {
domains := domain.ToStrArray()
if len(domains) == 0 {
return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given")
}
if strings.HasPrefix(domain.Main, "*") {
- if !wildcardAllowed {
- return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q from a 'Host' rule", strings.Join(domains, ","))
- }
-
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, ","))
}
diff --git a/pkg/provider/acme/provider_test.go b/pkg/provider/acme/provider_test.go
index 2f2581fa6..2629dda10 100644
--- a/pkg/provider/acme/provider_test.go
+++ b/pkg/provider/acme/provider_test.go
@@ -30,7 +30,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
desc string
dynamicCerts *safe.Safe
resolvingDomains map[string]struct{}
- acmeCertificates []*Certificate
+ acmeCertificates []*CertAndStore
domains []string
expectedDomains []string
}{
@@ -48,9 +48,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "wildcard already exists in ACME certificates",
domains: []string{"*.traefik.wtf"},
- acmeCertificates: []*Certificate{
+ acmeCertificates: []*CertAndStore{
{
- Domain: types.Domain{Main: "*.traefik.wtf"},
+ Certificate: Certificate{
+ Domain: types.Domain{Main: "*.traefik.wtf"},
+ },
},
},
expectedDomains: nil,
@@ -69,9 +71,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain CN already exists in ACME certificates and SANs to generate",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
- acmeCertificates: []*Certificate{
+ acmeCertificates: []*CertAndStore{
{
- Domain: types.Domain{Main: "traefik.wtf"},
+ Certificate: Certificate{
+ Domain: types.Domain{Main: "traefik.wtf"},
+ },
},
},
expectedDomains: []string{"foo.traefik.wtf"},
@@ -85,9 +89,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain already exists in ACME certificates",
domains: []string{"traefik.wtf"},
- acmeCertificates: []*Certificate{
+ acmeCertificates: []*CertAndStore{
{
- Domain: types.Domain{Main: "traefik.wtf"},
+ Certificate: Certificate{
+ Domain: types.Domain{Main: "traefik.wtf"},
+ },
},
},
expectedDomains: nil,
@@ -101,9 +107,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain matched by wildcard in ACME certificates",
domains: []string{"who.traefik.wtf", "foo.traefik.wtf"},
- acmeCertificates: []*Certificate{
+ acmeCertificates: []*CertAndStore{
{
- Domain: types.Domain{Main: "*.traefik.wtf"},
+ Certificate: Certificate{
+ Domain: types.Domain{Main: "*.traefik.wtf"},
+ },
},
},
expectedDomains: nil,
@@ -111,9 +119,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "root domain with wildcard in ACME certificates",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
- acmeCertificates: []*Certificate{
+ acmeCertificates: []*CertAndStore{
{
- Domain: types.Domain{Main: "*.traefik.wtf"},
+ Certificate: Certificate{
+ Domain: types.Domain{Main: "*.traefik.wtf"},
+ },
},
},
expectedDomains: []string{"traefik.wtf"},
@@ -171,7 +181,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
resolvingDomains: test.resolvingDomains,
}
- domains := acmeProvider.getUncheckedDomains(context.Background(), test.domains, false)
+ domains := acmeProvider.getUncheckedDomains(context.Background(), test.domains, "default")
assert.Equal(t, len(test.expectedDomains), len(domains), "Unexpected domains.")
})
}
@@ -181,7 +191,6 @@ func TestGetValidDomain(t *testing.T) {
testCases := []struct {
desc string
domains types.Domain
- wildcardAllowed bool
dnsChallenge *DNSChallenge
expectedErr string
expectedDomains []string
@@ -190,7 +199,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "valid wildcard",
domains: types.Domain{Main: "*.traefik.wtf"},
dnsChallenge: &DNSChallenge{},
- wildcardAllowed: true,
expectedErr: "",
expectedDomains: []string{"*.traefik.wtf"},
},
@@ -199,22 +207,12 @@ func TestGetValidDomain(t *testing.T) {
domains: types.Domain{Main: "traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: &DNSChallenge{},
expectedErr: "",
- wildcardAllowed: true,
expectedDomains: []string{"traefik.wtf", "foo.traefik.wtf"},
},
- {
- desc: "unauthorized wildcard",
- domains: types.Domain{Main: "*.traefik.wtf"},
- dnsChallenge: &DNSChallenge{},
- wildcardAllowed: false,
- expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf\" from a 'Host' rule",
- expectedDomains: nil,
- },
{
desc: "no domain",
domains: types.Domain{},
dnsChallenge: nil,
- wildcardAllowed: true,
expectedErr: "unable to generate a certificate in ACME provider when no domain is given",
expectedDomains: nil,
},
@@ -222,7 +220,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "no DNSChallenge",
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: nil,
- wildcardAllowed: true,
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : ACME needs a DNSChallenge",
expectedDomains: nil,
},
@@ -230,7 +227,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "unauthorized wildcard with SAN",
domains: types.Domain{Main: "*.*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: &DNSChallenge{},
- wildcardAllowed: true,
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,
},
@@ -238,7 +234,6 @@ func TestGetValidDomain(t *testing.T) {
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"},
},
@@ -246,7 +241,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "wildcard SANs",
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"*.acme.wtf"}},
dnsChallenge: &DNSChallenge{},
- wildcardAllowed: true,
expectedErr: "",
expectedDomains: []string{"*.traefik.wtf", "*.acme.wtf"},
},
@@ -259,7 +253,7 @@ func TestGetValidDomain(t *testing.T) {
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
- domains, err := acmeProvider.getValidDomains(context.Background(), test.domains, test.wildcardAllowed)
+ domains, err := acmeProvider.getValidDomains(context.Background(), test.domains)
if len(test.expectedErr) > 0 {
assert.EqualError(t, err, test.expectedErr, "Unexpected error.")
@@ -439,10 +433,8 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- acmeProvider := Provider{Configuration: &Configuration{Domains: test.domains}}
-
- acmeProvider.deleteUnnecessaryDomains(context.Background())
- assert.Equal(t, test.expectedDomains, acmeProvider.Domains, "unexpected domain")
+ domains := deleteUnnecessaryDomains(context.Background(), test.domains)
+ assert.Equal(t, test.expectedDomains, domains, "unexpected domain")
})
}
}
diff --git a/pkg/provider/acme/store.go b/pkg/provider/acme/store.go
index f8ea1baef..4d8c7965b 100644
--- a/pkg/provider/acme/store.go
+++ b/pkg/provider/acme/store.go
@@ -1,20 +1,27 @@
package acme
-// StoredData represents the data managed by Store
+// StoredData represents the data managed by Store.
type StoredData struct {
- Account *Account
- Certificates []*Certificate
+ Account *Account
+ Certificates []*CertAndStore
+}
+
+// StoredChallengeData represents the data managed by ChallengeStore.
+type StoredChallengeData struct {
HTTPChallenges map[string]map[string][]byte
TLSChallenges map[string]*Certificate
}
-// Store is a generic interface that represents a storage
+// Store is a generic interface that represents a storage.
type Store interface {
- GetAccount() (*Account, error)
- SaveAccount(*Account) error
- GetCertificates() ([]*Certificate, error)
- SaveCertificates([]*Certificate) error
+ GetAccount(string) (*Account, error)
+ SaveAccount(string, *Account) error
+ GetCertificates(string) ([]*CertAndStore, error)
+ SaveCertificates(string, []*CertAndStore) error
+}
+// ChallengeStore is a generic interface that represents a store for challenge data.
+type ChallengeStore interface {
GetHTTPChallengeToken(token, domain string) ([]byte, error)
SetHTTPChallengeToken(token, domain string, keyAuth []byte) error
RemoveHTTPChallengeToken(token, domain string) error
diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index 8f4a463d8..2450b6166 100644
--- a/pkg/provider/kubernetes/crd/kubernetes.go
+++ b/pkg/provider/kubernetes/crd/kubernetes.go
@@ -438,7 +438,10 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
if ingressRoute.Spec.TLS != nil {
- tlsConf := &dynamic.RouterTLSConfig{}
+ tlsConf := &dynamic.RouterTLSConfig{
+ CertResolver: ingressRoute.Spec.TLS.CertResolver,
+ }
+
if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRoute.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference, (i.e. not a cross-provider reference)
@@ -537,7 +540,8 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
if ingressRouteTCP.Spec.TLS != nil {
conf.Routers[serviceName].TLS = &dynamic.RouterTCPTLSConfig{
- Passthrough: ingressRouteTCP.Spec.TLS.Passthrough,
+ Passthrough: ingressRouteTCP.Spec.TLS.Passthrough,
+ CertResolver: ingressRouteTCP.Spec.TLS.CertResolver,
}
if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 {
diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go
index 2fb9be90a..ac7eb32e3 100644
--- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go
+++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go
@@ -32,7 +32,8 @@ type TLS struct {
// certificate details.
SecretName string `json:"secretName"`
// Options is a reference to a TLSOption, that specifies the parameters of the TLS connection.
- Options *TLSOptionRef `json:"options"`
+ Options *TLSOptionRef `json:"options"`
+ CertResolver string `json:"certResolver"`
}
// TLSOptionRef is a ref to the TLSOption resources.
diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go
index 4b844722c..c97bb109e 100644
--- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go
+++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go
@@ -30,7 +30,8 @@ type TLSTCP struct {
SecretName string `json:"secretName"`
Passthrough bool `json:"passthrough"`
// Options is a reference to a TLSOption, that specifies the parameters of the TLS connection.
- Options *TLSOptionTCPRef `json:"options"`
+ Options *TLSOptionTCPRef `json:"options"`
+ CertResolver string `json:"certResolver"`
}
// TLSOptionTCPRef is a ref to the TLSOption resources.
diff --git a/pkg/server/router/route_appender_factory.go b/pkg/server/router/route_appender_factory.go
index 5565aa616..2391fea79 100644
--- a/pkg/server/router/route_appender_factory.go
+++ b/pkg/server/router/route_appender_factory.go
@@ -11,7 +11,7 @@ import (
)
// NewRouteAppenderFactory Creates a new RouteAppenderFactory
-func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider *acme.Provider) *RouteAppenderFactory {
+func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider []*acme.Provider) *RouteAppenderFactory {
return &RouteAppenderFactory{
staticConfiguration: staticConfiguration,
entryPointName: entryPointName,
@@ -23,15 +23,18 @@ func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPoin
type RouteAppenderFactory struct {
staticConfiguration static.Configuration
entryPointName string
- acmeProvider *acme.Provider
+ acmeProvider []*acme.Provider
}
// NewAppender Creates a new RouteAppender
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *runtime.Configuration) types.RouteAppender {
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, runtimeConfiguration)
- if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName {
- aggregator.AddAppender(r.acmeProvider)
+ for _, p := range r.acmeProvider {
+ if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == r.entryPointName {
+ aggregator.AddAppender(p)
+ break
+ }
}
return aggregator
diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go
index 3111f3838..5552b2e22 100644
--- a/pkg/server/router/tcp/router.go
+++ b/pkg/server/router/tcp/router.go
@@ -79,6 +79,11 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m
return entryPointHandlers
}
+type nameAndConfig struct {
+ routerName string // just so we have it as additional information when logging
+ TLSConfig *tls.Config
+}
+
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
router := &tcp.Router{}
router.HTTPHandler(handlerHTTP)
@@ -86,15 +91,11 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
defaultTLSConf, err := m.tlsManager.Get("default", defaultTLSConfigName)
if err != nil {
- return nil, err
+ log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
- type nameAndConfig struct {
- routerName string // just so we have it as additional information when logging
- TLSConfig *tls.Config
- }
// Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
@@ -156,7 +157,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
- configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS option instead", hostSNI), false)
+ configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go
index 1873cfe58..90914998b 100644
--- a/pkg/tls/tlsmanager.go
+++ b/pkg/tls/tlsmanager.go
@@ -74,17 +74,22 @@ func (m *Manager) Get(storeName string, configName string) (*tls.Config, error)
m.lock.RLock()
defer m.lock.RUnlock()
+ var tlsConfig *tls.Config
+ var err error
+
config, ok := m.configs[configName]
if !ok {
- return nil, fmt.Errorf("unknown TLS options: %s", configName)
+ err = fmt.Errorf("unknown TLS options: %s", configName)
+ tlsConfig = &tls.Config{}
}
store := m.getStore(storeName)
- tlsConfig, err := buildTLSConfig(config)
- if err != nil {
- log.Error(err)
- tlsConfig = &tls.Config{}
+ if err == nil {
+ tlsConfig, err = buildTLSConfig(config)
+ if err != nil {
+ tlsConfig = &tls.Config{}
+ }
}
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -113,7 +118,8 @@ func (m *Manager) Get(storeName string, configName string) (*tls.Config, error)
log.WithoutContext().Debugf("Serving default certificate for request: %q", domainToCheck)
return store.DefaultCertificate, nil
}
- return tlsConfig, nil
+
+ return tlsConfig, err
}
func (m *Manager) getStore(storeName string) *CertificateStore {
@@ -143,7 +149,7 @@ func buildCertificateStore(tlsStore Store) (*CertificateStore, error) {
}
certificateStore.DefaultCertificate = cert
} else {
- log.Debug("No default certificate, generate one")
+ log.Debug("No default certificate, generating one")
cert, err := generate.DefaultCertificate()
if err != nil {
return certificateStore, err
diff --git a/pkg/tls/tlsmanager_test.go b/pkg/tls/tlsmanager_test.go
index a176bfe11..9fcc4935b 100644
--- a/pkg/tls/tlsmanager_test.go
+++ b/pkg/tls/tlsmanager_test.go
@@ -152,11 +152,21 @@ func TestManager_Get(t *testing.T) {
func TestClientAuth(t *testing.T) {
tlsConfigs := map[string]Options{
- "eca": {ClientAuth: ClientAuth{}},
- "ecat": {ClientAuth: ClientAuth{ClientAuthType: ""}},
- "ncc": {ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"}},
- "rcc": {ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"}},
- "racc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"}},
+ "eca": {
+ ClientAuth: ClientAuth{},
+ },
+ "ecat": {
+ ClientAuth: ClientAuth{ClientAuthType: ""},
+ },
+ "ncc": {
+ ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"},
+ },
+ "rcc": {
+ ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"},
+ },
+ "racc": {
+ ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"},
+ },
"vccig": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
@@ -166,7 +176,9 @@ func TestClientAuth(t *testing.T) {
"vccigwca": {
ClientAuth: ClientAuth{ClientAuthType: "VerifyClientCertIfGiven"},
},
- "ravcc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"}},
+ "ravcc": {
+ ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"},
+ },
"ravccwca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
@@ -179,7 +191,9 @@ func TestClientAuth(t *testing.T) {
ClientAuthType: "RequireAndVerifyClientCert",
},
},
- "ucat": {ClientAuth: ClientAuth{ClientAuthType: "Unknown"}},
+ "ucat": {
+ ClientAuth: ClientAuth{ClientAuthType: "Unknown"},
+ },
}
block, _ := pem.Decode([]byte(localhostCert))
@@ -191,6 +205,7 @@ func TestClientAuth(t *testing.T) {
tlsOptionsName string
expectedClientAuth tls.ClientAuthType
expectedRawSubject []byte
+ expectedError bool
}{
{
desc: "Empty ClientAuth option should get a tls.NoClientCert (default value)",
@@ -223,14 +238,16 @@ func TestClientAuth(t *testing.T) {
expectedClientAuth: tls.VerifyClientCertIfGiven,
},
{
- desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
+ desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "vccigwca",
expectedClientAuth: tls.NoClientCert,
+ expectedError: true,
},
{
- desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
+ desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravcc",
expectedClientAuth: tls.NoClientCert,
+ expectedError: true,
},
{
desc: "RequireAndVerifyClientCert option should get a tls.RequireAndVerifyClientCert as ClientAuthType with CA files",
@@ -242,11 +259,13 @@ func TestClientAuth(t *testing.T) {
desc: "Unknown option yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ucat",
expectedClientAuth: tls.NoClientCert,
+ expectedError: true,
},
{
desc: "Bad CA certificate content yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravccwbca",
expectedClientAuth: tls.NoClientCert,
+ expectedError: true,
},
}
@@ -259,6 +278,12 @@ func TestClientAuth(t *testing.T) {
t.Parallel()
config, err := tlsManager.Get("default", test.tlsOptionsName)
+
+ if test.expectedError {
+ assert.Error(t, err)
+ return
+ }
+
assert.NoError(t, err)
if test.expectedRawSubject != nil {
diff --git a/pkg/types/domains.go b/pkg/types/domains.go
index 0ebda7f59..101fb584a 100644
--- a/pkg/types/domains.go
+++ b/pkg/types/domains.go
@@ -1,10 +1,11 @@
package types
import (
- "fmt"
"strings"
)
+// +k8s:deepcopy-gen=true
+
// Domain holds a domain name with SANs.
type Domain struct {
Main string `description:"Default subject name." json:"main,omitempty" toml:"main,omitempty" yaml:"main,omitempty"`
@@ -28,44 +29,6 @@ func (d *Domain) Set(domains []string) {
}
}
-// Domains parse []Domain.
-type Domains []Domain
-
-// Set []Domain
-func (ds *Domains) Set(str string) error {
- fargs := func(c rune) bool {
- return c == ',' || c == ';'
- }
-
- // get function
- slice := strings.FieldsFunc(str, fargs)
- if len(slice) < 1 {
- return fmt.Errorf("parse error ACME.Domain. Unable to parse %s", str)
- }
-
- d := Domain{
- Main: slice[0],
- }
-
- if len(slice) > 1 {
- d.SANs = slice[1:]
- }
-
- *ds = append(*ds, d)
- return nil
-}
-
-// Get []Domain.
-func (ds *Domains) Get() interface{} { return []Domain(*ds) }
-
-// String returns []Domain in string.
-func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
-
-// SetValue sets []Domain into the parser
-func (ds *Domains) SetValue(val interface{}) {
- *ds = val.([]Domain)
-}
-
// MatchDomain returns true if a domain match the cert domain.
func MatchDomain(domain string, certDomain string) bool {
if domain == certDomain {
diff --git a/pkg/types/zz_generated.deepcopy.go b/pkg/types/zz_generated.deepcopy.go
new file mode 100644
index 000000000..78befb756
--- /dev/null
+++ b/pkg/types/zz_generated.deepcopy.go
@@ -0,0 +1,50 @@
+// +build !ignore_autogenerated
+
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016-2019 Containous SAS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package types
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Domain) DeepCopyInto(out *Domain) {
+ *out = *in
+ if in.SANs != nil {
+ in, out := &in.SANs, &out.SANs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Domain.
+func (in *Domain) DeepCopy() *Domain {
+ if in == nil {
+ return nil
+ }
+ out := new(Domain)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/script/update-generated-crd-code.sh b/script/update-generated-crd-code.sh
index b7f7a5e4b..b19eceb63 100755
--- a/script/update-generated-crd-code.sh
+++ b/script/update-generated-crd-code.sh
@@ -11,4 +11,8 @@ REPO_ROOT=${HACK_DIR}/..
--go-header-file "${HACK_DIR}"/boilerplate.go.tmpl \
"$@"
-deepcopy-gen --input-dirs github.com/containous/traefik/pkg/config/dynamic --input-dirs github.com/containous/traefik/pkg/tls -O zz_generated.deepcopy --go-header-file "${HACK_DIR}"/boilerplate.go.tmpl
+deepcopy-gen \
+--input-dirs github.com/containous/traefik/pkg/config/dynamic \
+--input-dirs github.com/containous/traefik/pkg/tls \
+--input-dirs github.com/containous/traefik/pkg/types \
+-O zz_generated.deepcopy --go-header-file "${HACK_DIR}"/boilerplate.go.tmpl