Merge branch 'v2.0' into v2.1
This commit is contained in:
commit
5c091a1871
24 changed files with 737 additions and 341 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,3 +1,20 @@
|
||||||
|
## [v2.0.7](https://github.com/containous/traefik/tree/v2.0.7) (2019-12-09)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.0.6...v2.0.7)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[logs,middleware]** Remove mirroring impact in accesslog ([#5967](https://github.com/containous/traefik/pull/5967) by [juliens](https://github.com/juliens))
|
||||||
|
- **[middleware]** fix: PassClientTLSCert middleware separators and formatting ([#5921](https://github.com/containous/traefik/pull/5921) by [ldez](https://github.com/ldez))
|
||||||
|
- **[server]** Do not stop to listen on tcp listeners on temporary errors ([#5935](https://github.com/containous/traefik/pull/5935) by [skwair](https://github.com/skwair))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[acme,k8s/crd,k8s/ingress]** Document LE caveats with Kubernetes on v2 ([#5902](https://github.com/containous/traefik/pull/5902) by [dtomcej](https://github.com/dtomcej))
|
||||||
|
- **[acme]** The Cloudflare hint for the GLOBAL API KEY for CF MAIL/API_KEY ([#5964](https://github.com/containous/traefik/pull/5964) by [EugenMayer](https://github.com/EugenMayer))
|
||||||
|
- **[acme]** Improve documentation for ACME/Let's Encrypt ([#5819](https://github.com/containous/traefik/pull/5819) by [dduportal](https://github.com/dduportal))
|
||||||
|
- **[file]** Improve documentation on file provider limitations with file system notifications ([#5939](https://github.com/containous/traefik/pull/5939) by [jbdoumenjou](https://github.com/jbdoumenjou))
|
||||||
|
- Make trailing slash more prominent for the "secure dashboard setup" too ([#5963](https://github.com/containous/traefik/pull/5963) by [EugenMayer](https://github.com/EugenMayer))
|
||||||
|
- Fix Docker example in "Strip and Rewrite Path Prefixes" in migration guide ([#5949](https://github.com/containous/traefik/pull/5949) by [q210](https://github.com/q210))
|
||||||
|
- readme: Fix link to file backend/provider documentation ([#5945](https://github.com/containous/traefik/pull/5945) by [hartwork](https://github.com/hartwork))
|
||||||
|
|
||||||
## [v2.1.0-rc3](https://github.com/containous/traefik/tree/v2.1.0-rc3) (2019-12-02)
|
## [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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
4
docs/content/https/.markdownlint.json
Normal file
4
docs/content/https/.markdownlint.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "../../.markdownlint.json",
|
||||||
|
"MD041": false
|
||||||
|
}
|
|
@ -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 .
|
||||||
|
|
||||||
|
|
88
docs/content/https/include-acme-multiple-domains-example.md
Normal file
88
docs/content/https/include-acme-multiple-domains-example.md
Normal 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"
|
||||||
|
```
|
|
@ -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
|
||||||
|
```
|
72
docs/content/https/include-acme-single-domain-example.md
Normal file
72
docs/content/https/include-acme-single-domain-example.md
Normal 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
|
||||||
|
```
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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`))"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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**
|
||||||
|
|
|
@ -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`"
|
||||||
|
|
|
@ -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 ##
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue