Merge branch 'v2.1' into master

This commit is contained in:
Fernandez Ludovic 2019-12-11 22:14:26 +01:00
commit 2d3fc613ec
44 changed files with 923 additions and 392 deletions

View file

@ -3,11 +3,11 @@ PLEASE READ THIS MESSAGE.
Documentation fixes or enhancements: Documentation fixes or enhancements:
- for Traefik v1: use branch v1.7 - for Traefik v1: use branch v1.7
- for Traefik v2: use branch v2.0 - for Traefik v2: use branch v2.1
Bug fixes: Bug fixes:
- for Traefik v1: use branch v1.7 - for Traefik v1: use branch v1.7
- for Traefik v2: use branch v2.0 - for Traefik v2: use branch v2.1
Enhancements: Enhancements:
- for Traefik v1: we only accept bug fixes - for Traefik v1: we only accept bug fixes

View file

@ -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) ## [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) [All Commits](https://github.com/containous/traefik/compare/v2.1.0-rc2...v2.1.0-rc3)

View file

@ -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/) - [Kubernetes](https://docs.traefik.io/providers/kubernetes-crd/)
- [Marathon](https://docs.traefik.io/providers/marathon/) - [Marathon](https://docs.traefik.io/providers/marathon/)
- [Rancher](https://docs.traefik.io/providers/rancher/) (Metadata) - [Rancher](https://docs.traefik.io/providers/rancher/) (Metadata)
- [File](https://docs.traefik.io/configuration/backends/file) - [File](https://docs.traefik.io/providers/file/)
## Quickstart ## Quickstart

View file

@ -69,10 +69,10 @@ Complete documentation is available at https://traefik.io`,
err = cli.Execute(cmdTraefik) err = cli.Execute(cmdTraefik)
if err != nil { if err != nil {
stdlog.Println(err) stdlog.Println(err)
os.Exit(1) logrus.Exit(1)
} }
os.Exit(0) logrus.Exit(0)
} }
func runCmd(staticConfiguration *static.Configuration) error { func runCmd(staticConfiguration *static.Configuration) error {
@ -156,7 +156,6 @@ func runCmd(staticConfiguration *static.Configuration) error {
svr.Wait() svr.Wait()
log.WithoutContext().Info("Shutting down") log.WithoutContext().Info("Shutting down")
logrus.Exit(0)
return nil return nil
} }

View file

@ -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 \ RUN apk --no-cache --no-progress add \
git \ git \
nodejs \ nodejs \
npm \ npm
&& npm install --global \
# To handle 'not get uid/gid'
RUN npm config set unsafe-perm true
RUN npm install --global \
markdownlint@0.17.2 \ markdownlint@0.17.2 \
markdownlint-cli@0.19.0 markdownlint-cli@0.19.0

View file

@ -0,0 +1,4 @@
{
"extends": "../../.markdownlint.json",
"MD041": false
}

View file

@ -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" !!! warning "Let's Encrypt and Rate Limiting"
Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). 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 ## Configuration Examples
??? example "Enabling ACME" ??? 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" --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 ## Automatic Renewals
Traefik automatically tracks the expiry date of ACME certificates it generates. 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 "" !!! 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. 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 ## 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." !!! 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) | | [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) | | [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) | | [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) | | [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) | | [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) | | [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. [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). 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" ??? 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. 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 - 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 . ACME certificates can be stored in a JSON file that needs to have a `600` file mode .

View file

@ -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"
```

View file

@ -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
```

View file

@ -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
```

View file

@ -20,4 +20,4 @@ Developing Traefik, our main goal is to make it simple to use, and we're sure yo
!!! info !!! 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.

View file

@ -406,7 +406,7 @@ In the example, it is the part between `-----BEGIN CERTIFICATE-----` and `-----E
!!! info "Extracted data" !!! info "Extracted data"
The delimiters and `\n` will be removed. 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" !!! 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: The following example shows an unescaped result that uses all the available fields:
```text ```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" !!! 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` #### `info.notAfter`
@ -442,7 +442,7 @@ The data are taken from the following certificate part:
The escape `notAfter` info part will be like: The escape `notAfter` info part will be like:
```text ```text
NA=1607166616 NA="1607166616"
``` ```
#### `info.notBefore` #### `info.notBefore`
@ -459,7 +459,7 @@ Validity
The escape `notBefore` info part will be like: The escape `notBefore` info part will be like:
```text ```text
NB=1544094616 NB="1544094616"
``` ```
#### `info.sans` #### `info.sans`
@ -476,7 +476,7 @@ The data are taken from the following certificate part:
The escape SANs info part will be like: The escape SANs info part will be like:
```text ```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" !!! info "multiple values"

View file

@ -560,8 +560,8 @@ with the path `/admin` stripped, e.g. to `http://<IP>:<port>/`. In this case, yo
```yaml tab="Docker" ```yaml tab="Docker"
labels: labels:
- "traefik.http.routers.admin.rule=Host(`company.org`) && PathPrefix(`/admin`)" - "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.middlewares.admin-stripprefix.stripprefix.prefixes=/admin"
- "traefik.http.routers.web.middlewares=admin-stripprefix@docker"
``` ```
```yaml tab="Kubernetes IngressRoute" ```yaml tab="Kubernetes IngressRoute"
@ -1029,12 +1029,12 @@ As the dashboard access is now secured by default you can either:
[api] [api]
[providers.file] [providers.file]
filename = "/dynamic-conf.toml" directory = "/path/to/dynamic/config"
##---------------------## ##---------------------##
## dynamic configuration ## dynamic configuration
# dynamic-conf.toml # /path/to/dynamic/config/dynamic-conf.toml
[http.routers.api] [http.routers.api]
rule = "Host(`traefik.docker.localhost`)" rule = "Host(`traefik.docker.localhost`)"
@ -1061,12 +1061,12 @@ As the dashboard access is now secured by default you can either:
providers: providers:
file: file:
filename: /dynamic-conf.yaml directory: /path/to/dynamic/config
##---------------------## ##---------------------##
## dynamic configuration ## dynamic configuration
# dynamic-conf.yaml # /path/to/dynamic/config/dynamic-conf.yaml
http: http:
routers: routers:

View file

@ -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: or to make sure that the defined rule captures both prefixes:
```bash tab="Host Rule" ```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`)" rule = "Host(`traefik.domain.com`)"
``` ```
```bash tab="Path Prefix Rule" ```bash tab="Path Prefix Rule"
# Matches http://traefik.domain.com/api , http://domain.com/api or http://traefik.domain.com/dashboard # The dashboard can be accessed on http://domain.com/dashboard/ or http://traefik.domain.com/dashboard/
# but does not match http://traefik.domain.com/hello
rule = "PathPrefix(`/api`) || PathPrefix(`/dashboard`)" rule = "PathPrefix(`/api`) || PathPrefix(`/dashboard`)"
``` ```
```bash tab="Combination of Rules" ```bash tab="Combination of Rules"
# Matches http://traefik.domain.com/api or http://traefik.domain.com/dashboard # The dashboard can be accessed on http://traefik.domain.com/dashboard/
# but does not match http://traefik.domain.com/hello
rule = "Host(`traefik.domain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" rule = "Host(`traefik.domain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
``` ```

View file

@ -59,7 +59,7 @@ ping:
--ping.entryPoint=ping --ping.entryPoint=ping
``` ```
#### `manualRouting` ### `manualRouting`
_Optional, Default=false_ _Optional, Default=false_

View file

@ -23,17 +23,17 @@ You can write one of these mutually exclusive configuration elements:
```toml tab="File (TOML)" ```toml tab="File (TOML)"
[providers.file] [providers.file]
filename = "/my/path/to/dynamic-conf.toml" directory = "/path/to/dynamic/conf"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:
file: file:
filename: "/my/path/to/dynamic-conf.yml" directory: "/path/to/dynamic/conf"
``` ```
```bash tab="CLI" ```bash tab="CLI"
--providers.file.filename=/my/path/to/dynamic_conf.toml --providers.file.directory=/path/to/dynamic/conf
``` ```
Declaring Routers, Middlewares & Services: 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). 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` ### `filename`
Defines the path of the configuration file. 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)" ```toml tab="File (TOML)"
[providers] [providers]
[providers.file] [providers.file]
filename = "dynamic_conf.toml" directory = "/path/to/dynamic/conf"
watch = true watch = true
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
providers: providers:
file: file:
filename: dynamic_conf.yml directory: /path/to/dynamic/conf
watch: true watch: true
``` ```
```bash tab="CLI" ```bash tab="CLI"
--providers.file.filename=dynamic_conf.toml --providers.file.directory=/my/path/to/dynamic/conf
--providers.file.watch=true --providers.file.watch=true
``` ```

View file

@ -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). 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 ## Provider Configuration
### `endpoint` ### `endpoint`

View file

@ -47,6 +47,20 @@ spec:
servicePort: 80 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 ## Provider Configuration
### `endpoint` ### `endpoint`

View file

@ -33,9 +33,9 @@ Static configuration:
address = ":8081" address = ":8081"
[providers] [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] [providers.file]
filename = "dynamic_conf.toml" directory = "/path/to/dynamic/conf"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
@ -45,17 +45,17 @@ entryPoints:
address: :8081 address: :8081
providers: 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: file:
filename: dynamic_conf.yml directory: /path/to/dynamic/conf
``` ```
```bash tab="CLI" ```bash tab="CLI"
# Listen on port 8081 for incoming requests # Listen on port 8081 for incoming requests
--entryPoints.web.address=:8081 --entryPoints.web.address=:8081
# 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 --providers.file.directory=/path/to/dynamic/conf
``` ```
Dynamic configuration: Dynamic configuration:
@ -133,9 +133,9 @@ http:
address = ":8081" address = ":8081"
[providers] [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] [providers.file]
filename = "dynamic_conf.toml" directory = "/path/to/dynamic/conf"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
@ -144,17 +144,17 @@ http:
# Listen on port 8081 for incoming requests # Listen on port 8081 for incoming requests
address: :8081 address: :8081
providers: 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: file:
filename: dynamic_conf.yml directory: /path/to/dynamic/conf
``` ```
```bash tab="CLI" ```bash tab="CLI"
# Listen on port 8081 for incoming requests # Listen on port 8081 for incoming requests
--entryPoints.web.address=:8081 --entryPoints.web.address=:8081
# 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 --providers.file.directory=/path/to/dynamic/conf
``` ```
**Dynamic Configuration** **Dynamic Configuration**

View file

@ -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. See [tls](../routers/index.md#tls) for more information.
```json ```json
"traefik.http.routers.myrouter>.tls": "true" "traefik.http.routers.myrouter.tls": "true"
``` ```
??? info "`traefik.http.routers.<router_name>.tls.certresolver`" ??? info "`traefik.http.routers.<router_name>.tls.certresolver`"

View file

@ -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). 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" ```toml tab="TOML"
## Dynamic configuration ## Dynamic configuration
@ -438,7 +440,9 @@ http:
The mirroring is able to mirror requests sent to a service to other services. 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" ```toml tab="TOML"
## Dynamic configuration ## 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 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" ```toml tab="TOML"
## Dynamic configuration ## Dynamic configuration

View file

@ -16,7 +16,7 @@ Static configuration:
[api] [api]
[providers.file] [providers.file]
filename = "dynamic_conf.toml" directory = "/path/to/dynamic/config"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
@ -26,18 +26,18 @@ entryPoints:
providers: providers:
file: file:
filename: dynamic_conf.yml directory: /path/to/dynamic/config
api: {} api: {}
``` ```
```yaml tab="CLI" ```yaml tab="CLI"
--entryPoints.web.address=:80 --entryPoints.web.address=:80
--providers.file.filename=dynamic_conf.toml --providers.file.directory=/path/to/dynamic/config
--api.insecure=true --api.insecure=true
``` ```
`dynamic_conf.{toml,yml}`: `/path/to/dynamic/config/dynamic_conf.{toml,yml}`:
```toml tab="TOML" ```toml tab="TOML"
## dynamic configuration ## ## dynamic configuration ##
@ -132,7 +132,7 @@ Static configuration:
[api] [api]
[provider.file] [provider.file]
filename = "dynamic_conf.toml" directory = "/path/to/dynamic/config"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
@ -147,7 +147,7 @@ serversTransport:
providers: providers:
file: file:
filename: dynamic_conf.yml directory: /path/to/dynamic/config
api: {} api: {}
``` ```
@ -156,11 +156,11 @@ api: {}
--entryPoints.websecure.address=:4443 --entryPoints.websecure.address=:4443
# For secure connection on backend.local # For secure connection on backend.local
--serversTransport.rootCAs=./backend.cert --serversTransport.rootCAs=./backend.cert
--providers.file.filename=dynamic_conf.toml --providers.file.directory=/path/to/dynamic/config
--api.insecure=true --api.insecure=true
``` ```
`dynamic_conf.{toml,yml}`: `/path/to/dynamic/config/dynamic_conf.{toml,yml}`:
```toml tab="TOML" ```toml tab="TOML"
## dynamic configuration ## ## dynamic configuration ##

View file

@ -44,7 +44,7 @@ plugins:
- search - search
- exclude: - exclude:
glob: glob:
- include-*.md - "**/include-*.md"
# https://squidfunk.github.io/mkdocs-material/extensions/admonition/ # https://squidfunk.github.io/mkdocs-material/extensions/admonition/
# https://facelessuser.github.io/pymdown-extensions/ # https://facelessuser.github.io/pymdown-extensions/

View file

@ -13,6 +13,20 @@ var (
_ middlewares.Stateful = &captureResponseWriter{} _ 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 // captureResponseWriter is a wrapper of type http.ResponseWriter
// that tracks request status and size // that tracks request status and size
type captureResponseWriter struct { type captureResponseWriter struct {
@ -21,6 +35,16 @@ type captureResponseWriter struct {
size int64 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 { func (crw *captureResponseWriter) Header() http.Header {
return crw.rw.Header() return crw.rw.Header()
} }

View file

@ -49,7 +49,7 @@ func AddServiceFields(rw http.ResponseWriter, req *http.Request, next http.Handl
// AddOriginFields add origin fields // AddOriginFields add origin fields
func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) { func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) {
crw := &captureResponseWriter{rw: rw} crw := newCaptureResponseWriter(rw)
start := time.Now().UTC() start := time.Now().UTC()
next.ServeHTTP(crw, req) next.ServeHTTP(crw, req)

View file

@ -200,7 +200,7 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
core[ClientHost] = forwardedFor core[ClientHost] = forwardedFor
} }
crw := &captureResponseWriter{rw: rw} crw := newCaptureResponseWriter(rw)
next.ServeHTTP(crw, reqWithDataTable) next.ServeHTTP(crw, reqWithDataTable)

View file

@ -89,10 +89,10 @@ func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request)
}(labels) }(labels)
start := time.Now() start := time.Now()
recorder := &responseRecorder{rw, http.StatusOK} recorder := newResponseRecorder(rw)
m.next.ServeHTTP(recorder, req) 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.reqsCounter.With(labels...).Add(1)
m.reqDurationHistogram.With(labels...).Observe(time.Since(start).Seconds()) m.reqDurationHistogram.With(labels...).Observe(time.Since(start).Seconds())
} }

View file

@ -6,6 +6,23 @@ import (
"net/http" "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 // responseRecorder captures information from the response and preserves it for
// later analysis. // later analysis.
type responseRecorder struct { type responseRecorder struct {
@ -13,6 +30,20 @@ type responseRecorder struct {
statusCode int 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. // WriteHeader captures the status code for later retrieval.
func (r *responseRecorder) WriteHeader(status int) { func (r *responseRecorder) WriteHeader(status int) {
r.ResponseWriter.WriteHeader(status) r.ResponseWriter.WriteHeader(status)

View file

@ -18,10 +18,17 @@ import (
"github.com/opentracing/opentracing-go/ext" "github.com/opentracing/opentracing-go/ext"
) )
const typeName = "PassClientTLSCert"
const ( const (
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert" xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info" xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
typeName = "PassClientTLSCert" )
const (
certSeparator = ","
fieldSeparator = ";"
subFieldSeparator = ","
) )
var attributeTypeNames = map[string]string{ 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. // passTLSClientCert is a middleware that helps setup a few tls info features.
type passTLSClientCert struct { type passTLSClientCert struct {
next http.Handler next http.Handler
@ -71,45 +101,84 @@ func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCer
next: next, next: next,
name: name, name: name,
pem: config.PEM, pem: config.PEM,
info: newTLSClientInfo(config.Info), info: newTLSClientCertificateInfo(config.Info),
}, nil }, 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) { func (p *passTLSClientCert) GetTracingInformation() (string, ext.SpanKindEnum) {
return p.name, tracing.SpanKindNoneEnum return p.name, tracing.SpanKindNoneEnum
} }
func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := middlewares.GetLoggerCtx(req.Context(), p.name, typeName) 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) 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 { if options == nil {
return "" return ""
} }
@ -120,7 +189,7 @@ func getDNInfo(ctx context.Context, prefix string, options *DistinguishedNameOpt
for _, name := range cs.Names { for _, name := range cs.Names {
// Domain Component - RFC 2247 // Domain Component - RFC 2247
if options.DomainComponent && attributeTypeNames[name.Type.String()] == "DC" { 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") writePart(ctx, content, cs.CommonName, "CN")
} }
if content.Len() > 0 { return content.String()
return prefix + `="` + strings.TrimSuffix(content.String(), ",") + `"`
}
return ""
} }
func writeParts(ctx context.Context, content io.StringWriter, entries []string, prefix 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) { func writePart(ctx context.Context, content io.StringWriter, entry string, prefix string) {
if len(entry) > 0 { 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 { if err != nil {
log.FromContext(ctx).Error(err) 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. // sanitize As we pass the raw certificates, remove the useless data and make it http request compliant.
func sanitize(cert []byte) string { func sanitize(cert []byte) string {
s := string(cert) cleaned := strings.NewReplacer(
r := strings.NewReplacer("-----BEGIN CERTIFICATE-----", "", "-----BEGIN CERTIFICATE-----", "",
"-----END CERTIFICATE-----", "", "-----END CERTIFICATE-----", "",
"\n", "") "\n", "",
cleaned := r.Replace(s) ).Replace(string(cert))
return url.QueryEscape(cleaned) return url.QueryEscape(cleaned)
} }
// extractCertificate extract the certificate from the request. // getCertificates Build a string with the client certificates.
func extractCertificate(ctx context.Context, cert *x509.Certificate) string { func getCertificates(ctx context.Context, certs []*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 {
var headerValues []string var headerValues []string
for _, peerCert := range certs { for _, peerCert := range certs {
headerValues = append(headerValues, extractCertificate(ctx, peerCert)) 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. // getSANs get the Subject Alternate Name values.
func getSANs(cert *x509.Certificate) []string { func getSANs(cert *x509.Certificate) []string {
var sans []string
if cert == nil { if cert == nil {
return sans return nil
} }
var sans []string
sans = append(sans, cert.DNSNames...) sans = append(sans, cert.DNSNames...)
sans = append(sans, cert.EmailAddresses...) sans = append(sans, cert.EmailAddresses...)
var ips []string
for _, ip := range cert.IPAddresses { 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 { for _, uri := range cert.URIs {
uris = append(uris, uri.String()) sans = append(sans, uri.String())
} }
return append(sans, uris...) return sans
} }

View file

@ -15,6 +15,7 @@ import (
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/testhelpers" "github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -113,6 +114,7 @@ Cg+XKmHzexmTnKaKac2w9ZECpRsQ9IBdQq9OghIwPtOnERTOUJEEgNcqA+9xELjb
pQ== pQ==
-----END CERTIFICATE----- -----END CERTIFICATE-----
` `
minimalCheeseCrt = `-----BEGIN CERTIFICATE----- minimalCheeseCrt = `-----BEGIN CERTIFICATE-----
MIIEQDCCAygCFFRY0OBk/L5Se0IZRj3CMljawL2UMA0GCSqGSIb3DQEBCwUAMIIB MIIEQDCCAygCFFRY0OBk/L5Se0IZRj3CMljawL2UMA0GCSqGSIb3DQEBCwUAMIIB
hDETMBEGCgmSJomT8ixkARkWA29yZzEWMBQGCgmSJomT8ixkARkWBmNoZWVzZTEP hDETMBEGCgmSJomT8ixkARkWA29yZzEWMBQGCgmSJomT8ixkARkWBmNoZWVzZTEP
@ -262,47 +264,6 @@ jECvgAY7Nfd9mZ1KtyNaW31is+kag7NsvjxU/kM=
-----END CERTIFICATE-----` -----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) { var next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("bar")) _, err := w.Write([]byte("bar"))
if err != nil { if err != nil {
@ -310,59 +271,7 @@ var next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
} }
}) })
func getExpectedSanitized(s string) string { func TestPassTLSClientCert_PEM(t *testing.T) {
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) {
testCases := []struct { testCases := []struct {
desc string desc string
certContents []string // set the request TLS attribute if defined 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.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") assert.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, "bar", res.Body.String(), "Should be the expected body")
if test.expectedHeader != "" { 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 { } 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) { func TestPassTLSClientCert_certInfo(t *testing.T) {
urlFoo, err := url.Parse("my.foo.com") minimalCheeseCertAllInfo := strings.Join([]string{
require.NoError(t, err) `Subject="C=FR,ST=Some-State,O=Cheese"`,
urlBar, err := url.Parse("my.bar.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"`,
require.NoError(t, err) `NB="1544094636"`,
`NA="1632568236"`,
}, fieldSeparator)
testCases := []struct { completeCertAllInfo := strings.Join([]string{
desc 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"`,
cert *x509.Certificate // set the request TLS attribute if defined `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"`,
expected []string `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"`,
desc: "With nil", }, fieldSeparator)
},
{
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`
testCases := []struct { testCases := []struct {
desc string 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", 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", 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", 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.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") assert.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, "bar", res.Body.String(), "Should be the expected body")
if test.expectedHeader != "" { 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 { } 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
}

View file

@ -21,6 +21,14 @@
"rule": "PathPrefix(`/`)", "rule": "PathPrefix(`/`)",
"priority": 2147483645 "priority": 2147483645
}, },
"debug": {
"entryPoints": [
"traefik"
],
"service": "api@internal",
"rule": "PathPrefix(`/debug`)",
"priority": 2147483646
},
"ping": { "ping": {
"entryPoints": [ "entryPoints": [
"test" "test"

View file

@ -95,6 +95,15 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
StripPrefix: &dynamic.StripPrefix{Prefixes: []string{"/dashboard/", "/dashboard"}}, 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{} cfg.HTTP.Services["api"] = &dynamic.Service{}

View file

@ -28,6 +28,7 @@ func Test_createConfiguration(t *testing.T) {
API: &static.API{ API: &static.API{
Insecure: true, Insecure: true,
Dashboard: true, Dashboard: true,
Debug: true,
}, },
Ping: &ping.Handler{ Ping: &ping.Handler{
EntryPoint: "test", EntryPoint: "test",

View file

@ -49,8 +49,8 @@ func NewConfigurationWatcher(routinesPool *safe.Pool, pvd provider.Provider, pro
// Start the configuration watcher. // Start the configuration watcher.
func (c *ConfigurationWatcher) Start() { func (c *ConfigurationWatcher) Start() {
c.routinesPool.Go(func(stop chan bool) { c.listenProviders(stop) }) c.routinesPool.Go(c.listenProviders)
c.routinesPool.Go(func(stop chan bool) { c.listenConfigurations(stop) }) c.routinesPool.Go(c.listenConfigurations)
c.startProvider() c.startProvider()
} }

View file

@ -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 // Start starts the server and Stop/Close it when context is Done
func (s *Server) Start(ctx context.Context) { func (s *Server) Start(ctx context.Context) {
go func() { go func() {
defer s.Close()
<-ctx.Done() <-ctx.Done()
logger := log.FromContext(ctx) logger := log.FromContext(ctx)
logger.Info("I have to go...") logger.Info("I have to go...")
@ -59,9 +58,7 @@ func (s *Server) Start(ctx context.Context) {
s.tcpEntryPoints.Start() s.tcpEntryPoints.Start()
s.watcher.Start() s.watcher.Start()
s.routinesPool.Go(func(stop chan bool) { s.routinesPool.Go(s.listenSignals)
s.listenSignals(stop)
})
} }
// Wait blocks until the server shutdown. // Wait blocks until the server shutdown.

View file

@ -158,6 +158,10 @@ func (e *TCPEntryPoint) StartTCP(ctx context.Context) {
conn, err := e.listener.Accept() conn, err := e.listener.Accept()
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
continue
}
return return
} }

View file

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"sync" "sync"
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
"github.com/containous/traefik/v2/pkg/safe" "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) { if handler.count*100 < total*uint64(handler.percent) {
handler.count++ handler.count++
handler.lock.Unlock() 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, // When a request served by m.handler is successful, req.Context will be canceled,
// which would trigger a cancellation of the ongoing mirrored requests. // which would trigger a cancellation of the ongoing mirrored requests.
// Therefore, we give a new, non-cancellable context to each of the mirrored calls, // Therefore, we give a new, non-cancellable context to each of the mirrored calls,
// so they can terminate by themselves. // so they can terminate by themselves.
handler.ServeHTTP(m.rw, req.WithContext(contextStopPropagation{req.Context()})) handler.ServeHTTP(m.rw, req.WithContext(contextStopPropagation{ctx}))
} else { } else {
handler.lock.Unlock() handler.lock.Unlock()
} }

View file

@ -102,7 +102,8 @@ func (f FileOrContent) IsPath() bool {
func (f FileOrContent) Read() ([]byte, error) { func (f FileOrContent) Read() ([]byte, error) {
var content []byte var content []byte
if _, err := os.Stat(f.String()); err == nil { if f.IsPath() {
var err error
content, err = ioutil.ReadFile(f.String()) content, err = ioutil.ReadFile(f.String())
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,14 +1,15 @@
import { APP } from '../_helpers/APP' import { APP } from '../_helpers/APP'
import { getTotal } from './utils'
const apiBase = '/http' const apiBase = '/http'
function getAllRouters (params) { function getAllRouters (params) {
return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`)
.then(body => { .then(response => {
const total = body.data ? body.data.length : 0 const { data = [], headers } = response
console.log('Success -> HttpService -> getAllRouters', body.data) const total = getTotal(headers, params)
// TODO - suggestion: add the total-pages in api response to optimize the query console.log('Success -> HttpService -> getAllRouters', response, response.data)
return { data: body.data || [], total } return { data, total }
}) })
} }
@ -22,11 +23,11 @@ function getRouterByName (name) {
function getAllServices (params) { function getAllServices (params) {
return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`)
.then(body => { .then(response => {
const total = body.data ? body.data.length : 0 const { data = [], headers } = response
console.log('Success -> HttpService -> getAllServices', body.data) const total = getTotal(headers, params)
// TODO - suggestion: add the total-pages in api response to optimize the query console.log('Success -> HttpService -> getAllServices', response.data)
return { data: body.data || [], total } return { data, total }
}) })
} }
@ -40,11 +41,11 @@ function getServiceByName (name) {
function getAllMiddlewares (params) { function getAllMiddlewares (params) {
return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`)
.then(body => { .then(response => {
const total = body.data ? body.data.length : 0 const { data = [], headers } = response
console.log('Success -> HttpService -> getAllMiddlewares', body.data) const total = getTotal(headers, params)
// TODO - suggestion: add the total-pages in api response to optimize the query console.log('Success -> HttpService -> getAllMiddlewares', response.data)
return { data: body.data || [], total } return { data, total }
}) })
} }

View file

@ -1,14 +1,15 @@
import { APP } from '../_helpers/APP' import { APP } from '../_helpers/APP'
import { getTotal } from './utils'
const apiBase = '/tcp' const apiBase = '/tcp'
function getAllRouters (params) { function getAllRouters (params) {
return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`)
.then(body => { .then(response => {
const total = body.data ? body.data.length : 0 const { data = [], headers } = response
console.log('Success -> HttpService -> getAllRouters', body.data) const total = getTotal(headers, params)
// TODO - suggestion: add the total-pages in api response to optimize the query console.log('Success -> HttpService -> getAllRouters', response.data)
return { data: body.data || [], total } return { data, total }
}) })
} }
@ -22,11 +23,11 @@ function getRouterByName (name) {
function getAllServices (params) { function getAllServices (params) {
return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`)
.then(body => { .then(response => {
const total = body.data ? body.data.length : 0 const { data = [], headers } = response
console.log('Success -> HttpService -> getAllServices', body.data) const total = getTotal(headers, params)
// TODO - suggestion: add the total-pages in api response to optimize the query console.log('Success -> HttpService -> getAllServices', response.data)
return { data: body.data || [], total } return { data, total }
}) })
} }

View file

@ -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
}

View file

@ -55,11 +55,11 @@ export default {
methods: { methods: {
getProvider (service) { getProvider (service) {
const words = service.name.split('@') const words = service.name.split('@')
if (words.length !== 2) { if (words.length === 2) {
return this.provider return words[1]
} }
return words[1] return this.data.provider
} }
} }
} }

View file

@ -54,7 +54,7 @@
</div> </div>
</q-card-section> </q-card-section>
<q-card-section v-if="data.loadBalancer.terminationDelay"> <q-card-section v-if="data.loadBalancer && data.loadBalancer.terminationDelay">
<div class="row items-start no-wrap"> <div class="row items-start no-wrap">
<div class="col"> <div class="col">
<div class="text-subtitle2">Termination Delay</div> <div class="text-subtitle2">Termination Delay</div>

View file

@ -55,11 +55,11 @@ export default {
methods: { methods: {
getProvider (service) { getProvider (service) {
const words = service.name.split('@') const words = service.name.split('@')
if (words.length !== 2) { if (words.length === 2) {
return this.provider return words[1]
} }
return words[1] return this.data.provider
} }
} }
} }