From fbdb6e6e78afd609930831642889d515a19c3d18 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou <925513+jbdoumenjou@users.noreply.github.com> Date: Thu, 24 Mar 2022 19:44:08 +0100 Subject: [PATCH] Add Traefik Hub Integration (Experimental Feature) --- cmd/traefik/traefik.go | 23 +- .../reference/static-configuration/cli-ref.md | 21 ++ .../reference/static-configuration/env-ref.md | 21 ++ .../reference/static-configuration/file.toml | 9 + .../reference/static-configuration/file.yaml | 8 + docs/content/traefik-hub/index.md | 294 ++++++++++++++++++ docs/mkdocs.yml | 1 + docs/requirements.txt | 1 + pkg/api/handler_overview.go | 2 + pkg/api/handler_overview_test.go | 2 + pkg/api/testdata/overview-dynamic.json | 5 +- pkg/api/testdata/overview-empty.json | 5 +- pkg/api/testdata/overview-features.json | 5 +- pkg/api/testdata/overview-providers.json | 5 +- pkg/config/static/experimental.go | 1 + pkg/config/static/static_config.go | 61 +++- pkg/provider/hub/handler.go | 146 +++++++++ pkg/provider/hub/handler_test.go | 168 ++++++++++ pkg/provider/hub/hub.go | 215 +++++++++++++ webui/src/statics/providers/hub.svg | 10 + 20 files changed, 992 insertions(+), 11 deletions(-) create mode 100644 docs/content/traefik-hub/index.md create mode 100644 pkg/provider/hub/handler.go create mode 100644 pkg/provider/hub/handler_test.go create mode 100644 pkg/provider/hub/hub.go create mode 100644 webui/src/statics/providers/hub.svg diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 4a7f49d25..875eff503 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -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. diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 340ae8235..9345cded4 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -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```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 9abba9e00..340917400 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -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```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 770346235..f84a975d6 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -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" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 95f1bf012..260734fd0 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -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 diff --git a/docs/content/traefik-hub/index.md b/docs/content/traefik-hub/index.md new file mode 100644 index 000000000..a784f2c0b --- /dev/null +++ b/docs/content/traefik-hub/index.md @@ -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 +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 438c9a194..bb239c9d9 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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' diff --git a/docs/requirements.txt b/docs/requirements.txt index c9c00839c..25c7ff97f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -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 diff --git a/pkg/api/handler_overview.go b/pkg/api/handler_overview.go index 4e6485e7d..d04abbc87 100644 --- a/pkg/api/handler_overview.go +++ b/pkg/api/handler_overview.go @@ -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, } } diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index 6dee89a72..8937c0204 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -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{ diff --git a/pkg/api/testdata/overview-dynamic.json b/pkg/api/testdata/overview-dynamic.json index abe3e1300..c6790c2dd 100644 --- a/pkg/api/testdata/overview-dynamic.json +++ b/pkg/api/testdata/overview-dynamic.json @@ -2,7 +2,8 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "" + "tracing": "", + "hub": false }, "http": { "middlewares": { @@ -50,4 +51,4 @@ "warnings": 0 } } -} \ No newline at end of file +} diff --git a/pkg/api/testdata/overview-empty.json b/pkg/api/testdata/overview-empty.json index 2a1f7251b..cd2a1611f 100644 --- a/pkg/api/testdata/overview-empty.json +++ b/pkg/api/testdata/overview-empty.json @@ -2,7 +2,8 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "" + "tracing": "", + "hub": false }, "http": { "middlewares": { @@ -50,4 +51,4 @@ "warnings": 0 } } -} \ No newline at end of file +} diff --git a/pkg/api/testdata/overview-features.json b/pkg/api/testdata/overview-features.json index 37ee19760..4ad3a1b91 100644 --- a/pkg/api/testdata/overview-features.json +++ b/pkg/api/testdata/overview-features.json @@ -2,7 +2,8 @@ "features": { "accessLog": false, "metrics": "Prometheus", - "tracing": "Jaeger" + "tracing": "Jaeger", + "hub": true }, "http": { "middlewares": { @@ -50,4 +51,4 @@ "warnings": 0 } } -} \ No newline at end of file +} diff --git a/pkg/api/testdata/overview-providers.json b/pkg/api/testdata/overview-providers.json index dedce0e46..95d4d10d2 100644 --- a/pkg/api/testdata/overview-providers.json +++ b/pkg/api/testdata/overview-providers.json @@ -2,7 +2,8 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "" + "tracing": "", + "hub": false }, "http": { "middlewares": { @@ -60,4 +61,4 @@ "warnings": 0 } } -} \ No newline at end of file +} diff --git a/pkg/config/static/experimental.go b/pkg/config/static/experimental.go index cb6cd097f..7f0c813ac 100644 --- a/pkg/config/static/experimental.go +++ b/pkg/config/static/experimental.go @@ -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"` } diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 277acb6bc..961786f94 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -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 diff --git a/pkg/provider/hub/handler.go b/pkg/provider/hub/handler.go new file mode 100644 index 000000000..328bf58b5 --- /dev/null +++ b/pkg/provider/hub/handler.go @@ -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) +} diff --git a/pkg/provider/hub/handler_test.go b/pkg/provider/hub/handler_test.go new file mode 100644 index 000000000..b69fc8b6d --- /dev/null +++ b/pkg/provider/hub/handler_test.go @@ -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) +} diff --git a/pkg/provider/hub/hub.go b/pkg/provider/hub/hub.go new file mode 100644 index 000000000..1643a0980 --- /dev/null +++ b/pkg/provider/hub/hub.go @@ -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 +} diff --git a/webui/src/statics/providers/hub.svg b/webui/src/statics/providers/hub.svg new file mode 100644 index 000000000..1df28d7a8 --- /dev/null +++ b/webui/src/statics/providers/hub.svg @@ -0,0 +1,10 @@ + + + + + + + + + +