From ef38810425c44442c321b8dfd0b99f4a870fac50 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 3 Dec 2019 10:16:05 +0100 Subject: [PATCH 01/13] Upgrade python version to 3.7 for netlify --- docs/runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/runtime.txt b/docs/runtime.txt index d70c8f8d8..475ba515c 100644 --- a/docs/runtime.txt +++ b/docs/runtime.txt @@ -1 +1 @@ -3.6 +3.7 From 0e6dce7093fec340959ce47ae3e1195a183347b9 Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 4 Dec 2019 16:26:05 +0100 Subject: [PATCH 02/13] Do not stop to listen on tcp listeners on temporary errors --- pkg/server/server_entrypoint_tcp.go | 4 ++++ pkg/tls/certificate.go | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 51043403d..a53750784 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -135,6 +135,10 @@ func (e *TCPEntryPoint) startTCP(ctx context.Context) { conn, err := e.listener.Accept() if err != nil { logger.Error(err) + if netErr, ok := err.(net.Error); ok && netErr.Temporary() { + continue + } + return } diff --git a/pkg/tls/certificate.go b/pkg/tls/certificate.go index 1e700f111..4620c4fba 100644 --- a/pkg/tls/certificate.go +++ b/pkg/tls/certificate.go @@ -80,7 +80,8 @@ func (f FileOrContent) IsPath() bool { func (f FileOrContent) Read() ([]byte, error) { var content []byte - if _, err := os.Stat(f.String()); err == nil { + if f.IsPath() { + var err error content, err = ioutil.ReadFile(f.String()) if err != nil { return nil, err From 8dfc0d9ddaa09c192ba64a251f3ab9f8903b5938 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Thu, 5 Dec 2019 21:50:04 +0100 Subject: [PATCH 03/13] readme: Fix link to file backend/provider documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee973b0ea..0c3da953c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t - [Kubernetes](https://docs.traefik.io/providers/kubernetes-crd/) - [Marathon](https://docs.traefik.io/providers/marathon/) - [Rancher](https://docs.traefik.io/providers/rancher/) (Metadata) -- [File](https://docs.traefik.io/configuration/backends/file) +- [File](https://docs.traefik.io/providers/file/) ## Quickstart From a7d7c2b98bd590c97e801debd5de45424ffbad9c Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 6 Dec 2019 02:42:04 +0300 Subject: [PATCH 04/13] Fix Docker example in "Strip and Rewrite Path Prefixes" in migration guide --- docs/content/migration/v1-to-v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 429eb7cc8..0ae0bbef5 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -560,8 +560,8 @@ with the path `/admin` stripped, e.g. to `http://:/`. In this case, yo ```yaml tab="Docker" labels: - "traefik.http.routers.admin.rule=Host(`company.org`) && PathPrefix(`/admin`)" + - "traefik.http.routers.admin.middlewares=admin-stripprefix" - "traefik.http.middlewares.admin-stripprefix.stripprefix.prefixes=/admin" - - "traefik.http.routers.web.middlewares=admin-stripprefix@docker" ``` ```yaml tab="Kubernetes IngressRoute" From 50bb69b7968c26228694eef48cd629bda0b3de70 Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Mon, 9 Dec 2019 03:16:05 -0600 Subject: [PATCH 05/13] Document LE caveats with Kubernetes on v2 --- docs/content/https/acme.md | 7 +++++++ docs/content/providers/kubernetes-crd.md | 17 +++++++++++++++++ docs/content/providers/kubernetes-ingress.md | 14 ++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index d23cf4317..9b86ce7b8 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -84,6 +84,13 @@ If there are less than 30 days remaining before the certificate expires, Traefik !!! info "" Certificates that are no longer used may still be renewed, as Traefik does not currently check if the certificate is being used before renewing. +## Using LetsEncrypt with Kubernetes + +When using LetsEncrypt with kubernetes, there are some known caveats with both the [ingress](../providers/kubernetes-ingress.md) and [crd](../providers/kubernetes-crd.md) providers. + +!!! info "" + If you intend to run multiple instances of Traefik with LetsEncrypt, please ensure you read the sections on those provider pages. + ## The Different ACME Challenges !!! important "Defining a certificates resolver does not result in all routers automatically using it. Each router that is supposed to use the resolver must [reference](../routing/routers/index.md#certresolver) it." diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index c992d3164..8305b2705 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -12,6 +12,23 @@ we ended up writing a [Custom Resource Definition](https://kubernetes.io/docs/co See the dedicated section in [routing](../routing/providers/kubernetes-crd.md). +## LetsEncrypt Support with the Custom Resource Definition Provider + +By design, Traefik is a stateless application, meaning that it only derives its configuration from the environment it runs in, without additional configuration. +For this reason, users can run multiple instances of Traefik at the same time to achieve HA, as is a common pattern in the kubernetes ecosystem. + +When using a single instance of Traefik with LetsEncrypt, no issues should be encountered, however this could be a single point of failure. +Unfortunately, it is not possible to run multiple instances of Traefik 2.0 with LetsEncrypt enabled, because there is no way to ensure that the correct instance of Traefik will receive the challenge request, and subsequent responses. +Previous versions of Traefik used a [KV store](https://docs.traefik.io/v1.7/configuration/acme/#storage) to attempt to achieve this, but due to sub-optimal performance was dropped as a feature in 2.0. + +If you require LetsEncrypt with HA in a kubernetes environment, we recommend using [TraefikEE](https://containo.us/traefikee/) where distributed LetsEncrypt is a supported feature. + +If you are wanting to continue to run Traefik Community Edition, LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). +When using Cert-Manager to manage certificates, it will create secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). +When using the Traefik Kubernetes CRD Provider, unfortunately Cert-Manager cannot interface directly with the CRDs _yet_, but this is being worked on by our team. +A workaround it to enable the [Kubernetes Ingress provider](./kubernetes-ingress.md) to allow Cert-Manager to create ingress objects to complete the challenges. +Please note that this still requires manual intervention to create the certificates through Cert-Manager, but once created, Cert-Manager will keep the certificate renewed. + ## Provider Configuration ### `endpoint` diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index 95aeeeda3..2a269863a 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -47,6 +47,20 @@ spec: servicePort: 80 ``` +## LetsEncrypt Support with the Ingress Provider + +By design, Traefik is a stateless application, meaning that it only derives its configuration from the environment it runs in, without additional configuration. +For this reason, users can run multiple instances of Traefik at the same time to achieve HA, as is a common pattern in the kubernetes ecosystem. + +When using a single instance of Traefik with LetsEncrypt, no issues should be encountered, however this could be a single point of failure. +Unfortunately, it is not possible to run multiple instances of Traefik 2.0 with LetsEncrypt enabled, because there is no way to ensure that the correct instance of Traefik will receive the challenge request, and subsequent responses. +Previous versions of Traefik used a [KV store](https://docs.traefik.io/v1.7/configuration/acme/#storage) to attempt to achieve this, but due to sub-optimal performance was dropped as a feature in 2.0. + +If you require LetsEncrypt with HA in a kubernetes environment, we recommend using [TraefikEE](https://containo.us/traefikee/) where distributed LetsEncrypt is a supported feature. + +If you are wanting to continue to run Traefik Community Edition, LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). +When using Cert-Manager to manage certificates, it will create secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). + ## Provider Configuration ### `endpoint` From f40cf2cd8ecad09515dda91c54eb6f67f74c8c34 Mon Sep 17 00:00:00 2001 From: Eugen Mayer Date: Mon, 9 Dec 2019 11:42:06 +0100 Subject: [PATCH 06/13] The Cloudflare hint for the GLOBAL API KEY for CF MAIL/API_KEY --- docs/content/https/acme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index 9b86ce7b8..7602bc244 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -227,7 +227,7 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used | [Bindman](https://github.com/labbsr0x/bindman-dns-webhook) | `bindman` | `BINDMAN_MANAGER_ADDRESS` | [Additional configuration](https://go-acme.github.io/lego/dns/bindman) | | [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | [Additional configuration](https://go-acme.github.io/lego/dns/bluecat) | | [ClouDNS](https://www.cloudns.net/) | `cloudns` | `CLOUDNS_AUTH_ID`, `CLOUDNS_AUTH_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudns) | -| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CF_API_EMAIL`, `CF_API_KEY` or `CF_DNS_API_TOKEN`, `[CF_ZONE_API_TOKEN]` [^5] | [Additional configuration](https://go-acme.github.io/lego/dns/cloudflare) | +| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CF_API_EMAIL`, `CF_API_KEY` [^5] or `CF_DNS_API_TOKEN`, `[CF_ZONE_API_TOKEN]` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudflare) | | [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudxns) | | [ConoHa](https://www.conoha.jp) | `conoha` | `CONOHA_TENANT_ID`, `CONOHA_API_USERNAME`, `CONOHA_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/conoha) | | [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/digitalocean) | From 89db08eb93fd8cf66ac1948efb4e5bbaa421239f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou Date: Mon, 9 Dec 2019 11:48:05 +0100 Subject: [PATCH 07/13] Improve documentation on file provider limitations with file system notifications --- docs/content/migration/v1-to-v2.md | 8 ++++---- docs/content/providers/file.md | 28 ++++++++++++++++++++++------ docs/content/routing/overview.md | 24 ++++++++++++------------ docs/content/user-guides/grpc.md | 16 ++++++++-------- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 0ae0bbef5..ead0fc0e8 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -1029,12 +1029,12 @@ As the dashboard access is now secured by default you can either: [api] [providers.file] - filename = "/dynamic-conf.toml" + directory = "/path/to/dynamic/config" ##---------------------## ## dynamic configuration - # dynamic-conf.toml + # /path/to/dynamic/config/dynamic-conf.toml [http.routers.api] rule = "Host(`traefik.docker.localhost`)" @@ -1061,12 +1061,12 @@ As the dashboard access is now secured by default you can either: providers: file: - filename: /dynamic-conf.yaml + directory: /path/to/dynamic/config ##---------------------## ## dynamic configuration - # dynamic-conf.yaml + # /path/to/dynamic/config/dynamic-conf.yaml http: routers: diff --git a/docs/content/providers/file.md b/docs/content/providers/file.md index 747c2de1a..7fac1e95f 100644 --- a/docs/content/providers/file.md +++ b/docs/content/providers/file.md @@ -23,17 +23,17 @@ You can write one of these mutually exclusive configuration elements: ```toml tab="File (TOML)" [providers.file] - filename = "/my/path/to/dynamic-conf.toml" + directory = "/path/to/dynamic/conf" ``` ```yaml tab="File (YAML)" providers: file: - filename: "/my/path/to/dynamic-conf.yml" + directory: "/path/to/dynamic/conf" ``` ```bash tab="CLI" - --providers.file.filename=/my/path/to/dynamic_conf.toml + --providers.file.directory=/path/to/dynamic/conf ``` Declaring Routers, Middlewares & Services: @@ -100,6 +100,22 @@ You can write one of these mutually exclusive configuration elements: If you're in a hurry, maybe you'd rather go through the [dynamic configuration](../reference/dynamic-configuration/file.md) references and the [static configuration](../reference/static-configuration/overview.md). +!!! warning "Limitations" + + With the file provider, Traefik listens for file system notifications to update the dynamic configuration. + + If you use a mounted/bound file system in your orchestrator (like docker or kubernetes), the way the files are linked may be a source of errors. + If the link between the file systems is broken, when a source file/directory is changed/renamed, nothing will be reported to the linked file/directory, so the file system notifications will be neither triggered nor caught. + + For example, in docker, if the host file is renamed, the link to the mounted file will be broken and the container's file will not be updated. + To avoid this kind of issue, a good practice is to: + + * set the Traefik [**directory**](#directory) configuration with the parent directory + * mount/bind the parent directory + + As it is very difficult to listen to all file system notifications, Traefik use [fsnotify](https://github.com/fsnotify/fsnotify). + If using a directory with a mounted directory does not fix your issue, please check your file system compatibility with fsnotify. + ### `filename` Defines the path of the configuration file. @@ -148,19 +164,19 @@ It works with both the `filename` and the `directory` options. ```toml tab="File (TOML)" [providers] [providers.file] - filename = "dynamic_conf.toml" + directory = "/path/to/dynamic/conf" watch = true ``` ```yaml tab="File (YAML)" providers: file: - filename: dynamic_conf.yml + directory: /path/to/dynamic/conf watch: true ``` ```bash tab="CLI" ---providers.file.filename=dynamic_conf.toml +--providers.file.directory=/my/path/to/dynamic/conf --providers.file.watch=true ``` diff --git a/docs/content/routing/overview.md b/docs/content/routing/overview.md index 8808e0a48..c66cdb161 100644 --- a/docs/content/routing/overview.md +++ b/docs/content/routing/overview.md @@ -33,9 +33,9 @@ Static configuration: address = ":8081" [providers] - # Enable the file provider to define routers / middlewares / services in a file + # Enable the file provider to define routers / middlewares / services in file [providers.file] - filename = "dynamic_conf.toml" + directory = "/path/to/dynamic/conf" ``` ```yaml tab="File (YAML)" @@ -45,17 +45,17 @@ entryPoints: address: :8081 providers: - # Enable the file provider to define routers / middlewares / services in a file + # Enable the file provider to define routers / middlewares / services in file file: - filename: dynamic_conf.yml + directory: /path/to/dynamic/conf ``` ```bash tab="CLI" # Listen on port 8081 for incoming requests --entryPoints.web.address=:8081 -# Enable the file provider to define routers / middlewares / services in a file ---providers.file.filename=dynamic_conf.toml +# Enable the file provider to define routers / middlewares / services in file +--providers.file.directory=/path/to/dynamic/conf ``` Dynamic configuration: @@ -133,9 +133,9 @@ http: address = ":8081" [providers] - # Enable the file provider to define routers / middlewares / services in a file + # Enable the file provider to define routers / middlewares / services in file [providers.file] - filename = "dynamic_conf.toml" + directory = "/path/to/dynamic/conf" ``` ```yaml tab="File (YAML)" @@ -144,17 +144,17 @@ http: # Listen on port 8081 for incoming requests address: :8081 providers: - # Enable the file provider to define routers / middlewares / services in a file + # Enable the file provider to define routers / middlewares / services in file file: - filename: dynamic_conf.yml + directory: /path/to/dynamic/conf ``` ```bash tab="CLI" # Listen on port 8081 for incoming requests --entryPoints.web.address=:8081 - # Enable the file provider to define routers / middlewares / services in a file - --providers.file.filename=dynamic_conf.toml + # Enable the file provider to define routers / middlewares / services in file + --providers.file.directory=/path/to/dynamic/conf ``` **Dynamic Configuration** diff --git a/docs/content/user-guides/grpc.md b/docs/content/user-guides/grpc.md index 6c8979dec..3fef7d420 100644 --- a/docs/content/user-guides/grpc.md +++ b/docs/content/user-guides/grpc.md @@ -16,7 +16,7 @@ Static configuration: [api] [providers.file] - filename = "dynamic_conf.toml" + directory = "/path/to/dynamic/config" ``` ```yaml tab="File (YAML)" @@ -26,18 +26,18 @@ entryPoints: providers: file: - filename: dynamic_conf.yml + directory: /path/to/dynamic/config api: {} ``` ```yaml tab="CLI" --entryPoints.web.address=:80 ---providers.file.filename=dynamic_conf.toml +--providers.file.directory=/path/to/dynamic/config --api.insecure=true ``` -`dynamic_conf.{toml,yml}`: +`/path/to/dynamic/config/dynamic_conf.{toml,yml}`: ```toml tab="TOML" ## dynamic configuration ## @@ -132,7 +132,7 @@ Static configuration: [api] [provider.file] - filename = "dynamic_conf.toml" + directory = "/path/to/dynamic/config" ``` ```yaml tab="File (YAML)" @@ -147,7 +147,7 @@ serversTransport: providers: file: - filename: dynamic_conf.yml + directory: /path/to/dynamic/config api: {} ``` @@ -156,11 +156,11 @@ api: {} --entryPoints.websecure.address=:4443 # For secure connection on backend.local --serversTransport.rootCAs=./backend.cert ---providers.file.filename=dynamic_conf.toml +--providers.file.directory=/path/to/dynamic/config --api.insecure=true ``` -`dynamic_conf.{toml,yml}`: +`/path/to/dynamic/config/dynamic_conf.{toml,yml}`: ```toml tab="TOML" ## dynamic configuration ## From 39a3cefc216e6c30b625de17119f73b39fd984c6 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 9 Dec 2019 12:20:06 +0100 Subject: [PATCH 08/13] fix: PassClientTLSCert middleware separators and formatting --- docs/content/middlewares/passtlsclientcert.md | 12 +- .../passtlsclientcert/pass_tls_client_cert.go | 251 +++++++------ .../pass_tls_client_cert_test.go | 333 +++++++++--------- 3 files changed, 303 insertions(+), 293 deletions(-) diff --git a/docs/content/middlewares/passtlsclientcert.md b/docs/content/middlewares/passtlsclientcert.md index 9da4dbed0..137336ba1 100644 --- a/docs/content/middlewares/passtlsclientcert.md +++ b/docs/content/middlewares/passtlsclientcert.md @@ -380,7 +380,7 @@ In the example, it is the part between `-----BEGIN CERTIFICATE-----` and `-----E !!! info "Extracted data" The delimiters and `\n` will be removed. - If there are more than one certificate, they are separated by a "`;`". + If there are more than one certificate, they are separated by a "`,`". !!! warning "`X-Forwarded-Tls-Client-Cert` value could exceed the web server header size limit" @@ -395,12 +395,12 @@ The value of the header will be an escaped concatenation of all the selected cer The following example shows an unescaped result that uses all the available fields: ```text -Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com",Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2",NB=1544094616,NA=1607166616,SAN=*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2 +Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com";Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2";NB="1544094616";NA="1607166616";SAN="*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2" ``` !!! info "Multiple certificates" - If there are more than one certificate, they are separated by a `;`. + If there are more than one certificate, they are separated by a `,`. #### `info.notAfter` @@ -416,7 +416,7 @@ The data are taken from the following certificate part: The escape `notAfter` info part will be like: ```text -NA=1607166616 +NA="1607166616" ``` #### `info.notBefore` @@ -433,7 +433,7 @@ Validity The escape `notBefore` info part will be like: ```text -NB=1544094616 +NB="1544094616" ``` #### `info.sans` @@ -450,7 +450,7 @@ The data are taken from the following certificate part: The escape SANs info part will be like: ```text -SAN=*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2 +SAN="*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2" ``` !!! info "multiple values" diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go index a07bb9a73..2b5e29dd2 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go @@ -18,10 +18,17 @@ import ( "github.com/opentracing/opentracing-go/ext" ) +const typeName = "PassClientTLSCert" + const ( xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert" xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info" - typeName = "PassClientTLSCert" +) + +const ( + certSeparator = "," + fieldSeparator = ";" + subFieldSeparator = "," ) var attributeTypeNames = map[string]string{ @@ -55,6 +62,29 @@ func newDistinguishedNameOptions(info *dynamic.TLSCLientCertificateDNInfo) *Dist } } +// tlsClientCertificateInfo is a struct for specifying the configuration for the passTLSClientCert middleware. +type tlsClientCertificateInfo struct { + notAfter bool + notBefore bool + sans bool + subject *DistinguishedNameOptions + issuer *DistinguishedNameOptions +} + +func newTLSClientCertificateInfo(info *dynamic.TLSClientCertificateInfo) *tlsClientCertificateInfo { + if info == nil { + return nil + } + + return &tlsClientCertificateInfo{ + issuer: newDistinguishedNameOptions(info.Issuer), + notAfter: info.NotAfter, + notBefore: info.NotBefore, + subject: newDistinguishedNameOptions(info.Subject), + sans: info.Sans, + } +} + // passTLSClientCert is a middleware that helps setup a few tls info features. type passTLSClientCert struct { next http.Handler @@ -71,45 +101,84 @@ func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCer next: next, name: name, pem: config.PEM, - info: newTLSClientInfo(config.Info), + info: newTLSClientCertificateInfo(config.Info), }, nil } -// tlsClientCertificateInfo is a struct for specifying the configuration for the passTLSClientCert middleware. -type tlsClientCertificateInfo struct { - notAfter bool - notBefore bool - sans bool - subject *DistinguishedNameOptions - issuer *DistinguishedNameOptions -} - -func newTLSClientInfo(info *dynamic.TLSClientCertificateInfo) *tlsClientCertificateInfo { - if info == nil { - return nil - } - - return &tlsClientCertificateInfo{ - issuer: newDistinguishedNameOptions(info.Issuer), - notAfter: info.NotAfter, - notBefore: info.NotBefore, - subject: newDistinguishedNameOptions(info.Subject), - sans: info.Sans, - } -} - func (p *passTLSClientCert) GetTracingInformation() (string, ext.SpanKindEnum) { return p.name, tracing.SpanKindNoneEnum } func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) { ctx := middlewares.GetLoggerCtx(req.Context(), p.name, typeName) + logger := log.FromContext(ctx) + + if p.pem { + if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 { + req.Header.Set(xForwardedTLSClientCert, getCertificates(ctx, req.TLS.PeerCertificates)) + } else { + logger.Warn("Tried to extract a certificate on a request without mutual TLS") + } + } + + if p.info != nil { + if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 { + headerContent := p.getCertInfo(ctx, req.TLS.PeerCertificates) + req.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent)) + } else { + logger.Warn("Tried to extract a certificate on a request without mutual TLS") + } + } - p.modifyRequestHeaders(ctx, req) p.next.ServeHTTP(rw, req) } -func getDNInfo(ctx context.Context, prefix string, options *DistinguishedNameOptions, cs *pkix.Name) string { +// getCertInfo Build a string with the wanted client certificates information +// - the `,` is used to separate certificates +// - the `;` is used to separate root fields +// - the value of root fields is always wrapped by double quote +// - if a field is empty, the field is ignored +func (p *passTLSClientCert) getCertInfo(ctx context.Context, certs []*x509.Certificate) string { + var headerValues []string + + for _, peerCert := range certs { + var values []string + + if p.info != nil { + subject := getDNInfo(ctx, p.info.subject, &peerCert.Subject) + if subject != "" { + values = append(values, fmt.Sprintf(`Subject="%s"`, strings.TrimSuffix(subject, subFieldSeparator))) + } + + issuer := getDNInfo(ctx, p.info.issuer, &peerCert.Issuer) + if issuer != "" { + values = append(values, fmt.Sprintf(`Issuer="%s"`, strings.TrimSuffix(issuer, subFieldSeparator))) + } + + if p.info.notBefore { + values = append(values, fmt.Sprintf(`NB="%d"`, uint64(peerCert.NotBefore.Unix()))) + } + + if p.info.notAfter { + values = append(values, fmt.Sprintf(`NA="%d"`, uint64(peerCert.NotAfter.Unix()))) + } + + if p.info.sans { + sans := getSANs(peerCert) + if len(sans) > 0 { + values = append(values, fmt.Sprintf(`SAN="%s"`, strings.Join(sans, subFieldSeparator))) + } + } + } + + value := strings.Join(values, fieldSeparator) + headerValues = append(headerValues, value) + } + + return strings.Join(headerValues, certSeparator) +} + +func getDNInfo(ctx context.Context, options *DistinguishedNameOptions, cs *pkix.Name) string { if options == nil { return "" } @@ -120,7 +189,7 @@ func getDNInfo(ctx context.Context, prefix string, options *DistinguishedNameOpt for _, name := range cs.Names { // Domain Component - RFC 2247 if options.DomainComponent && attributeTypeNames[name.Type.String()] == "DC" { - content.WriteString(fmt.Sprintf("DC=%s,", name.Value)) + content.WriteString(fmt.Sprintf("DC=%s%s", name.Value, subFieldSeparator)) } } @@ -148,11 +217,7 @@ func getDNInfo(ctx context.Context, prefix string, options *DistinguishedNameOpt writePart(ctx, content, cs.CommonName, "CN") } - if content.Len() > 0 { - return prefix + `="` + strings.TrimSuffix(content.String(), ",") + `"` - } - - return "" + return content.String() } func writeParts(ctx context.Context, content io.StringWriter, entries []string, prefix string) { @@ -163,135 +228,63 @@ func writeParts(ctx context.Context, content io.StringWriter, entries []string, func writePart(ctx context.Context, content io.StringWriter, entry string, prefix string) { if len(entry) > 0 { - _, err := content.WriteString(fmt.Sprintf("%s=%s,", prefix, entry)) + _, err := content.WriteString(fmt.Sprintf("%s=%s%s", prefix, entry, subFieldSeparator)) if err != nil { log.FromContext(ctx).Error(err) } } } -// getXForwardedTLSClientCertInfo Build a string with the wanted client certificates information -// like Subject="C=%s,ST=%s,L=%s,O=%s,CN=%s",NB=%d,NA=%d,SAN=%s; -func (p *passTLSClientCert) getXForwardedTLSClientCertInfo(ctx context.Context, certs []*x509.Certificate) string { - var headerValues []string - - for _, peerCert := range certs { - var values []string - var sans string - var nb string - var na string - - if p.info != nil { - subject := getDNInfo(ctx, "Subject", p.info.subject, &peerCert.Subject) - if len(subject) > 0 { - values = append(values, subject) - } - - issuer := getDNInfo(ctx, "Issuer", p.info.issuer, &peerCert.Issuer) - if len(issuer) > 0 { - values = append(values, issuer) - } - } - - ci := p.info - if ci != nil { - if ci.notBefore { - nb = fmt.Sprintf("NB=%d", uint64(peerCert.NotBefore.Unix())) - values = append(values, nb) - } - if ci.notAfter { - na = fmt.Sprintf("NA=%d", uint64(peerCert.NotAfter.Unix())) - values = append(values, na) - } - - if ci.sans { - sans = fmt.Sprintf("SAN=%s", strings.Join(getSANs(peerCert), ",")) - values = append(values, sans) - } - } - - value := strings.Join(values, ",") - headerValues = append(headerValues, value) - } - - return strings.Join(headerValues, ";") -} - -// modifyRequestHeaders set the wanted headers with the certificates information. -func (p *passTLSClientCert) modifyRequestHeaders(ctx context.Context, r *http.Request) { - logger := log.FromContext(ctx) - - if p.pem { - if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { - r.Header.Set(xForwardedTLSClientCert, getXForwardedTLSClientCert(ctx, r.TLS.PeerCertificates)) - } else { - logger.Warn("Tried to extract a certificate on a request without mutual TLS") - } - } - - if p.info != nil { - if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { - headerContent := p.getXForwardedTLSClientCertInfo(ctx, r.TLS.PeerCertificates) - r.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent)) - } else { - logger.Warn("Tried to extract a certificate on a request without mutual TLS") - } - } -} - // sanitize As we pass the raw certificates, remove the useless data and make it http request compliant. func sanitize(cert []byte) string { - s := string(cert) - r := strings.NewReplacer("-----BEGIN CERTIFICATE-----", "", + cleaned := strings.NewReplacer( + "-----BEGIN CERTIFICATE-----", "", "-----END CERTIFICATE-----", "", - "\n", "") - cleaned := r.Replace(s) + "\n", "", + ).Replace(string(cert)) return url.QueryEscape(cleaned) } -// extractCertificate extract the certificate from the request. -func extractCertificate(ctx context.Context, cert *x509.Certificate) string { - b := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} - certPEM := pem.EncodeToMemory(&b) - if certPEM == nil { - log.FromContext(ctx).Error("Cannot extract the certificate content") - return "" - } - return sanitize(certPEM) -} - -// getXForwardedTLSClientCert Build a string with the client certificates. -func getXForwardedTLSClientCert(ctx context.Context, certs []*x509.Certificate) string { +// getCertificates Build a string with the client certificates. +func getCertificates(ctx context.Context, certs []*x509.Certificate) string { var headerValues []string for _, peerCert := range certs { headerValues = append(headerValues, extractCertificate(ctx, peerCert)) } - return strings.Join(headerValues, ",") + return strings.Join(headerValues, certSeparator) +} + +// extractCertificate extract the certificate from the request. +func extractCertificate(ctx context.Context, cert *x509.Certificate) string { + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if certPEM == nil { + log.FromContext(ctx).Error("Cannot extract the certificate content") + return "" + } + + return sanitize(certPEM) } // getSANs get the Subject Alternate Name values. func getSANs(cert *x509.Certificate) []string { - var sans []string if cert == nil { - return sans + return nil } + var sans []string sans = append(sans, cert.DNSNames...) sans = append(sans, cert.EmailAddresses...) - var ips []string for _, ip := range cert.IPAddresses { - ips = append(ips, ip.String()) + sans = append(sans, ip.String()) } - sans = append(sans, ips...) - var uris []string for _, uri := range cert.URIs { - uris = append(uris, uri.String()) + sans = append(sans, uri.String()) } - return append(sans, uris...) + return sans } diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go index 8bf5b2e50..49497080e 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go @@ -15,6 +15,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/testhelpers" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -113,6 +114,7 @@ Cg+XKmHzexmTnKaKac2w9ZECpRsQ9IBdQq9OghIwPtOnERTOUJEEgNcqA+9xELjb pQ== -----END CERTIFICATE----- ` + minimalCheeseCrt = `-----BEGIN CERTIFICATE----- MIIEQDCCAygCFFRY0OBk/L5Se0IZRj3CMljawL2UMA0GCSqGSIb3DQEBCwUAMIIB hDETMBEGCgmSJomT8ixkARkWA29yZzEWMBQGCgmSJomT8ixkARkWBmNoZWVzZTEP @@ -262,47 +264,6 @@ jECvgAY7Nfd9mZ1KtyNaW31is+kag7NsvjxU/kM= -----END CERTIFICATE-----` ) -func getCleanCertContents(certContents []string) string { - var re = regexp.MustCompile("-----BEGIN CERTIFICATE-----(?s)(.*)") - - var cleanedCertContent []string - for _, certContent := range certContents { - cert := re.FindString(certContent) - cleanedCertContent = append(cleanedCertContent, sanitize([]byte(cert))) - } - - return strings.Join(cleanedCertContent, ",") -} - -func getCertificate(certContent string) *x509.Certificate { - roots := x509.NewCertPool() - ok := roots.AppendCertsFromPEM([]byte(signingCA)) - if !ok { - panic("failed to parse root certificate") - } - - block, _ := pem.Decode([]byte(certContent)) - if block == nil { - panic("failed to parse certificate PEM") - } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - panic("failed to parse certificate: " + err.Error()) - } - - return cert -} - -func buildTLSWith(certContents []string) *tls.ConnectionState { - var peerCertificates []*x509.Certificate - - for _, certContent := range certContents { - peerCertificates = append(peerCertificates, getCertificate(certContent)) - } - - return &tls.ConnectionState{PeerCertificates: peerCertificates} -} - var next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("bar")) if err != nil { @@ -310,59 +271,7 @@ var next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { } }) -func getExpectedSanitized(s string) string { - return url.QueryEscape(strings.Replace(s, "\n", "", -1)) -} - -func TestSanitize(t *testing.T) { - testCases := []struct { - desc string - toSanitize []byte - expected string - }{ - { - desc: "Empty", - }, - { - desc: "With a minimal cert", - toSanitize: []byte(minimalCheeseCrt), - expected: getExpectedSanitized(`MIIEQDCCAygCFFRY0OBk/L5Se0IZRj3CMljawL2UMA0GCSqGSIb3DQEBCwUAMIIB -hDETMBEGCgmSJomT8ixkARkWA29yZzEWMBQGCgmSJomT8ixkARkWBmNoZWVzZTEP -MA0GA1UECgwGQ2hlZXNlMREwDwYDVQQKDAhDaGVlc2UgMjEfMB0GA1UECwwWU2lt -cGxlIFNpZ25pbmcgU2VjdGlvbjEhMB8GA1UECwwYU2ltcGxlIFNpZ25pbmcgU2Vj -dGlvbiAyMRowGAYDVQQDDBFTaW1wbGUgU2lnbmluZyBDQTEcMBoGA1UEAwwTU2lt -cGxlIFNpZ25pbmcgQ0EgMjELMAkGA1UEBhMCRlIxCzAJBgNVBAYTAlVTMREwDwYD -VQQHDAhUT1VMT1VTRTENMAsGA1UEBwwETFlPTjEWMBQGA1UECAwNU2lnbmluZyBT -dGF0ZTEYMBYGA1UECAwPU2lnbmluZyBTdGF0ZSAyMSEwHwYJKoZIhvcNAQkBFhJz -aW1wbGVAc2lnbmluZy5jb20xIjAgBgkqhkiG9w0BCQEWE3NpbXBsZTJAc2lnbmlu -Zy5jb20wHhcNMTgxMjA2MTExMDM2WhcNMjEwOTI1MTExMDM2WjAzMQswCQYDVQQG -EwJGUjETMBEGA1UECAwKU29tZS1TdGF0ZTEPMA0GA1UECgwGQ2hlZXNlMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskX/bUtwFo1gF2BTPNaNcTUMaRFu -FMZozK8IgLjccZ4kZ0R9oFO6Yp8Zl/IvPaf7tE26PI7XP7eHriUdhnQzX7iioDd0 -RZa68waIhAGc+xPzRFrP3b3yj3S2a9Rve3c0K+SCV+EtKAwsxMqQDhoo9PcBfo5B -RHfht07uD5MncUcGirwN+/pxHV5xzAGPcc7On0/5L7bq/G+63nhu78zw9XyuLaHC -PM5VbOUvpyIESJHbMMzTdFGL8ob9VKO+Kr1kVGdEA9i8FLGl3xz/GBKuW/JD0xyW -DrU29mri5vYWHmkuv7ZWHGXnpXjTtPHwveE9/0/ArnmpMyR9JtqFr1oEvQIDAQAB -MA0GCSqGSIb3DQEBCwUAA4IBAQBHta+NWXI08UHeOkGzOTGRiWXsOH2dqdX6gTe9 -xF1AIjyoQ0gvpoGVvlnChSzmlUj+vnx/nOYGIt1poE3hZA3ZHZD/awsvGyp3GwWD -IfXrEViSCIyF+8tNNKYyUcEO3xdAsAUGgfUwwF/mZ6MBV5+A/ZEEILlTq8zFt9dV -vdKzIt7fZYxYBBHFSarl1x8pDgWXlf3hAufevGJXip9xGYmznF0T5cq1RbWJ4be3 -/9K7yuWhuBYC3sbTbCneHBa91M82za+PIISc1ygCYtWSBoZKSAqLk0rkZpHaekDP -WqeUSNGYV//RunTeuRDAf5OxehERb1srzBXhRZ3cZdzXbgR/`), - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - require.Equal(t, test.expected, sanitize(test.toSanitize), "The sanitized certificates should be equal") - }) - } -} - -func TestTLSClientHeadersWithPEM(t *testing.T) { +func TestPassTLSClientCert_PEM(t *testing.T) { testCases := []struct { desc string certContents []string // set the request TLS attribute if defined @@ -417,70 +326,36 @@ func TestTLSClientHeadersWithPEM(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") - require.Equal(t, "bar", res.Body.String(), "Should be the expected body") + assert.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") + assert.Equal(t, "bar", res.Body.String(), "Should be the expected body") if test.expectedHeader != "" { - require.Equal(t, getCleanCertContents(test.certContents), req.Header.Get(xForwardedTLSClientCert), "The request header should contain the cleaned certificate") + expected := getCleanCertContents(test.certContents) + assert.Equal(t, expected, req.Header.Get(xForwardedTLSClientCert), "The request header should contain the cleaned certificate") } else { - require.Empty(t, req.Header.Get(xForwardedTLSClientCert)) + assert.Empty(t, req.Header.Get(xForwardedTLSClientCert)) } - require.Empty(t, res.Header().Get(xForwardedTLSClientCert), "The response header should be always empty") + + assert.Empty(t, res.Header().Get(xForwardedTLSClientCert), "The response header should be always empty") }) } } -func TestGetSans(t *testing.T) { - urlFoo, err := url.Parse("my.foo.com") - require.NoError(t, err) - urlBar, err := url.Parse("my.bar.com") - require.NoError(t, err) +func TestPassTLSClientCert_certInfo(t *testing.T) { + minimalCheeseCertAllInfo := strings.Join([]string{ + `Subject="C=FR,ST=Some-State,O=Cheese"`, + `Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2"`, + `NB="1544094636"`, + `NA="1632568236"`, + }, fieldSeparator) - testCases := []struct { - desc string - cert *x509.Certificate // set the request TLS attribute if defined - expected []string - }{ - { - desc: "With nil", - }, - { - desc: "Certificate without Sans", - cert: &x509.Certificate{}, - }, - { - desc: "Certificate with all Sans", - cert: &x509.Certificate{ - DNSNames: []string{"foo", "bar"}, - EmailAddresses: []string{"test@test.com", "test2@test.com"}, - IPAddresses: []net.IP{net.IPv4(10, 0, 0, 1), net.IPv4(10, 0, 0, 2)}, - URIs: []*url.URL{urlFoo, urlBar}, - }, - expected: []string{"foo", "bar", "test@test.com", "test2@test.com", "10.0.0.1", "10.0.0.2", urlFoo.String(), urlBar.String()}, - }, - } - - for _, test := range testCases { - sans := getSANs(test.cert) - - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - if len(test.expected) > 0 { - for i, expected := range test.expected { - require.Equal(t, expected, sans[i]) - } - } else { - require.Empty(t, sans) - } - }) - } -} - -func TestTLSClientHeadersWithCertInfo(t *testing.T) { - minimalCheeseCertAllInfo := `Subject="C=FR,ST=Some-State,O=Cheese",Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2",NB=1544094636,NA=1632568236,SAN=` - completeCertAllInfo := `Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com",Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2",NB=1544094616,NA=1607166616,SAN=*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2` + completeCertAllInfo := strings.Join([]string{ + `Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com"`, + `Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2"`, + `NB="1544094616"`, + `NA="1607166616"`, + `SAN="*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2"`, + }, fieldSeparator) testCases := []struct { desc string @@ -547,7 +422,7 @@ func TestTLSClientHeadersWithCertInfo(t *testing.T) { }, }, }, - expectedHeader: url.QueryEscape(minimalCheeseCertAllInfo), + expectedHeader: minimalCheeseCertAllInfo, }, { desc: "TLS with simple certificate, with some info", @@ -564,7 +439,7 @@ func TestTLSClientHeadersWithCertInfo(t *testing.T) { }, }, }, - expectedHeader: url.QueryEscape(`Subject="O=Cheese",Issuer="C=FR,C=US",NA=1632568236,SAN=`), + expectedHeader: `Subject="O=Cheese";Issuer="C=FR,C=US";NA="1632568236"`, }, { desc: "TLS with complete certificate, with all info", @@ -594,7 +469,7 @@ func TestTLSClientHeadersWithCertInfo(t *testing.T) { }, }, }, - expectedHeader: url.QueryEscape(completeCertAllInfo), + expectedHeader: completeCertAllInfo, }, { desc: "TLS with 2 certificates, with all info", @@ -624,7 +499,7 @@ func TestTLSClientHeadersWithCertInfo(t *testing.T) { }, }, }, - expectedHeader: url.QueryEscape(strings.Join([]string{minimalCheeseCertAllInfo, completeCertAllInfo}, ";")), + expectedHeader: strings.Join([]string{minimalCheeseCertAllInfo, completeCertAllInfo}, certSeparator), }, } @@ -645,15 +520,157 @@ func TestTLSClientHeadersWithCertInfo(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") - require.Equal(t, "bar", res.Body.String(), "Should be the expected body") + assert.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") + assert.Equal(t, "bar", res.Body.String(), "Should be the expected body") if test.expectedHeader != "" { - require.Equal(t, test.expectedHeader, req.Header.Get(xForwardedTLSClientCertInfo), "The request header should contain the cleaned certificate") + unescape, err := url.QueryUnescape(req.Header.Get(xForwardedTLSClientCertInfo)) + require.NoError(t, err) + assert.Equal(t, test.expectedHeader, unescape, "The request header should contain the cleaned certificate") } else { - require.Empty(t, req.Header.Get(xForwardedTLSClientCertInfo)) + assert.Empty(t, req.Header.Get(xForwardedTLSClientCertInfo)) } - require.Empty(t, res.Header().Get(xForwardedTLSClientCertInfo), "The response header should be always empty") + + assert.Empty(t, res.Header().Get(xForwardedTLSClientCertInfo), "The response header should be always empty") }) } } + +func Test_sanitize(t *testing.T) { + testCases := []struct { + desc string + toSanitize []byte + expected string + }{ + { + desc: "Empty", + }, + { + desc: "With a minimal cert", + toSanitize: []byte(minimalCheeseCrt), + expected: `MIIEQDCCAygCFFRY0OBk/L5Se0IZRj3CMljawL2UMA0GCSqGSIb3DQEBCwUAMIIB +hDETMBEGCgmSJomT8ixkARkWA29yZzEWMBQGCgmSJomT8ixkARkWBmNoZWVzZTEP +MA0GA1UECgwGQ2hlZXNlMREwDwYDVQQKDAhDaGVlc2UgMjEfMB0GA1UECwwWU2lt +cGxlIFNpZ25pbmcgU2VjdGlvbjEhMB8GA1UECwwYU2ltcGxlIFNpZ25pbmcgU2Vj +dGlvbiAyMRowGAYDVQQDDBFTaW1wbGUgU2lnbmluZyBDQTEcMBoGA1UEAwwTU2lt +cGxlIFNpZ25pbmcgQ0EgMjELMAkGA1UEBhMCRlIxCzAJBgNVBAYTAlVTMREwDwYD +VQQHDAhUT1VMT1VTRTENMAsGA1UEBwwETFlPTjEWMBQGA1UECAwNU2lnbmluZyBT +dGF0ZTEYMBYGA1UECAwPU2lnbmluZyBTdGF0ZSAyMSEwHwYJKoZIhvcNAQkBFhJz +aW1wbGVAc2lnbmluZy5jb20xIjAgBgkqhkiG9w0BCQEWE3NpbXBsZTJAc2lnbmlu +Zy5jb20wHhcNMTgxMjA2MTExMDM2WhcNMjEwOTI1MTExMDM2WjAzMQswCQYDVQQG +EwJGUjETMBEGA1UECAwKU29tZS1TdGF0ZTEPMA0GA1UECgwGQ2hlZXNlMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskX/bUtwFo1gF2BTPNaNcTUMaRFu +FMZozK8IgLjccZ4kZ0R9oFO6Yp8Zl/IvPaf7tE26PI7XP7eHriUdhnQzX7iioDd0 +RZa68waIhAGc+xPzRFrP3b3yj3S2a9Rve3c0K+SCV+EtKAwsxMqQDhoo9PcBfo5B +RHfht07uD5MncUcGirwN+/pxHV5xzAGPcc7On0/5L7bq/G+63nhu78zw9XyuLaHC +PM5VbOUvpyIESJHbMMzTdFGL8ob9VKO+Kr1kVGdEA9i8FLGl3xz/GBKuW/JD0xyW +DrU29mri5vYWHmkuv7ZWHGXnpXjTtPHwveE9/0/ArnmpMyR9JtqFr1oEvQIDAQAB +MA0GCSqGSIb3DQEBCwUAA4IBAQBHta+NWXI08UHeOkGzOTGRiWXsOH2dqdX6gTe9 +xF1AIjyoQ0gvpoGVvlnChSzmlUj+vnx/nOYGIt1poE3hZA3ZHZD/awsvGyp3GwWD +IfXrEViSCIyF+8tNNKYyUcEO3xdAsAUGgfUwwF/mZ6MBV5+A/ZEEILlTq8zFt9dV +vdKzIt7fZYxYBBHFSarl1x8pDgWXlf3hAufevGJXip9xGYmznF0T5cq1RbWJ4be3 +/9K7yuWhuBYC3sbTbCneHBa91M82za+PIISc1ygCYtWSBoZKSAqLk0rkZpHaekDP +WqeUSNGYV//RunTeuRDAf5OxehERb1srzBXhRZ3cZdzXbgR/`, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + content := sanitize(test.toSanitize) + + expected := url.QueryEscape(strings.Replace(test.expected, "\n", "", -1)) + assert.Equal(t, expected, content, "The sanitized certificates should be equal") + }) + } +} + +func Test_getSANs(t *testing.T) { + urlFoo := testhelpers.MustParseURL("my.foo.com") + urlBar := testhelpers.MustParseURL("my.bar.com") + + testCases := []struct { + desc string + cert *x509.Certificate // set the request TLS attribute if defined + expected []string + }{ + { + desc: "With nil", + }, + { + desc: "Certificate without Sans", + cert: &x509.Certificate{}, + }, + { + desc: "Certificate with all Sans", + cert: &x509.Certificate{ + DNSNames: []string{"foo", "bar"}, + EmailAddresses: []string{"test@test.com", "test2@test.com"}, + IPAddresses: []net.IP{net.IPv4(10, 0, 0, 1), net.IPv4(10, 0, 0, 2)}, + URIs: []*url.URL{urlFoo, urlBar}, + }, + expected: []string{"foo", "bar", "test@test.com", "test2@test.com", "10.0.0.1", "10.0.0.2", urlFoo.String(), urlBar.String()}, + }, + } + + for _, test := range testCases { + sans := getSANs(test.cert) + + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + if len(test.expected) > 0 { + for i, expected := range test.expected { + assert.Equal(t, expected, sans[i]) + } + } else { + assert.Empty(t, sans) + } + }) + } +} + +func getCleanCertContents(certContents []string) string { + exp := regexp.MustCompile("-----BEGIN CERTIFICATE-----(?s)(.*)") + + var cleanedCertContent []string + for _, certContent := range certContents { + cert := sanitize([]byte(exp.FindString(certContent))) + cleanedCertContent = append(cleanedCertContent, cert) + } + + return strings.Join(cleanedCertContent, certSeparator) +} + +func buildTLSWith(certContents []string) *tls.ConnectionState { + var peerCertificates []*x509.Certificate + + for _, certContent := range certContents { + peerCertificates = append(peerCertificates, getCertificate(certContent)) + } + + return &tls.ConnectionState{PeerCertificates: peerCertificates} +} + +func getCertificate(certContent string) *x509.Certificate { + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM([]byte(signingCA)) + if !ok { + panic("failed to parse root certificate") + } + + block, _ := pem.Decode([]byte(certContent)) + if block == nil { + panic("failed to parse certificate PEM") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic("failed to parse certificate: " + err.Error()) + } + + return cert +} From e0f265db1549e0f84384b7aa374c788b616a2705 Mon Sep 17 00:00:00 2001 From: Eugen Mayer Date: Mon, 9 Dec 2019 12:32:04 +0100 Subject: [PATCH 09/13] Make trailing slash more prominent for the "secure dashboard setup" too --- docs/content/operations/dashboard.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/content/operations/dashboard.md b/docs/content/operations/dashboard.md index 46fbb9775..70b0637f9 100644 --- a/docs/content/operations/dashboard.md +++ b/docs/content/operations/dashboard.md @@ -85,19 +85,17 @@ We recommend to use a "Host Based rule" as ```Host(`traefik.domain.com`)``` to m or to make sure that the defined rule captures both prefixes: ```bash tab="Host Rule" -# Matches http://traefik.domain.com/api or http://traefik.domain.com/dashboard +# The dashboard can be accessed on http://traefik.domain.com/dashboard/ rule = "Host(`traefik.domain.com`)" ``` ```bash tab="Path Prefix Rule" -# Matches http://traefik.domain.com/api , http://domain.com/api or http://traefik.domain.com/dashboard -# but does not match http://traefik.domain.com/hello +# The dashboard can be accessed on http://domain.com/dashboard/ or http://traefik.domain.com/dashboard/ rule = "PathPrefix(`/api`) || PathPrefix(`/dashboard`)" ``` ```bash tab="Combination of Rules" -# Matches http://traefik.domain.com/api or http://traefik.domain.com/dashboard -# but does not match http://traefik.domain.com/hello +# The dashboard can be accessed on http://traefik.domain.com/dashboard/ rule = "Host(`traefik.domain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" ``` From d2e458f67346bc4d88a49db89d6850a7c7a2ab98 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Mon, 9 Dec 2019 15:12:06 +0100 Subject: [PATCH 10/13] Remove mirroring impact in accesslog --- pkg/server/service/loadbalancer/mirror/mirror.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/server/service/loadbalancer/mirror/mirror.go b/pkg/server/service/loadbalancer/mirror/mirror.go index f20b630ca..27d8e3230 100644 --- a/pkg/server/service/loadbalancer/mirror/mirror.go +++ b/pkg/server/service/loadbalancer/mirror/mirror.go @@ -8,6 +8,7 @@ import ( "net/http" "sync" + "github.com/containous/traefik/v2/pkg/middlewares/accesslog" "github.com/containous/traefik/v2/pkg/safe" ) @@ -63,11 +64,21 @@ func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if handler.count*100 < total*uint64(handler.percent) { handler.count++ handler.lock.Unlock() + + // In ServeHTTP, we rely on the presence of the accesslog datatable found in the + // request's context to know whether we should mutate said datatable (and + // contribute some fields to the log). In this instance, we do not want the mirrors + // mutating (i.e. changing the service name in) the logs related to the mirrored + // server. Especially since it would result in unguarded concurrent reads/writes on + // the datatable. Therefore, we reset any potential datatable key in the new + // context that we pass around. + ctx := context.WithValue(req.Context(), accesslog.DataTableKey, nil) + // When a request served by m.handler is successful, req.Context will be canceled, // which would trigger a cancellation of the ongoing mirrored requests. // Therefore, we give a new, non-cancellable context to each of the mirrored calls, // so they can terminate by themselves. - handler.ServeHTTP(m.rw, req.WithContext(contextStopPropagation{req.Context()})) + handler.ServeHTTP(m.rw, req.WithContext(contextStopPropagation{ctx})) } else { handler.lock.Unlock() } From c9dc0226fde94f8a041b77c5382553ec94891eb7 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 9 Dec 2019 15:52:04 +0100 Subject: [PATCH 11/13] fix: flaky Travis builds due to 'not get uid/gid' --- docs/check.Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/check.Dockerfile b/docs/check.Dockerfile index 79af5c3cf..4126bf2f7 100644 --- a/docs/check.Dockerfile +++ b/docs/check.Dockerfile @@ -15,8 +15,12 @@ RUN gem install html-proofer --version 3.13.0 --no-document -- --use-system-libr RUN apk --no-cache --no-progress add \ git \ nodejs \ - npm \ - && npm install --global \ + npm + +# To handle 'not get uid/gid' +RUN npm config set unsafe-perm true + +RUN npm install --global \ markdownlint@0.17.2 \ markdownlint-cli@0.19.0 From eef3ca0295950651f40e55d8baa0fa4c6bf17820 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Mon, 9 Dec 2019 18:08:04 +0100 Subject: [PATCH 12/13] Improve documentation for ACME/Let's Encrypt --- docs/content/https/.markdownlint.json | 4 + docs/content/https/acme.md | 67 +++++++++++++- .../include-acme-multiple-domains-example.md | 88 +++++++++++++++++++ ...acme-multiple-domains-from-rule-example.md | 72 +++++++++++++++ .../include-acme-single-domain-example.md | 72 +++++++++++++++ docs/content/index.md | 2 +- docs/content/routing/providers/marathon.md | 2 +- docs/mkdocs.yml | 2 +- 8 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 docs/content/https/.markdownlint.json create mode 100644 docs/content/https/include-acme-multiple-domains-example.md create mode 100644 docs/content/https/include-acme-multiple-domains-from-rule-example.md create mode 100644 docs/content/https/include-acme-single-domain-example.md diff --git a/docs/content/https/.markdownlint.json b/docs/content/https/.markdownlint.json new file mode 100644 index 000000000..3ad8a7f24 --- /dev/null +++ b/docs/content/https/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../.markdownlint.json", + "MD041": false +} diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index 7602bc244..ef476df82 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -8,6 +8,45 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom !!! warning "Let's Encrypt and Rate Limiting" Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). + Use Let's Encrypt staging server with the [`caServer`](#caserver) configuration option + when experimenting to avoid hitting this limit too fast. + +## Certificate Resolvers + +Traefik requires you to define "Certificate Resolvers" in the [static configuration](../getting-started/configuration-overview.md#the-static-configuration), +which are responsible for retrieving certificates from an ACME server. + +Then, each ["router"](../routing/routers/index.md) is configured to enable TLS, +and is associated to a certificate resolver through the [`tls.certresolver` configuration option](../routing/routers/index.md#certresolver). + +Certificates are requested for domain names retrieved from the router's [dynamic configuration](../getting-started/configuration-overview.md#the-dynamic-configuration). + +You can read more about this retrieval mechanism in the following section: [ACME Domain Definition](#domain-definition). + +## Domain Definition + +Certificate resolvers request certificates for a set of the domain names +inferred from routers, with the following logic: + +- If the router has a [`tls.domains`](../routing/routers/index.md#domains) option set, + then the certificate resolver uses the `main` (and optionally `sans`) option of `tls.domains` to know the domain names for this router. + +- If no [`tls.domains`](../routing/routers/index.md#domains) option is set, + then the certificate resolver uses the [router's rule](../routing/routers/index.md#rule), + by checking the `Host()` matchers. + Please note that [multiple `Host()` matchers can be used](../routing/routers/index.md#certresolver)) for specifying multiple domain names for this router. + +Please note that: + +- When multiple domain names are inferred from a given router, + only **one** certificate is requested with the first domain name as the main domain, + and the other domains as ["SANs" (Subject Alternative Name)](https://en.wikipedia.org/wiki/Subject_Alternative_Name). + +- As [ACME V2 supports "wildcard domains"](#wildcard-domains), + any router can provide a [wildcard domain](https://en.wikipedia.org/wiki/Wildcard_certificate) name, as "main" domain or as "SAN" domain. + +Please check the [configuration examples below](#configuration-examples) for more details. + ## Configuration Examples ??? example "Enabling ACME" @@ -75,6 +114,26 @@ You can configure Traefik to use an ACME provider (like Let's Encrypt) for autom --8<-- "content/https/ref-acme.txt" ``` +??? example "Single Domain from Router's Rule Example" + + * A certificate for the domain `company.com` is requested: + + --8<-- "content/https/include-acme-single-domain-example.md" + +??? example "Multiple Domains from Router's Rule Example" + + * A certificate for the domains `company.com` (main) and `blog.company.org` + is requested: + + --8<-- "content/https/include-acme-multiple-domains-from-rule-example.md" + +??? example "Multiple Domains from Router's `tls.domain` Example" + + * A certificate for the domains `company.com` (main) and `*.company.org` (SAN) + is requested: + + --8<-- "content/https/include-acme-multiple-domains-example.md" + ## Automatic Renewals Traefik automatically tracks the expiry date of ACME certificates it generates. @@ -327,7 +386,9 @@ certificatesResolvers: [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). -## `caServer` +## More Configuration + +### `caServer` ??? example "Using the Let's Encrypt staging server" @@ -353,7 +414,7 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi # ... ``` -## `storage` +### `storage` The `storage` option sets the location where your ACME certificates are saved to. @@ -383,7 +444,7 @@ The value can refer to some kinds of storage: - a JSON file -### In a File +#### In a File ACME certificates can be stored in a JSON file that needs to have a `600` file mode . diff --git a/docs/content/https/include-acme-multiple-domains-example.md b/docs/content/https/include-acme-multiple-domains-example.md new file mode 100644 index 000000000..2fe5b156c --- /dev/null +++ b/docs/content/https/include-acme-multiple-domains-example.md @@ -0,0 +1,88 @@ + +```yaml tab="Docker" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le + - traefik.http.routers.blog.tls.domains[0].main=company.org + - traefik.http.routers.blog.tls.domains[0].sans=*.company.org +``` + +```yaml tab="Docker (Swarm)" +## Dynamic configuration +deploy: + labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.services.blog-svc.loadbalancer.server.port=8080" + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le + - traefik.http.routers.blog.tls.domains[0].main=company.org + - traefik.http.routers.blog.tls.domains[0].sans=*.company.org +``` + +```yaml tab="Kubernetes" +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: blogtls +spec: + entryPoints: + - websecure + routes: + - match: Host(`company.com`) && Path(`/blog`) + kind: Rule + services: + - name: blog + port: 8080 + tls: + certResolver: le +``` + +```json tab="Marathon" +labels: { + "traefik.http.routers.blog.rule": "Host(`company.com`) && Path(`/blog`)", + "traefik.http.routers.blog.tls": "true", + "traefik.http.routers.blog.tls.certresolver": "le", + "traefik.http.routers.blog.tls.domains[0].main": "company.com", + "traefik.http.routers.blog.tls.domains[0].sans": "*.company.com", + "traefik.http.services.blog-svc.loadbalancer.server.port": "8080" +} +``` + +```yaml tab="Rancher" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le + - traefik.http.routers.blog.tls.domains[0].main=company.org + - traefik.http.routers.blog.tls.domains[0].sans=*.company.org +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.routers] + [http.routers.blog] + rule = "Host(`company.com`) && Path(`/blog`)" + [http.routers.blog.tls] + certResolver = "le" # From static configuration + [[http.routers.blog.tls.domains]] + main = "company.org" + sans = ["*.company.org"] +``` + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + routers: + blog: + rule: "Host(`company.com`) && Path(`/blog`)" + tls: + certResolver: le + domains: + - main: "company.org" + sans: + - "*.company.org" +``` diff --git a/docs/content/https/include-acme-multiple-domains-from-rule-example.md b/docs/content/https/include-acme-multiple-domains-from-rule-example.md new file mode 100644 index 000000000..f82cb8e0f --- /dev/null +++ b/docs/content/https/include-acme-multiple-domains-from-rule-example.md @@ -0,0 +1,72 @@ + +```yaml tab="Docker" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```yaml tab="Docker (Swarm)" +## Dynamic configuration +deploy: + labels: + - traefik.http.routers.blog.rule=(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`) + - traefik.http.services.blog-svc.loadbalancer.server.port=8080" + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```yaml tab="Kubernetes" +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: blogtls +spec: + entryPoints: + - websecure + routes: + - match: (Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`) + kind: Rule + services: + - name: blog + port: 8080 + tls: {} +``` + +```json tab="Marathon" +labels: { + "traefik.http.routers.blog.rule": "(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`)", + "traefik.http.routers.blog.tls": "true", + "traefik.http.routers.blog.tls.certresolver": "le", + "traefik.http.services.blog-svc.loadbalancer.server.port": "8080" +} +``` + +```yaml tab="Rancher" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.routers] + [http.routers.blog] + rule = "(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`)" + [http.routers.blog.tls] + certResolver = "le" # From static configuration +``` + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + routers: + blog: + rule: "(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`)" + tls: + certResolver: le +``` diff --git a/docs/content/https/include-acme-single-domain-example.md b/docs/content/https/include-acme-single-domain-example.md new file mode 100644 index 000000000..f8e087b31 --- /dev/null +++ b/docs/content/https/include-acme-single-domain-example.md @@ -0,0 +1,72 @@ + +```yaml tab="Docker" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```yaml tab="Docker (Swarm)" +## Dynamic configuration +deploy: + labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.services.blog-svc.loadbalancer.server.port=8080" + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```yaml tab="Kubernetes" +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: blogtls +spec: + entryPoints: + - websecure + routes: + - match: Host(`company.com`) && Path(`/blog`) + kind: Rule + services: + - name: blog + port: 8080 + tls: {} +``` + +```json tab="Marathon" +labels: { + "traefik.http.routers.blog.rule": "Host(`company.com`) && Path(`/blog`)", + "traefik.http.routers.blog.tls": "true", + "traefik.http.routers.blog.tls.certresolver": "le", + "traefik.http.services.blog-svc.loadbalancer.server.port": "8080" +} +``` + +```yaml tab="Rancher" +## Dynamic configuration +labels: + - traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`) + - traefik.http.routers.blog.tls=true + - traefik.http.routers.blog.tls.certresolver=le +``` + +```toml tab="Single Domain" +## Dynamic configuration +[http.routers] + [http.routers.blog] + rule = "Host(`company.com`) && Path(`/blog`)" + [http.routers.blog.tls] + certResolver = "le" # From static configuration +``` + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + routers: + blog: + rule: "Host(`company.com`) && Path(`/blog`)" + tls: + certResolver: le +``` diff --git a/docs/content/index.md b/docs/content/index.md index 6f7cdb268..b10303154 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -20,4 +20,4 @@ Developing Traefik, our main goal is to make it simple to use, and we're sure yo !!! info - If you're a business running critical services behind Traefik, know that [Containous](https://containo.us), the company that sponsors Traefik's development, can provide [commercial support](https://containo.us/services/#commercial-support) and develops an [Enterprise Edition](https://containo.us/traefikee/) of Traefik. + If you're a business running critical services behind Traefik, know that [Containous](https://containo.us), the company that sponsors Traefik's development, can provide [commercial support](https://info.containo.us/commercial-services) and develops an [Enterprise Edition](https://containo.us/traefikee/) of Traefik. diff --git a/docs/content/routing/providers/marathon.md b/docs/content/routing/providers/marathon.md index b272b42a0..7bc79a6b7 100644 --- a/docs/content/routing/providers/marathon.md +++ b/docs/content/routing/providers/marathon.md @@ -91,7 +91,7 @@ For example, to change the routing rule, you could add the label ```"traefik.htt See [tls](../routers/index.md#tls) for more information. ```json - "traefik.http.routers.myrouter>.tls": "true" + "traefik.http.routers.myrouter.tls": "true" ``` ??? info "`traefik.http.routers..tls.certresolver`" diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6e30c5f20..46622f894 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -44,7 +44,7 @@ plugins: - search - exclude: glob: - - include-*.md + - "**/include-*.md" # https://squidfunk.github.io/mkdocs-material/extensions/admonition/ # https://facelessuser.github.io/pymdown-extensions/ From fb3839e09602895615fefc3934090cb013dea4f5 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 9 Dec 2019 18:34:04 +0100 Subject: [PATCH 13/13] Prepare release v2.0.7 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d362fdda..8394e69a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [v2.0.7](https://github.com/containous/traefik/tree/v2.0.7) (2019-12-09) +[All Commits](https://github.com/containous/traefik/compare/v2.0.6...v2.0.7) + +**Bug fixes:** +- **[logs,middleware]** Remove mirroring impact in accesslog ([#5967](https://github.com/containous/traefik/pull/5967) by [juliens](https://github.com/juliens)) +- **[middleware]** fix: PassClientTLSCert middleware separators and formatting ([#5921](https://github.com/containous/traefik/pull/5921) by [ldez](https://github.com/ldez)) +- **[server]** Do not stop to listen on tcp listeners on temporary errors ([#5935](https://github.com/containous/traefik/pull/5935) by [skwair](https://github.com/skwair)) + +**Documentation:** +- **[acme,k8s/crd,k8s/ingress]** Document LE caveats with Kubernetes on v2 ([#5902](https://github.com/containous/traefik/pull/5902) by [dtomcej](https://github.com/dtomcej)) +- **[acme]** The Cloudflare hint for the GLOBAL API KEY for CF MAIL/API_KEY ([#5964](https://github.com/containous/traefik/pull/5964) by [EugenMayer](https://github.com/EugenMayer)) +- **[acme]** Improve documentation for ACME/Let's Encrypt ([#5819](https://github.com/containous/traefik/pull/5819) by [dduportal](https://github.com/dduportal)) +- **[file]** Improve documentation on file provider limitations with file system notifications ([#5939](https://github.com/containous/traefik/pull/5939) by [jbdoumenjou](https://github.com/jbdoumenjou)) +- Make trailing slash more prominent for the "secure dashboard setup" too ([#5963](https://github.com/containous/traefik/pull/5963) by [EugenMayer](https://github.com/EugenMayer)) +- Fix Docker example in "Strip and Rewrite Path Prefixes" in migration guide ([#5949](https://github.com/containous/traefik/pull/5949) by [q210](https://github.com/q210)) +- readme: Fix link to file backend/provider documentation ([#5945](https://github.com/containous/traefik/pull/5945) by [hartwork](https://github.com/hartwork)) + ## [v2.0.6](https://github.com/containous/traefik/tree/v2.0.6) (2019-12-02) [All Commits](https://github.com/containous/traefik/compare/v2.0.5...v2.0.6)