diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 94f89bedd..5c90b5689 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,11 +3,11 @@ PLEASE READ THIS MESSAGE. Documentation fixes or enhancements: - for Traefik v1: use branch v1.7 -- for Traefik v2: use branch v2.0 +- for Traefik v2: use branch v2.1 Bug fixes: - for Traefik v1: use branch v1.7 -- for Traefik v2: use branch v2.0 +- for Traefik v2: use branch v2.1 Enhancements: - for Traefik v1: we only accept bug fixes diff --git a/CHANGELOG.md b/CHANGELOG.md index ba47dce71..d197bdafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +## [v2.1.0](https://github.com/containous/traefik/tree/v2.1.0) (2019-12-10) +[All Commits](https://github.com/containous/traefik/compare/v2.0.0-rc1...v2.1.0) + +**Enhancements:** +- **[consulcatalog]** Add consul catalog options: requireConsistent, stale, cache ([#5752](https://github.com/containous/traefik/pull/5752) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** Add Consul Catalog provider ([#5395](https://github.com/containous/traefik/pull/5395) by [negasus](https://github.com/negasus)) +- **[k8s,k8s/crd,service]** Support for all services kinds (and sticky) in CRD ([#5711](https://github.com/containous/traefik/pull/5711) by [mpl](https://github.com/mpl)) +- **[metrics]** Added configurable prefix for statsd metrics collection ([#5336](https://github.com/containous/traefik/pull/5336) by [schulterklopfer](https://github.com/schulterklopfer)) +- **[middleware]** Conditional compression based on request Content-Type ([#5721](https://github.com/containous/traefik/pull/5721) by [ldez](https://github.com/ldez)) +- **[server]** Add internal provider ([#5815](https://github.com/containous/traefik/pull/5815) by [ldez](https://github.com/ldez)) +- **[tls]** Add support for MaxVersion in tls.Options ([#5650](https://github.com/containous/traefik/pull/5650) by [kmeekva](https://github.com/kmeekva)) +- **[tls]** Add tls option for Elliptic Curve Preferences ([#5466](https://github.com/containous/traefik/pull/5466) by [ksarink](https://github.com/ksarink)) +- **[tracing]** Update jaeger dependencies ([#5637](https://github.com/containous/traefik/pull/5637) by [mmatur](https://github.com/mmatur)) + +**Bug fixes:** +- **[api]** fix: debug endpoint when insecure API. ([#5937](https://github.com/containous/traefik/pull/5937) by [ldez](https://github.com/ldez)) +- **[cli]** fix: sub command help ([#5887](https://github.com/containous/traefik/pull/5887) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** fix: consul catalog constraints. ([#5913](https://github.com/containous/traefik/pull/5913) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** Service registered with same id on Consul Catalog ([#5900](https://github.com/containous/traefik/pull/5900) by [mmatur](https://github.com/mmatur)) +- **[consulcatalog]** Fix empty address for registering service without IP ([#5826](https://github.com/containous/traefik/pull/5826) by [mmatur](https://github.com/mmatur)) +- **[logs,middleware,metrics]** detect CloseNotify capability in accesslog and metrics ([#5985](https://github.com/containous/traefik/pull/5985) by [mpl](https://github.com/mpl)) +- **[server]** fix: remove double call to server Close. ([#5960](https://github.com/containous/traefik/pull/5960) by [ldez](https://github.com/ldez)) +- **[webui]** Fix weighted service provider icon ([#5983](https://github.com/containous/traefik/pull/5983) by [sh7dm](https://github.com/sh7dm)) +- **[webui]** Fix http/tcp resources pagination ([#5986](https://github.com/containous/traefik/pull/5986) by [matthieuh](https://github.com/matthieuh)) +- **[webui]** Use valid condition in the service details panel UI ([#5984](https://github.com/containous/traefik/pull/5984) by [jbdoumenjou](https://github.com/jbdoumenjou)) +- **[webui]** Web UI: Avoid polling on /api/entrypoints ([#5863](https://github.com/containous/traefik/pull/5863) by [matthieuh](https://github.com/matthieuh)) +- **[webui]** Web UI: Sync toolbar table state with url query params ([#5861](https://github.com/containous/traefik/pull/5861) by [matthieuh](https://github.com/matthieuh)) + +**Documentation:** +- **[consulcatalog]** fix: Consul Catalog documentation. ([#5725](https://github.com/containous/traefik/pull/5725) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** Fix consul catalog documentation ([#5661](https://github.com/containous/traefik/pull/5661) by [mmatur](https://github.com/mmatur)) +- Prepare release v2.1.0-rc2 ([#5846](https://github.com/containous/traefik/pull/5846) by [ldez](https://github.com/ldez)) +- Prepare release v2.1.0-rc1 ([#5844](https://github.com/containous/traefik/pull/5844) by [jbdoumenjou](https://github.com/jbdoumenjou)) +- Several documentation fixes ([#5987](https://github.com/containous/traefik/pull/5987) by [ldez](https://github.com/ldez)) +- Prepare release v2.1.0-rc3 ([#5929](https://github.com/containous/traefik/pull/5929) by [ldez](https://github.com/ldez)) + +**Misc:** +- **[cli]** Add custom help function to command ([#5923](https://github.com/containous/traefik/pull/5923) by [Ullaakut](https://github.com/Ullaakut)) +- **[server]** fix: use MaxInt32. ([#5845](https://github.com/containous/traefik/pull/5845) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into master ([#5841](https://github.com/containous/traefik/pull/5841) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into master ([#5749](https://github.com/containous/traefik/pull/5749) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into master ([#5619](https://github.com/containous/traefik/pull/5619) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into master ([#5464](https://github.com/containous/traefik/pull/5464) by [ldez](https://github.com/ldez)) +- Merge v2.0.0 into master ([#5402](https://github.com/containous/traefik/pull/5402) by [ldez](https://github.com/ldez)) +- Merge v2.0.0-rc3 into master ([#5354](https://github.com/containous/traefik/pull/5354) by [ldez](https://github.com/ldez)) +- Merge v2.0.0-rc1 into master ([#5253](https://github.com/containous/traefik/pull/5253) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into v2.1 ([#5977](https://github.com/containous/traefik/pull/5977) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into v2.1 ([#5931](https://github.com/containous/traefik/pull/5931) by [ldez](https://github.com/ldez)) +- Merge current v2.0 branch into v2.1 ([#5928](https://github.com/containous/traefik/pull/5928) by [ldez](https://github.com/ldez)) + +## [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/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 542766495..b773a0917 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -69,10 +69,10 @@ Complete documentation is available at https://traefik.io`, err = cli.Execute(cmdTraefik) if err != nil { stdlog.Println(err) - os.Exit(1) + logrus.Exit(1) } - os.Exit(0) + logrus.Exit(0) } func runCmd(staticConfiguration *static.Configuration) error { @@ -156,7 +156,6 @@ func runCmd(staticConfiguration *static.Configuration) error { svr.Wait() log.WithoutContext().Info("Shutting down") - logrus.Exit(0) return nil } 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/operations/ping.md b/docs/content/operations/ping.md index 0e5abd9d4..1183dbd17 100644 --- a/docs/content/operations/ping.md +++ b/docs/content/operations/ping.md @@ -59,7 +59,7 @@ ping: --ping.entryPoint=ping ``` -#### `manualRouting` +### `manualRouting` _Optional, Default=false_ 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/routing/services/index.md b/docs/content/routing/services/index.md index f3e03ebda..eff981637 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -387,7 +387,9 @@ The WRR is able to load balance the requests between multiple services based on This strategy is only available to load balance between [services](./index.md) and not between [servers](./index.md#servers). -!!! info "This strategy can be defined only with [File](../../providers/file.md)." +!!! info "Supported Providers" + + This strategy can be defined currently with the [File](../../providers/file.md) or [IngressRoute](../../providers/kubernetes-crd.md) providers. ```toml tab="TOML" ## Dynamic configuration @@ -438,7 +440,9 @@ http: The mirroring is able to mirror requests sent to a service to other services. -!!! info "This strategy can be defined only with [File](../../providers/file.md)." +!!! info "Supported Providers" + + This strategy can be defined currently with the [File](../../providers/file.md) or [IngressRoute](../../providers/kubernetes-crd.md) providers. ```toml tab="TOML" ## Dynamic configuration @@ -583,7 +587,9 @@ The Weighted Round Robin (alias `WRR`) load-balancer of services is in charge of This strategy is only available to load balance between [services](./index.md) and not between [servers](./index.md#servers). -This strategy can only be defined with [File](../../providers/file.md). +!!! info "Supported Providers" + + This strategy can be defined currently with the [File](../../providers/file.md) or [IngressRoute](../../providers/kubernetes-crd.md) providers. ```toml tab="TOML" ## 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 ## diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 601c33e7d..f4aade025 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/accesslog/capture_response_writer.go b/pkg/middlewares/accesslog/capture_response_writer.go index 094b5c922..da4991a72 100644 --- a/pkg/middlewares/accesslog/capture_response_writer.go +++ b/pkg/middlewares/accesslog/capture_response_writer.go @@ -13,6 +13,20 @@ var ( _ middlewares.Stateful = &captureResponseWriter{} ) +type capturer interface { + http.ResponseWriter + Size() int64 + Status() int +} + +func newCaptureResponseWriter(rw http.ResponseWriter) capturer { + capt := &captureResponseWriter{rw: rw} + if _, ok := rw.(http.CloseNotifier); !ok { + return capt + } + return captureResponseWriterWithCloseNotify{capt} +} + // captureResponseWriter is a wrapper of type http.ResponseWriter // that tracks request status and size type captureResponseWriter struct { @@ -21,6 +35,16 @@ type captureResponseWriter struct { size int64 } +type captureResponseWriterWithCloseNotify struct { + *captureResponseWriter +} + +// CloseNotify returns a channel that receives at most a +// single value (true) when the client connection has gone away. +func (r *captureResponseWriterWithCloseNotify) CloseNotify() <-chan bool { + return r.rw.(http.CloseNotifier).CloseNotify() +} + func (crw *captureResponseWriter) Header() http.Header { return crw.rw.Header() } diff --git a/pkg/middlewares/accesslog/field_middleware.go b/pkg/middlewares/accesslog/field_middleware.go index 19b455939..f201ba53a 100644 --- a/pkg/middlewares/accesslog/field_middleware.go +++ b/pkg/middlewares/accesslog/field_middleware.go @@ -49,7 +49,7 @@ func AddServiceFields(rw http.ResponseWriter, req *http.Request, next http.Handl // AddOriginFields add origin fields func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) { - crw := &captureResponseWriter{rw: rw} + crw := newCaptureResponseWriter(rw) start := time.Now().UTC() next.ServeHTTP(crw, req) diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 5f9a3c9f9..8114d9319 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -200,7 +200,7 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http core[ClientHost] = forwardedFor } - crw := &captureResponseWriter{rw: rw} + crw := newCaptureResponseWriter(rw) next.ServeHTTP(crw, reqWithDataTable) diff --git a/pkg/middlewares/metrics/metrics.go b/pkg/middlewares/metrics/metrics.go index b5f36640a..34d9dccc8 100644 --- a/pkg/middlewares/metrics/metrics.go +++ b/pkg/middlewares/metrics/metrics.go @@ -89,10 +89,10 @@ func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) }(labels) start := time.Now() - recorder := &responseRecorder{rw, http.StatusOK} + recorder := newResponseRecorder(rw) m.next.ServeHTTP(recorder, req) - labels = append(labels, "code", strconv.Itoa(recorder.statusCode)) + labels = append(labels, "code", strconv.Itoa(recorder.getCode())) m.reqsCounter.With(labels...).Add(1) m.reqDurationHistogram.With(labels...).Observe(time.Since(start).Seconds()) } diff --git a/pkg/middlewares/metrics/recorder.go b/pkg/middlewares/metrics/recorder.go index 7ada2d988..4206558c7 100644 --- a/pkg/middlewares/metrics/recorder.go +++ b/pkg/middlewares/metrics/recorder.go @@ -6,6 +6,23 @@ import ( "net/http" ) +type recorder interface { + http.ResponseWriter + http.Flusher + getCode() int +} + +func newResponseRecorder(rw http.ResponseWriter) recorder { + rec := &responseRecorder{ + ResponseWriter: rw, + statusCode: http.StatusOK, + } + if _, ok := rw.(http.CloseNotifier); !ok { + return rec + } + return responseRecorderWithCloseNotify{rec} +} + // responseRecorder captures information from the response and preserves it for // later analysis. type responseRecorder struct { @@ -13,6 +30,20 @@ type responseRecorder struct { statusCode int } +type responseRecorderWithCloseNotify struct { + *responseRecorder +} + +// CloseNotify returns a channel that receives at most a +// single value (true) when the client connection has gone away. +func (r *responseRecorderWithCloseNotify) CloseNotify() <-chan bool { + return r.ResponseWriter.(http.CloseNotifier).CloseNotify() +} + +func (r *responseRecorder) getCode() int { + return r.statusCode +} + // WriteHeader captures the status code for later retrieval. func (r *responseRecorder) WriteHeader(status int) { r.ResponseWriter.WriteHeader(status) 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/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json index 3544aa39c..e8c3a9ec6 100644 --- a/pkg/provider/traefik/fixtures/full_configuration.json +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -21,6 +21,14 @@ "rule": "PathPrefix(`/`)", "priority": 2147483645 }, + "debug": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/debug`)", + "priority": 2147483646 + }, "ping": { "entryPoints": [ "test" diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 1ee282cee..1501b29a0 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -95,6 +95,15 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { StripPrefix: &dynamic.StripPrefix{Prefixes: []string{"/dashboard/", "/dashboard"}}, } } + + if i.staticCfg.API.Debug { + cfg.HTTP.Routers["debug"] = &dynamic.Router{ + EntryPoints: []string{"traefik"}, + Service: "api@internal", + Priority: math.MaxInt32 - 1, + Rule: "PathPrefix(`/debug`)", + } + } } cfg.HTTP.Services["api"] = &dynamic.Service{} diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index 9d1edbc5e..6e43a531f 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -28,6 +28,7 @@ func Test_createConfiguration(t *testing.T) { API: &static.API{ Insecure: true, Dashboard: true, + Debug: true, }, Ping: &ping.Handler{ EntryPoint: "test", diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index e593ae9cd..06b07aedd 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -49,8 +49,8 @@ func NewConfigurationWatcher(routinesPool *safe.Pool, pvd provider.Provider, pro // Start the configuration watcher. func (c *ConfigurationWatcher) Start() { - c.routinesPool.Go(func(stop chan bool) { c.listenProviders(stop) }) - c.routinesPool.Go(func(stop chan bool) { c.listenConfigurations(stop) }) + c.routinesPool.Go(c.listenProviders) + c.routinesPool.Go(c.listenConfigurations) c.startProvider() } diff --git a/pkg/server/server.go b/pkg/server/server.go index 90fb8e82b..7e395a759 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -48,7 +48,6 @@ func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, watcher *Con // Start starts the server and Stop/Close it when context is Done func (s *Server) Start(ctx context.Context) { go func() { - defer s.Close() <-ctx.Done() logger := log.FromContext(ctx) logger.Info("I have to go...") @@ -59,9 +58,7 @@ func (s *Server) Start(ctx context.Context) { s.tcpEntryPoints.Start() s.watcher.Start() - s.routinesPool.Go(func(stop chan bool) { - s.listenSignals(stop) - }) + s.routinesPool.Go(s.listenSignals) } // Wait blocks until the server shutdown. 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 diff --git a/webui/src/_services/HttpService.js b/webui/src/_services/HttpService.js index 48010c1e0..0e206b24d 100644 --- a/webui/src/_services/HttpService.js +++ b/webui/src/_services/HttpService.js @@ -1,14 +1,15 @@ import { APP } from '../_helpers/APP' +import { getTotal } from './utils' const apiBase = '/http' function getAllRouters (params) { return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) - .then(body => { - const total = body.data ? body.data.length : 0 - console.log('Success -> HttpService -> getAllRouters', body.data) - // TODO - suggestion: add the total-pages in api response to optimize the query - return { data: body.data || [], total } + .then(response => { + const { data = [], headers } = response + const total = getTotal(headers, params) + console.log('Success -> HttpService -> getAllRouters', response, response.data) + return { data, total } }) } @@ -22,11 +23,11 @@ function getRouterByName (name) { function getAllServices (params) { return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) - .then(body => { - const total = body.data ? body.data.length : 0 - console.log('Success -> HttpService -> getAllServices', body.data) - // TODO - suggestion: add the total-pages in api response to optimize the query - return { data: body.data || [], total } + .then(response => { + const { data = [], headers } = response + const total = getTotal(headers, params) + console.log('Success -> HttpService -> getAllServices', response.data) + return { data, total } }) } @@ -40,11 +41,11 @@ function getServiceByName (name) { function getAllMiddlewares (params) { return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) - .then(body => { - const total = body.data ? body.data.length : 0 - console.log('Success -> HttpService -> getAllMiddlewares', body.data) - // TODO - suggestion: add the total-pages in api response to optimize the query - return { data: body.data || [], total } + .then(response => { + const { data = [], headers } = response + const total = getTotal(headers, params) + console.log('Success -> HttpService -> getAllMiddlewares', response.data) + return { data, total } }) } diff --git a/webui/src/_services/TcpService.js b/webui/src/_services/TcpService.js index 694ce099c..41c7d3dfa 100644 --- a/webui/src/_services/TcpService.js +++ b/webui/src/_services/TcpService.js @@ -1,14 +1,15 @@ import { APP } from '../_helpers/APP' +import { getTotal } from './utils' const apiBase = '/tcp' function getAllRouters (params) { return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) - .then(body => { - const total = body.data ? body.data.length : 0 - console.log('Success -> HttpService -> getAllRouters', body.data) - // TODO - suggestion: add the total-pages in api response to optimize the query - return { data: body.data || [], total } + .then(response => { + const { data = [], headers } = response + const total = getTotal(headers, params) + console.log('Success -> HttpService -> getAllRouters', response.data) + return { data, total } }) } @@ -22,11 +23,11 @@ function getRouterByName (name) { function getAllServices (params) { return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) - .then(body => { - const total = body.data ? body.data.length : 0 - console.log('Success -> HttpService -> getAllServices', body.data) - // TODO - suggestion: add the total-pages in api response to optimize the query - return { data: body.data || [], total } + .then(response => { + const { data = [], headers } = response + const total = getTotal(headers, params) + console.log('Success -> HttpService -> getAllServices', response.data) + return { data, total } }) } diff --git a/webui/src/_services/utils.js b/webui/src/_services/utils.js new file mode 100644 index 000000000..1f310c15d --- /dev/null +++ b/webui/src/_services/utils.js @@ -0,0 +1,8 @@ +export const getTotal = (headers, params) => { + const nextPage = parseInt(headers['x-next-page'], 10) || 1 + const hasNextPage = nextPage > 1 + + return hasNextPage + ? (params.page + 1) * params.limit + : params.page * params.limit +} diff --git a/webui/src/components/_commons/PanelMirroringServices.vue b/webui/src/components/_commons/PanelMirroringServices.vue index defa0eb74..f9b505408 100644 --- a/webui/src/components/_commons/PanelMirroringServices.vue +++ b/webui/src/components/_commons/PanelMirroringServices.vue @@ -55,11 +55,11 @@ export default { methods: { getProvider (service) { const words = service.name.split('@') - if (words.length !== 2) { - return this.provider + if (words.length === 2) { + return words[1] } - return words[1] + return this.data.provider } } } diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index 37e8dfe06..d1aa57bbe 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -54,7 +54,7 @@ - +
Termination Delay
diff --git a/webui/src/components/_commons/PanelWeightedServices.vue b/webui/src/components/_commons/PanelWeightedServices.vue index fb9147913..6cf5784c0 100644 --- a/webui/src/components/_commons/PanelWeightedServices.vue +++ b/webui/src/components/_commons/PanelWeightedServices.vue @@ -55,11 +55,11 @@ export default { methods: { getProvider (service) { const words = service.name.split('@') - if (words.length !== 2) { - return this.provider + if (words.length === 2) { + return words[1] } - return words[1] + return this.data.provider } } }