diff --git a/CHANGELOG.md b/CHANGELOG.md index ba47dce71..1315e7772 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.1.0-rc3](https://github.com/containous/traefik/tree/v2.1.0-rc3) (2019-12-02) [All Commits](https://github.com/containous/traefik/compare/v2.1.0-rc2...v2.1.0-rc3) 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 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 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 d23cf4317..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. @@ -84,6 +143,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." @@ -220,7 +286,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) | @@ -320,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" @@ -346,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. @@ -376,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/middlewares/passtlsclientcert.md b/docs/content/middlewares/passtlsclientcert.md index 845be967a..b483adced 100644 --- a/docs/content/middlewares/passtlsclientcert.md +++ b/docs/content/middlewares/passtlsclientcert.md @@ -406,7 +406,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" @@ -421,12 +421,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` @@ -442,7 +442,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` @@ -459,7 +459,7 @@ Validity The escape `notBefore` info part will be like: ```text -NB=1544094616 +NB="1544094616" ``` #### `info.sans` @@ -476,7 +476,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/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 07d1c3dad..a2968ce00 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" @@ -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/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`))" ``` 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/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` 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/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/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 ## diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 50cd3f807..24d242542 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/ 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 +} diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index cac8d0aa4..e67b5b200 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -158,6 +158,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/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() } diff --git a/pkg/tls/certificate.go b/pkg/tls/certificate.go index a7d6d4f27..81624ed6c 100644 --- a/pkg/tls/certificate.go +++ b/pkg/tls/certificate.go @@ -102,7 +102,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