Add Traefik Hub Integration (Experimental Feature)

This commit is contained in:
Jean-Baptiste Doumenjou 2022-03-24 19:44:08 +01:00 committed by GitHub
parent 8d58f33a28
commit fbdb6e6e78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 992 additions and 11 deletions

View file

@ -241,6 +241,19 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
}
}
// Traefik Hub
if staticConfiguration.Hub != nil {
if err = providerAggregator.AddProvider(staticConfiguration.Hub); err != nil {
return nil, fmt.Errorf("adding Traefik Hub provider: %w", err)
}
// API is mandatory for Traefik Hub to access the dynamic configuration.
if staticConfiguration.API == nil {
staticConfiguration.API = &static.API{}
}
}
// Metrics
metricRegistries := registerMetricClients(staticConfiguration.Metrics)
@ -323,7 +336,10 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
continue
}
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok &&
// "traefik-hub" is an allowed certificate resolver name in a Traefik Hub Experimental feature context.
// It is used to activate its own certificate resolution, even though it is not a "classical" traefik certificate resolver.
(staticConfiguration.Hub == nil || rt.TLS.CertResolver != "traefik-hub") {
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
}
}
@ -346,6 +362,11 @@ func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvid
func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string {
var defaultEntryPoints []string
for name, cfg := range staticConfiguration.EntryPoints {
// Traefik Hub entryPoint should not be part of the set of default entryPoints.
if staticConfiguration.Hub != nil && staticConfiguration.Hub.EntryPoint == name {
continue
}
protocol, err := cfg.GetProtocol()
if err != nil {
// Should never happen because Traefik should not start if protocol is invalid.

View file

@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
`--experimental.http3`:
Enable HTTP3. (Default: ```false```)
`--experimental.hub`:
Enable the Traefik Hub provider. (Default: ```false```)
`--experimental.kubernetesgateway`:
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
`--hostresolver.resolvdepth`:
The maximal depth of DNS recursive resolving (Default: ```5```)
`--hub`:
Traefik Hub configuration. (Default: ```false```)
`--hub.entrypoint`:
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
`--hub.tls.ca`:
The certificate authority authenticates the Traefik Hub Agent certificate.
`--hub.tls.cert`:
The TLS certificate for Traefik Proxy as a TLS client.
`--hub.tls.insecure`:
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
`--hub.tls.key`:
The TLS key for Traefik Proxy as a TLS client.
`--log`:
Traefik log settings. (Default: ```false```)

View file

@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
`TRAEFIK_EXPERIMENTAL_HTTP3`:
Enable HTTP3. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_HUB`:
Enable the Traefik Hub provider. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
`TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`:
The maximal depth of DNS recursive resolving (Default: ```5```)
`TRAEFIK_HUB`:
Traefik Hub configuration. (Default: ```false```)
`TRAEFIK_HUB_ENTRYPOINT`:
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
`TRAEFIK_HUB_TLS_CA`:
The certificate authority authenticates the Traefik Hub Agent certificate.
`TRAEFIK_HUB_TLS_CERT`:
The TLS certificate for Traefik Proxy as a TLS client.
`TRAEFIK_HUB_TLS_INSECURE`:
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
`TRAEFIK_HUB_TLS_KEY`:
The TLS key for Traefik Proxy as a TLS client.
`TRAEFIK_LOG`:
Traefik log settings. (Default: ```false```)

View file

@ -419,9 +419,18 @@
token = "foobar"
dashboard = true
[hub]
entrypoint = "foobar"
[hub.tls]
insecure = true
ca = "foobar"
cert = "foobar"
key = "foobar"
[experimental]
kubernetesGateway = true
http3 = true
hub = true
[experimental.plugins]
[experimental.plugins.Descriptor0]
moduleName = "foobar"

View file

@ -440,9 +440,17 @@ certificatesResolvers:
pilot:
token: foobar
dashboard: true
hub:
entrypoint: foobar
tls:
insecure: true
ca: foobar
cert: foobar
key: foobar
experimental:
kubernetesGateway: true
http3: true
hub: true
plugins:
Descriptor0:
moduleName: foobar

View file

@ -0,0 +1,294 @@
# Traefik Hub (Experimental)
## Overview
Once the Traefik Hub Experimental feature is enabled in Traefik,
Traefik and its local agent communicate together.
This agent can:
* get the Traefik metrics to display them in the Traefik Hub UI
* secure the Traefik routers
* provide ACME certificates to Traefik
* transfer requests from the SaaS Platform to Traefik (and then avoid the users to expose directly their infrastructure on the internet)
!!! warning "Traefik Hub EntryPoint"
When the Traefik Hub feature is enabled, Traefik exposes some services meant for the Traefik Hub Agent on a dedicated entryPoint (on port `9900` by default).
Given their sensitive nature, those services should not be publicly exposed.
Also this dedicated entryPoint, regardless of how it is created (default, or user-defined), should not be used by anything other than the Hub Agent.
!!! important "Learn More About Traefik Hub"
This section is intended only as a brief overview for Traefik users who are not familiar with Traefik Hub.
To explore all that Traefik Hub has to offer, please consult the [Traefik Hub Documentation](https://doc.traefik.io/traefik-hub).
!!! Note "Prerequisites"
* Traefik Hub is compatible with Traefik Proxy 2.7 or later.
* The Traefik Hub Agent must be installed to connect to the Traefik Hub platform.
* Activate this feature in the experimental section of the static configuration.
!!! example "Minimal Static Configuration to Activate Traefik Hub"
```yaml tab="File (YAML)"
experimental:
hub: true
hub:
tls:
insecure: true
metrics:
prometheus: {}
```
```toml tab="File (TOML)"
[experimental]
hub = true
[hub]
[hub.tls]
insecure = true
[metrics]
[metrics.prometheus]
```
```bash tab="CLI"
--experimental.hub
--hub.tls.insecure=true
--metrics.prometheus=true
```
## Configuration
### `entryPoint`
_Optional, Default="traefik-hub"_
Defines the entryPoint that exposes data for Traefik Hub Agent.
!!! info
* If no entryPoint is defined, a default `traefik-hub` entryPoint is created (on port `9900`).
* If defined, the value must match an existing entryPoint name.
* This dedicated Traefik Hub entryPoint should not be used by anything other than Traefik Hub.
```yaml tab="File (YAML)"
entryPoints:
hub-ep: ":8000"
hub:
entryPoint: "hub-ep"
```
```toml tab="File (TOML)"
[entryPoints.hub-ep]
address = ":8000"
[hub]
entryPoint = "hub-ep"
```
```bash tab="CLI"
--entrypoints.hub-ep.address=:8000
--hub.entrypoint=hub-ep
```
### `tls`
_Required, Default=None_
This section allows configuring mutual TLS connection between Traefik Proxy and the Traefik Hub Agent.
The key and the certificate are the credentials for Traefik Proxy as a TLS client.
The certificate authority authenticates the Traefik Hub Agent certificate.
!!! note "Certificate Domain"
The certificate must be valid for the `proxy.traefik` domain.
!!! note "Certificates Definition"
Certificates can be defined either by their content or their path.
!!! note "Insecure Mode"
The `insecure` option is mutually exclusive with any other option.
```yaml tab="File (YAML)"
hub:
tls:
ca: /path/to/ca.pem
cert: /path/to/cert.pem
key: /path/to/key.pem
```
```toml tab="File (TOML)"
[hub.tls]
ca= "/path/to/ca.pem"
cert= "/path/to/cert.pem"
key= "/path/to/key.pem"
```
```bash tab="CLI"
--hub.tls.ca=/path/to/ca.pem
--hub.tls.cert=/path/to/cert.pem
--hub.tls.key=/path/to/key.pem
```
### `tls.ca`
The certificate authority authenticates the Traefik Hub Agent certificate.
```yaml tab="File (YAML)"
hub:
tls:
ca: |-
-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
```toml tab="File (TOML)"
[hub.tls]
ca = """-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----"""
```
```bash tab="CLI"
--hub.tls.ca=-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
### `tls.cert`
The TLS certificate for Traefik Proxy as a TLS client.
!!! note "Certificate Domain"
The certificate must be valid for the `proxy.traefik` domain.
```yaml tab="File (YAML)"
hub:
tls:
ca: |-
-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
```toml tab="File (TOML)"
[hub.tls]
cert = """-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----"""
```
```bash tab="CLI"
--hub.tls.cert=-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
### `tls.key`
The TLS key for Traefik Proxy as a TLS client.
```yaml tab="File (YAML)"
hub:
tls:
key: |-
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----
```
```toml tab="File (TOML)"
[hub.tls]
key = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----"""
```
```bash tab="CLI"
--hub.tls.key=-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----
```
### `tls.insecure`
_Optional, Default=false_
Enables an insecure TLS connection that uses default credentials,
and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent.
The `insecure` option is mutually exclusive with any other option.
!!! warning "Security Consideration"
Do not use this setup in production.
This option implies sensitive data can be exposed to potential malicious third-party programs.
```yaml tab="File (YAML)"
hub:
insecure: true
```
```toml tab="File (TOML)"
[hub]
insecure = true
```
```bash tab="CLI"
--hub.insecure=true
```

View file

@ -135,6 +135,7 @@ nav:
- 'InFlightConn': 'middlewares/tcp/inflightconn.md'
- 'IpWhitelist': 'middlewares/tcp/ipwhitelist.md'
- 'Plugins & Traefik Pilot': 'plugins/index.md'
- 'Traefik Hub': 'traefik-hub/index.md'
- 'Operations':
- 'CLI': 'operations/cli.md'
- 'Dashboard' : 'operations/dashboard.md'

View file

@ -4,3 +4,4 @@ mkdocs-bootswatch==1.0
mkdocs-traefiklabs>=100.0.7
markdown-include==0.5.1
mkdocs-exclude==1.0.2
Jinja2==3.0.0

View file

@ -26,6 +26,7 @@ type features struct {
Tracing string `json:"tracing"`
Metrics string `json:"metrics"`
AccessLog bool `json:"accessLog"`
Hub bool `json:"hub"`
// TODO add certificates resolvers
}
@ -247,6 +248,7 @@ func getFeatures(conf static.Configuration) features {
Tracing: getTracing(conf),
Metrics: getMetrics(conf),
AccessLog: conf.AccessLog != nil,
Hub: conf.Hub != nil,
}
}

View file

@ -15,6 +15,7 @@ import (
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/provider/docker"
"github.com/traefik/traefik/v2/pkg/provider/file"
"github.com/traefik/traefik/v2/pkg/provider/hub"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
"github.com/traefik/traefik/v2/pkg/provider/marathon"
@ -265,6 +266,7 @@ func TestHandler_Overview(t *testing.T) {
Tracing: &static.Tracing{
Jaeger: &jaeger.Config{},
},
Hub: &hub.Provider{},
},
confDyn: runtime.Configuration{},
expected: expected{

View file

@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View file

@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View file

@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "Prometheus",
"tracing": "Jaeger"
"tracing": "Jaeger",
"hub": true
},
"http": {
"middlewares": {
@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View file

@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@ -60,4 +61,4 @@
"warnings": 0
}
}
}
}

View file

@ -9,4 +9,5 @@ type Experimental struct {
KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"`
Hub bool `description:"Enable the Traefik Hub provider." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" export:"true"`
}

View file

@ -1,6 +1,7 @@
package static
import (
"errors"
"fmt"
stdlog "log"
"strings"
@ -17,6 +18,7 @@ import (
"github.com/traefik/traefik/v2/pkg/provider/ecs"
"github.com/traefik/traefik/v2/pkg/provider/file"
"github.com/traefik/traefik/v2/pkg/provider/http"
"github.com/traefik/traefik/v2/pkg/provider/hub"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/gateway"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
@ -79,6 +81,8 @@ type Configuration struct {
// Deprecated.
Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"`
Hub *hub.Provider `description:"Traefik Hub configuration." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
}
@ -197,10 +201,14 @@ type Providers struct {
// It also takes care of maintaining backwards compatibility.
func (c *Configuration) SetEffectiveConfiguration() {
// Creates the default entry point if needed
if len(c.EntryPoints) == 0 {
if len(c.EntryPoints) == 0 || (c.Hub != nil && len(c.EntryPoints) == 1 && c.EntryPoints[c.Hub.EntryPoint] != nil) {
ep := &EntryPoint{Address: ":80"}
ep.SetDefaults()
c.EntryPoints = EntryPoints{"http": ep}
// TODO: double check this tomorrow
if c.EntryPoints == nil {
c.EntryPoints = make(EntryPoints)
}
c.EntryPoints["http"] = ep
}
// Creates the internal traefik entry point if needed
@ -215,6 +223,15 @@ func (c *Configuration) SetEffectiveConfiguration() {
}
}
if c.Hub != nil {
if err := c.initHubProvider(); err != nil {
c.Hub = nil
log.WithoutContext().Errorf("Unable to activate the Hub provider: %v", err)
} else {
log.WithoutContext().Debugf("Experimental Hub provider has been activated.")
}
}
if c.Providers.Docker != nil {
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
@ -280,6 +297,46 @@ func (c *Configuration) initACMEProvider() {
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
}
func (c *Configuration) initHubProvider() error {
// Hub provider is an experimental feature. Require the experimental flag to be enabled before continuing.
if c.Experimental == nil || !c.Experimental.Hub {
return errors.New("experimental flag for Hub not set")
}
if c.Hub.TLS == nil {
return errors.New("no TLS configuration defined for Hub")
}
if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") {
return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive")
}
if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") {
return errors.New("incomplete mTLS configuration for Hub")
}
if c.Hub.TLS.Insecure {
log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.")
}
// Creates the internal Hub entry point if needed.
if c.Hub.EntryPoint == hub.DefaultEntryPointName {
if _, ok := c.EntryPoints[hub.DefaultEntryPointName]; !ok {
var ep EntryPoint
ep.SetDefaults()
ep.Address = ":9900"
c.EntryPoints[hub.DefaultEntryPointName] = &ep
log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.DefaultEntryPointName)
}
}
c.EntryPoints[c.Hub.EntryPoint].HTTP.TLS = &TLSConfig{
Options: "traefik-hub",
}
return nil
}
// ValidateConfiguration validate that configuration is coherent.
func (c *Configuration) ValidateConfiguration() error {
var acmeEmail string

146
pkg/provider/hub/handler.go Normal file
View file

@ -0,0 +1,146 @@
package hub
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync/atomic"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
)
type handler struct {
mux *http.ServeMux
client http.Client
entryPoint string
port int
tlsCfg *TLS
// Accessed atomically.
lastCfgUnixNano int64
cfgChan chan<- dynamic.Message
}
func newHandler(entryPoint string, port int, cfgChan chan<- dynamic.Message, tlsCfg *TLS, client http.Client) http.Handler {
h := &handler{
mux: http.NewServeMux(),
entryPoint: entryPoint,
port: port,
cfgChan: cfgChan,
tlsCfg: tlsCfg,
client: client,
}
h.mux.HandleFunc("/config", h.handleConfig)
h.mux.HandleFunc("/discover-ip", h.handleDiscoverIP)
h.mux.HandleFunc("/state", h.handleState)
return h
}
type configRequest struct {
UnixNano int64 `json:"unixNano"`
Configuration *dynamic.Configuration `json:"configuration"`
}
func (h *handler) handleConfig(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
payload := &configRequest{Configuration: emptyDynamicConfiguration()}
if err := json.NewDecoder(req.Body).Decode(payload); err != nil {
err = fmt.Errorf("decoding config request: %w", err)
log.WithoutContext().Errorf("Handling config: %v", err)
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
cfg := payload.Configuration
patchDynamicConfiguration(cfg, h.entryPoint, h.port, h.tlsCfg)
// We can safely drop messages here if the other end is not ready to receive them
// as the agent will re-apply the same configuration.
select {
case h.cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}:
atomic.StoreInt64(&h.lastCfgUnixNano, payload.UnixNano)
default:
}
}
func (h *handler) handleDiscoverIP(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
xff := req.Header.Get("X-Forwarded-For")
port := req.URL.Query().Get("port")
nonce := req.URL.Query().Get("nonce")
if err := h.doDiscoveryReq(req.Context(), xff, port, nonce); err != nil {
err = fmt.Errorf("doing discovery request: %w", err)
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
return
}
if err := json.NewEncoder(rw).Encode(xff); err != nil {
err = fmt.Errorf("encoding discover ip response: %w", err)
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func (h *handler) doDiscoveryReq(ctx context.Context, ip, port, nonce string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s:%s", ip, port), http.NoBody)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
q := make(url.Values)
q.Set("nonce", nonce)
req.URL.RawQuery = q.Encode()
req.Host = "agent.traefik"
resp, err := h.client.Do(req)
if err != nil {
return fmt.Errorf("doing request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
return nil
}
type stateResponse struct {
LastConfigUnixNano int64 `json:"lastConfigUnixNano"`
}
func (h *handler) handleState(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
resp := stateResponse{
LastConfigUnixNano: atomic.LoadInt64(&h.lastCfgUnixNano),
}
if err := json.NewEncoder(rw).Encode(resp); err != nil {
err = fmt.Errorf("encoding last config received response: %w", err)
log.WithoutContext().Errorf("Handling state: %v", err)
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
}
func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
h.mux.ServeHTTP(rw, req)
}

View file

@ -0,0 +1,168 @@
package hub
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/tls/generate"
)
func TestHandleConfig(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
cfg := emptyDynamicConfiguration()
cfg.HTTP.Routers["foo"] = &dynamic.Router{
EntryPoints: []string{"ep"},
Service: "bar",
Rule: "Host(`foo.com`)",
}
req := configRequest{Configuration: cfg}
b, err := json.Marshal(req)
require.NoError(t, err)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Post(server.URL+"/config", "application/json", bytes.NewReader(b))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
select {
case gotCfgRaw := <-cfgChan:
patchDynamicConfiguration(cfg, "traefik-hub-ep", 42, nil)
assert.Equal(t, cfg, gotCfgRaw.Configuration)
case <-time.After(time.Second):
t.Fatal("Configuration not received")
}
}
func TestHandle_Config_MethodNotAllowed(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Get(server.URL + "/config")
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
}
func TestHandle_DiscoverIP(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := listener.Addr().(*net.TCPAddr).Port
nonce := "XVlBzgbaiCMRAjWw"
mux := http.NewServeMux()
var handlerCallCount int
mux.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) {
handlerCallCount++
assert.Equal(t, nonce, req.URL.Query().Get("nonce"))
})
certificate, err := generate.DefaultCertificate()
require.NoError(t, err)
agentServer := &http.Server{
Handler: mux,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{*certificate},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
},
}
t.Cleanup(func() { _ = agentServer.Close() })
rdy := make(chan struct{})
go func(s *http.Server) {
close(rdy)
if err = s.ServeTLS(listener, "", ""); errors.Is(err, http.ErrServerClosed) {
return
}
}(agentServer)
<-rdy
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
traefikServer := httptest.NewServer(h)
t.Cleanup(traefikServer.Close)
req, err := http.NewRequest(http.MethodGet, traefikServer.URL+"/discover-ip", http.NoBody)
require.NoError(t, err)
q := make(url.Values)
q.Set("port", strconv.Itoa(port))
q.Set("nonce", nonce)
req.URL.RawQuery = q.Encode()
// Simulate a call from behind different proxies.
req.Header.Add("X-Forwarded-For", "127.0.0.1")
req.Header.Add("X-Forwarded-For", "10.10.0.13")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() {
err = resp.Body.Close()
require.NoError(t, err)
}()
assert.Equal(t, 1, handlerCallCount)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var ip string
err = json.NewDecoder(resp.Body).Decode(&ip)
require.NoError(t, err)
assert.Equal(t, "127.0.0.1", ip)
}
func TestHandle_DiscoverIP_MethodNotAllowed(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Post(server.URL+"/discover-ip", "", http.NoBody)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
}

215
pkg/provider/hub/hub.go Normal file
View file

@ -0,0 +1,215 @@
package hub
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe"
ttls "github.com/traefik/traefik/v2/pkg/tls"
)
var _ provider.Provider = (*Provider)(nil)
// DefaultEntryPointName is the name of the default internal entry point.
const DefaultEntryPointName = "traefik-hub"
// Provider holds configurations of the provider.
type Provider struct {
EntryPoint string `description:"Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty" export:"true"`
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
server *http.Server
}
// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent.
type TLS struct {
Insecure bool `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
CA ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"`
Cert ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"`
Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.EntryPoint = DefaultEntryPointName
}
// Init the provider.
func (p *Provider) Init() error {
return nil
}
// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("listener: %w", err)
}
port := listener.Addr().(*net.TCPAddr).Port
client, err := createAgentClient(p.TLS)
if err != nil {
return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
}
p.server = &http.Server{Handler: newHandler(p.EntryPoint, port, configurationChan, p.TLS, client)}
// TODO: this is going to be leaky (because no context to make it terminate)
// if/when Provide lifecycle differs with Traefik lifecycle.
go func() {
if err = p.server.Serve(listener); err != nil {
log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err)
return
}
}()
exposeAPIAndMetrics(configurationChan, p.EntryPoint, port, p.TLS)
return nil
}
func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) {
cfg := emptyDynamicConfiguration()
patchDynamicConfiguration(cfg, ep, port, tlsCfg)
cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}
}
func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) {
cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "api@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/api`)",
}
cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "prometheus@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/metrics`)",
}
cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "traefik-hub-agent-service",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
}
cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: fmt.Sprintf("http://127.0.0.1:%d", port),
},
},
},
}
if tlsCfg == nil {
return
}
if tlsCfg.Insecure {
cfg.TLS.Options["traefik-hub"] = ttls.Options{
MinVersion: "VersionTLS13",
}
return
}
cfg.TLS.Options["traefik-hub"] = ttls.Options{
ClientAuth: ttls.ClientAuth{
CAFiles: []ttls.FileOrContent{tlsCfg.CA},
ClientAuthType: "RequireAndVerifyClientCert",
},
SniStrict: true,
MinVersion: "VersionTLS13",
}
cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{
Certificate: ttls.Certificate{
CertFile: tlsCfg.Cert,
KeyFile: tlsCfg.Key,
},
})
}
func emptyDynamicConfiguration() *dynamic.Configuration {
return &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]ttls.Store),
Options: make(map[string]ttls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
}
func createAgentClient(tlsCfg *TLS) (http.Client, error) {
var client http.Client
if tlsCfg.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}
caContent, err := tlsCfg.CA.Read()
if err != nil {
return client, fmt.Errorf("reading CA: %w", err)
}
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(caContent); !ok {
return client, errors.New("appending CA error")
}
certContent, err := tlsCfg.Cert.Read()
if err != nil {
return client, fmt.Errorf("reading Cert: %w", err)
}
keyContent, err := tlsCfg.Key.Read()
if err != nil {
return client, fmt.Errorf("reading Key: %w", err)
}
certificate, err := tls.X509KeyPair(certContent, keyContent)
if err != nil {
return client, fmt.Errorf("creating key pair: %w", err)
}
// mTLS
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{certificate},
ServerName: "agent.traefik",
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}

View file

@ -0,0 +1,10 @@
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g fill="#dfdfdf">
<path d="M128 0H0v128h128V0z"/>
<path d="M48 0H0v48h48V0zM128 0H80v48h48V0zM128 80H80v48h48V80zM48 80H0v48h48V80z"/>
</g>
<path d="M27.429 27.429h73.142v73.142H27.43V27.43z"/>
<path d="m65.323 31.205 16.89 9.164a2.905 2.905 0 0 1 1.516 2.557l-.014 14.763 15.34 8.323a2.905 2.905 0 0 1 1.516 2.558l-.016 16.47a2.905 2.905 0 0 1-1.51 2.55L82.17 96.792a2.873 2.873 0 0 1-2.776-.013l-15.279-8.512-15.63 8.525a2.873 2.873 0 0 1-2.775-.013l-16.794-9.356a2.906 2.906 0 0 1-1.488-2.539V68.606c0-1.063.577-2.041 1.505-2.55l15.337-8.403v-14.69c0-1.064.577-2.041 1.505-2.55l16.794-9.202a2.873 2.873 0 0 1 2.753-.006zM79.888 63.21l-11.997 6.57a1.937 1.937 0 0 0-1.004 1.7v10.554c0 .704.38 1.352.992 1.693l12.006 6.688c.575.32 1.273.324 1.85.009l12.042-6.566a1.937 1.937 0 0 0 1.007-1.7l.01-10.703c0-.712-.388-1.367-1.01-1.705l-12.061-6.544a1.916 1.916 0 0 0-1.835.004zm-33.69 0-11.991 6.57a1.937 1.937 0 0 0-1.004 1.7v10.554c0 .704.38 1.352.992 1.693l12.006 6.688c.575.32 1.273.324 1.85.009l12.041-6.566a1.937 1.937 0 0 0 1.007-1.7l.01-10.672c.001-.71-.385-1.365-1.007-1.703l-12.066-6.576a1.916 1.916 0 0 0-1.838.003zm16.844-25.645L51.05 44.137a1.937 1.937 0 0 0-1.004 1.699v10.552c0 .705.381 1.355.996 1.695l12.036 6.673c.575.319 1.271.321 1.848.007l12.01-6.554a1.937 1.937 0 0 0 1.007-1.699l.01-10.699c0-.712-.388-1.367-1.01-1.705l-12.065-6.545a1.916 1.916 0 0 0-1.835.004z" fill="#7F8C2B" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB