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