diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 42b48b2d8..a8216d48b 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -19,6 +19,7 @@ import ( "github.com/go-acme/lego/v4/challenge" gokitmetrics "github.com/go-kit/kit/metrics" "github.com/sirupsen/logrus" + "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/traefik/paerser/cli" "github.com/traefik/traefik/v2/cmd" "github.com/traefik/traefik/v2/cmd/healthcheck" @@ -257,7 +258,28 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err // Service manager factory - roundTripperManager := service.NewRoundTripperManager() + var spiffeX509Source *workloadapi.X509Source + if staticConfiguration.Spiffe != nil && staticConfiguration.Spiffe.WorkloadAPIAddr != "" { + log.WithoutContext(). + WithField("workloadAPIAddr", staticConfiguration.Spiffe.WorkloadAPIAddr). + Info("Waiting on SPIFFE SVID delivery") + + spiffeX509Source, err = workloadapi.NewX509Source( + ctx, + workloadapi.WithClientOptions( + workloadapi.WithAddr( + staticConfiguration.Spiffe.WorkloadAPIAddr, + ), + ), + ) + if err != nil { + return nil, fmt.Errorf("unable to create SPIFFE x509 source: %w", err) + } + + log.WithoutContext().Info("Successfully obtained SPIFFE SVID.") + } + + roundTripperManager := service.NewRoundTripperManager(spiffeX509Source) acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider) managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry, roundTripperManager, acmeHTTPHandler) diff --git a/docs/content/https/spiffe.md b/docs/content/https/spiffe.md new file mode 100644 index 000000000..9d70e5a20 --- /dev/null +++ b/docs/content/https/spiffe.md @@ -0,0 +1,54 @@ +--- +title: "Traefik SPIFFE Documentation" +description: "Learn how to configure Traefik to use SPIFFE. Read the technical documentation." +--- + +# SPIFFE + +Secure the backend connection with SPIFFE. +{: .subtitle } + +[SPIFFE](https://spiffe.io/docs/latest/spiffe-about/overview/) (Secure Production Identity Framework For Everyone), +provides a secure identity in the form of a specially crafted X.509 certificate, +to every workload in an environment. + +Traefik is able to connect to the Workload API to obtain an x509-SVID used to secure the connection with SPIFFE enabled backends. + +## Configuration + +### General + +Enabling SPIFFE is part of the [static configuration](../getting-started/configuration-overview.md#the-static-configuration). +It can be defined by using a file (YAML or TOML) or CLI arguments. + +### Workload API + +The `workloadAPIAddr` configuration defines the address of the SPIFFE [Workload API](https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/#spiffe-workload-api). + +!!! info "Enabling SPIFFE in ServersTransports" + + Enabling SPIFFE does not imply that backend connections are going to use it automatically. + Each [ServersTransport](../routing/services/index.md#serverstransport_1) that is meant to be secured with SPIFFE must [explicitly](../routing/services/index.md#spiffe) enable it. + +!!! warning "SPIFFE can cause Traefik to stall" + When using SPIFFE, + Traefik will wait for the first SVID to be delivered before starting. + If Traefik is hanging when waiting on SPIFFE SVID delivery, + please double check that it is correctly registered as workload in your SPIFFE infrastructure. + +```yaml tab="File (YAML)" +## Static configuration +spiffe: + workloadAPIAddr: localhost +``` + +```toml tab="File (TOML)" +## Static configuration +[spiffe] + workloadAPIAddr: localhost +``` + +```bash tab="CLI" +## Static configuration +--spiffe.workloadAPIAddr=localhost +``` diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 4634afea2..c197e75cb 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -301,12 +301,18 @@ [[http.serversTransports.ServersTransport0.certificates]] certFile = "foobar" keyFile = "foobar" + [http.serversTransports.ServersTransport0.forwardingTimeouts] dialTimeout = "42s" responseHeaderTimeout = "42s" idleConnTimeout = "42s" readIdleTimeout = "42s" pingTimeout = "42s" + + [http.serversTransports.ServersTransport0.spiffe] + ids = ["foobar", "foobar"] + trustDomain = "foobar" + [http.serversTransports.ServersTransport1] serverName = "foobar" insecureSkipVerify = true @@ -322,6 +328,7 @@ [[http.serversTransports.ServersTransport1.certificates]] certFile = "foobar" keyFile = "foobar" + [http.serversTransports.ServersTransport1.forwardingTimeouts] dialTimeout = "42s" responseHeaderTimeout = "42s" @@ -329,6 +336,10 @@ readIdleTimeout = "42s" pingTimeout = "42s" + [http.serversTransports.ServersTransport1.spiffe] + ids = ["foobar", "foobar"] + trustDomain = "foobar" + [tcp] [tcp.routers] [tcp.routers.TCPRouter0] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 807bc8a14..fc446ec0f 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -345,6 +345,12 @@ http: pingTimeout: 42s disableHTTP2: true peerCertURI: foobar + spiffe: + ids: + - foobar + - foobar + trustDomain: foobar + ServersTransport1: serverName: foobar insecureSkipVerify: true @@ -365,6 +371,12 @@ http: pingTimeout: 42s disableHTTP2: true peerCertURI: foobar + spiffe: + ids: + - foobar + - foobar + trustDomain: foobar + tcp: routers: TCPRouter0: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 17dc336f6..983a678b8 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1679,6 +1679,19 @@ spec: description: ServerName defines the server name used to contact the server. type: string + spiffe: + description: Spiffe defines the SPIFFE configuration. + properties: + ids: + description: IDs defines the allowed SPIFFE IDs (takes precedence + over the SPIFFE TrustDomain). + items: + type: string + type: array + trustDomain: + description: TrustDomain defines the allowed SPIFFE trust domain. + type: string + type: object type: object required: - metadata diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index f72e024e4..d83a15476 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -186,6 +186,9 @@ | `traefik/http/serversTransports/ServersTransport0/rootCAs/0` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/rootCAs/1` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/serverName` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/spiffe/ids/0` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/spiffe/ids/1` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/spiffe/trustDomain` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/certificates/0/certFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/certificates/0/keyFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/certificates/1/certFile` | `foobar` | @@ -202,6 +205,9 @@ | `traefik/http/serversTransports/ServersTransport1/rootCAs/0` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/rootCAs/1` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/serverName` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/spiffe/ids/0` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/spiffe/ids/1` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/spiffe/trustDomain` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/followRedirects` | `true` | | `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml index afc038496..f23d345ce 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml @@ -113,6 +113,19 @@ spec: description: ServerName defines the server name used to contact the server. type: string + spiffe: + description: Spiffe defines the SPIFFE configuration. + properties: + ids: + description: IDs defines the allowed SPIFFE IDs (takes precedence + over the SPIFFE TrustDomain). + items: + type: string + type: array + trustDomain: + description: TrustDomain defines the allowed SPIFFE trust domain. + type: string + type: object type: object required: - metadata diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 444a2ce84..eb7831f52 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -984,6 +984,18 @@ If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, D `--serverstransport.rootcas`: Add cert file for self-signed certificate. +`--serverstransport.spiffe`: +Defines the SPIFFE configuration. (Default: ```false```) + +`--serverstransport.spiffe.ids`: +Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). + +`--serverstransport.spiffe.trustdomain`: +Defines the allowed SPIFFE trust domain. + +`--spiffe.workloadapiaddr`: +Defines the workload API address. + `--tracing`: OpenTracing configuration. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 3cae404b1..e23990497 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -984,6 +984,18 @@ If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, D `TRAEFIK_SERVERSTRANSPORT_ROOTCAS`: Add cert file for self-signed certificate. +`TRAEFIK_SERVERSTRANSPORT_SPIFFE`: +Defines the SPIFFE configuration. (Default: ```false```) + +`TRAEFIK_SERVERSTRANSPORT_SPIFFE_IDS`: +Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). + +`TRAEFIK_SERVERSTRANSPORT_SPIFFE_TRUSTDOMAIN`: +Defines the allowed SPIFFE trust domain. + +`TRAEFIK_SPIFFE_WORKLOADAPIADDR`: +Defines the workload API address. + `TRAEFIK_TRACING`: OpenTracing configuration. (Default: ```false```) diff --git a/docs/content/routing/overview.md b/docs/content/routing/overview.md index b49c9748b..3816c1e70 100644 --- a/docs/content/routing/overview.md +++ b/docs/content/routing/overview.md @@ -325,6 +325,61 @@ serversTransport: --serversTransport.maxIdleConnsPerHost=7 ``` +### `spiffe` + +Please note that [SPIFFE](../https/spiffe.md) must be enabled in the static configuration +before using it to secure the connection between Traefik and the backends. + +#### `spiffe.ids` + +_Optional_ + +`ids` defines the allowed SPIFFE IDs. +This takes precedence over the SPIFFE TrustDomain. + +```yaml tab="File (YAML)" +## Static configuration +serversTransport: + spiffe: + ids: + - spiffe://trust-domain/id1 + - spiffe://trust-domain/id2 +``` + +```toml tab="File (TOML)" +## Static configuration +[serversTransport.spiffe] + ids = ["spiffe://trust-domain/id1", "spiffe://trust-domain/id2"] +``` + +```bash tab="CLI" +## Static configuration +--serversTransport.spiffe.ids=spiffe://trust-domain/id1,spiffe://trust-domain/id2 +``` + +#### `spiffe.trustDomain` + +_Optional_ + +`trustDomain` defines the allowed SPIFFE trust domain. + +```yaml tab="File (YAML)" +## Static configuration +serversTransport: + trustDomain: spiffe://trust-domain +``` + +```toml tab="File (TOML)" +## Static configuration +[serversTransport.spiffe] + trustDomain = "spiffe://trust-domain" +``` + +```bash tab="CLI" +## Static configuration +--serversTransport.spiffe.trustDomain=spiffe://trust-domain +``` + ### `forwardingTimeouts` `forwardingTimeouts` is about a number of timeouts relevant to when forwarding requests to the backend servers. diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 7186765f1..800471cf8 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -776,6 +776,82 @@ spec: peerCertURI: foobar ``` +#### `spiffe` + +Please note that [SPIFFE](../../https/spiffe.md) must be enabled in the static configuration +before using it to secure the connection between Traefik and the backends. + +##### `spiffe.ids` + +_Optional_ + +`ids` defines the allowed SPIFFE IDs. +This takes precedence over the SPIFFE TrustDomain. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + spiffe: + ids: + - spiffe://trust-domain/id1 + - spiffe://trust-domain/id2 +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport.spiffe] + ids = ["spiffe://trust-domain/id1", "spiffe://trust-domain/id2"] +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default + +spec: + spiffe: + ids: + - spiffe://trust-domain/id1 + - spiffe://trust-domain/id2 +``` + +##### `spiffe.trustDomain` + +_Optional_ + +`trustDomain` defines the allowed SPIFFE trust domain. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + spiffe: + trustDomain: spiffe://trust-domain +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport.spiffe] + trustDomain = "spiffe://trust-domain" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default + +spec: + spiffe: + trustDomain: "spiffe://trust-domain" +``` + #### `forwardingTimeouts` `forwardingTimeouts` are the timeouts applied when forwarding requests to the servers. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 218154bc3..549fa367c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -110,6 +110,7 @@ nav: - 'TLS': 'https/tls.md' - 'Let''s Encrypt': 'https/acme.md' - 'Tailscale': 'https/tailscale.md' + - 'SPIFFE': 'https/spiffe.md' - 'Middlewares': - 'Overview': 'middlewares/overview.md' - 'HTTP': diff --git a/go.mod b/go.mod index 35ee71c30..6377a2a2c 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac github.com/sirupsen/logrus v1.8.1 + github.com/spiffe/go-spiffe/v2 v2.1.1 github.com/stretchr/testify v1.8.0 github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 @@ -76,7 +77,7 @@ require ( golang.org/x/text v0.3.7 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 - google.golang.org/grpc v1.41.0 + google.golang.org/grpc v1.46.0 gopkg.in/DataDog/dd-trace-go.v1 v1.38.1 gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/yaml.v3 v3.0.1 @@ -111,7 +112,7 @@ require ( github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/hcsshim v0.8.23 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/Shopify/sarama v1.23.1 // indirect @@ -311,6 +312,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect + github.com/zeebo/errs v1.2.2 // indirect go.elastic.co/apm/module/apmhttp v1.13.1 // indirect go.elastic.co/fastjson v1.1.0 // indirect go.etcd.io/etcd/api/v3 v3.5.4 // indirect diff --git a/go.sum b/go.sum index e15dae5df..9ae168cae 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,9 @@ github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JP github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -338,8 +339,12 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= @@ -629,6 +634,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= @@ -1890,6 +1896,8 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spiffe/go-spiffe/v2 v2.1.1 h1:RT9kM8MZLZIsPTH+HKQEP5yaAk3yd/VBzlINaRjXs8k= +github.com/spiffe/go-spiffe/v2 v2.1.1/go.mod h1:5qg6rpqlwIub0JAiF1UK9IMD6BpPTmvG6yfSgDBs5lg= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -2059,6 +2067,8 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= github.com/zclconf/go-cty v1.7.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o= +github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g= +github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -2676,6 +2686,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2724,8 +2735,10 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2794,6 +2807,7 @@ gopkg.in/olivere/elastic.v5 v5.0.84/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUr gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 17dc336f6..983a678b8 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1679,6 +1679,19 @@ spec: description: ServerName defines the server name used to contact the server. type: string + spiffe: + description: Spiffe defines the SPIFFE configuration. + properties: + ids: + description: IDs defines the allowed SPIFFE IDs (takes precedence + over the SPIFFE TrustDomain). + items: + type: string + type: array + trustDomain: + description: TrustDomain defines the allowed SPIFFE trust domain. + type: string + type: object type: object required: - metadata diff --git a/integration/grpc_test.go b/integration/grpc_test.go index 496f4b371..ab1c7f899 100644 --- a/integration/grpc_test.go +++ b/integration/grpc_test.go @@ -16,6 +16,7 @@ import ( "github.com/traefik/traefik/v2/pkg/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" ) var ( @@ -94,7 +95,7 @@ func getHelloClientGRPC() (helloworld.GreeterClient, func() error, error) { } func getHelloClientGRPCh2c() (helloworld.GreeterClient, func() error, error) { - conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithInsecure()) + conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, func() error { return nil }, err } diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 376223e1c..f93c1a6a3 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -250,6 +250,17 @@ type ServersTransport struct { ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` DisableHTTP2 bool `description:"Disable HTTP/2 for connections with backend servers." json:"disableHTTP2,omitempty" toml:"disableHTTP2,omitempty" yaml:"disableHTTP2,omitempty" export:"true"` PeerCertURI string `description:"URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty" export:"true"` + Spiffe *Spiffe `description:"Define the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` +} + +// +k8s:deepcopy-gen=true + +// Spiffe holds the SPIFFE configuration. +type Spiffe struct { + // IDs defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). + IDs []string `description:"Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain)." json:"ids,omitempty" toml:"ids,omitempty" yaml:"ids,omitempty"` + // TrustDomain defines the allowed SPIFFE trust domain. + TrustDomain string `description:"Defines the allowed SPIFFE trust domain." json:"trustDomain,omitempty" yaml:"trustDomain,omitempty" toml:"trustDomain,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 587048180..adba5a4ee 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1161,6 +1161,11 @@ func (in *ServersTransport) DeepCopyInto(out *ServersTransport) { *out = new(ForwardingTimeouts) **out = **in } + if in.Spiffe != nil { + in, out := &in.Spiffe, &out.Spiffe + *out = new(Spiffe) + (*in).DeepCopyInto(*out) + } return } @@ -1231,6 +1236,27 @@ func (in *SourceCriterion) DeepCopy() *SourceCriterion { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spiffe) DeepCopyInto(out *Spiffe) { + *out = *in + if in.IDs != nil { + in, out := &in.IDs, &out.IDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spiffe. +func (in *Spiffe) DeepCopy() *Spiffe { + if in == nil { + return nil + } + out := new(Spiffe) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Sticky) DeepCopyInto(out *Sticky) { *out = *in diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 4d803b2ad..4a2558b8b 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -84,6 +84,13 @@ type Configuration struct { 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"` + + Spiffe *SpiffeClientConfig `description:"SPIFFE integration configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" export:"true"` +} + +// SpiffeClientConfig defines the SPIFFE client configuration. +type SpiffeClientConfig struct { + WorkloadAPIAddr string `description:"Defines the workload API address." json:"workloadAPIAddr,omitempty" toml:"workloadAPIAddr,omitempty" yaml:"workloadAPIAddr,omitempty"` } // CertificateResolver contains the configuration for the different types of certificates resolver. @@ -104,6 +111,13 @@ type ServersTransport struct { RootCAs []tls.FileOrContent `description:"Add cert file for self-signed certificate." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` + Spiffe *Spiffe `description:"Defines the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` +} + +// Spiffe holds the SPIFFE configuration. +type Spiffe struct { + IDs []string `description:"Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain)." json:"ids,omitempty" toml:"ids,omitempty" yaml:"ids,omitempty"` + TrustDomain string `description:"Defines the allowed SPIFFE trust domain." json:"trustDomain,omitempty" yaml:"trustDomain,omitempty" toml:"trustDomain,omitempty"` } // API holds the API configuration. diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index 36178ce98..d3dc851bd 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -21,6 +21,7 @@ import ( "github.com/vulcand/oxy/roundrobin" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" ) @@ -317,7 +318,7 @@ func checkHealthGRPC(serverURL *url.URL, backend *BackendConfig) error { var opts []grpc.DialOption switch backend.Options.Scheme { case "http", "h2c", "": - opts = append(opts, grpc.WithInsecure()) + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) } ctx, cancel := context.WithTimeout(context.Background(), backend.Options.Timeout) diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml index 04d9b4e05..fdbf7b930 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml @@ -112,6 +112,11 @@ spec: idleConnTimeout: 42ms readIdleTimeout: 42s pingTimeout: 42s + spiffe: + ids: + - spiffe://foo/buz + - spiffe://bar/biz + trustDomain: spiffe://lol --- apiVersion: traefik.containo.us/v1alpha1 @@ -144,4 +149,3 @@ spec: - name: whoamitls port: 443 serversTransport: default-test - diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index a43345e67..ff6c6b588 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -381,6 +381,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) MaxIdleConnsPerHost: serversTransport.Spec.MaxIdleConnsPerHost, ForwardingTimeouts: forwardingTimeout, PeerCertURI: serversTransport.Spec.PeerCertURI, + Spiffe: serversTransport.Spec.Spiffe, } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 7465c8fae..60c079406 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3916,6 +3916,13 @@ func TestLoadIngressRoutes(t *testing.T) { PingTimeout: ptypes.Duration(42 * time.Second), }, PeerCertURI: "foo://bar", + Spiffe: &dynamic.Spiffe{ + IDs: []string{ + "spiffe://foo/buz", + "spiffe://bar/biz", + }, + TrustDomain: "spiffe://lol", + }, }, "default-test": { ServerName: "test", diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go index 20bc7d18a..10486a426 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/traefik/traefik/v2/pkg/config/dynamic" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -42,6 +43,8 @@ type ServersTransportSpec struct { DisableHTTP2 bool `json:"disableHTTP2,omitempty"` // PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. PeerCertURI string `json:"peerCertURI,omitempty"` + // Spiffe defines the SPIFFE configuration. + Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 5cc7903fc..5dbd71621 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -1142,6 +1142,11 @@ func (in *ServersTransportSpec) DeepCopyInto(out *ServersTransportSpec) { *out = new(ForwardingTimeouts) (*in).DeepCopyInto(*out) } + if in.Spiffe != nil { + in, out := &in.Spiffe, &out.Spiffe + *out = new(dynamic.Spiffe) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 47d2c3023..0c8d5aca0 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -322,6 +322,13 @@ func (i *Provider) serverTransport(cfg *dynamic.Configuration) { MaxIdleConnsPerHost: i.staticCfg.ServersTransport.MaxIdleConnsPerHost, } + if i.staticCfg.Spiffe != nil { + st.Spiffe = &dynamic.Spiffe{ + IDs: i.staticCfg.ServersTransport.Spiffe.IDs, + TrustDomain: i.staticCfg.ServersTransport.Spiffe.TrustDomain, + } + } + if i.staticCfg.ServersTransport.ForwardingTimeouts != nil { st.ForwardingTimeouts = &dynamic.ForwardingTimeouts{ DialTimeout: i.staticCfg.ServersTransport.ForwardingTimeouts.DialTimeout, diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 867da0a9b..383fac0c2 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -312,7 +312,7 @@ func TestRouterManager_Get(t *testing.T) { }, }) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) @@ -418,7 +418,7 @@ func TestAccessLog(t *testing.T) { }, }) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) @@ -713,7 +713,7 @@ func TestRuntimeConfiguration(t *testing.T) { }, }) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) @@ -788,7 +788,7 @@ func TestProviderOnMiddlewares(t *testing.T) { }, }) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index 0c6697e38..0afa69584 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -48,7 +48,7 @@ func TestReuseService(t *testing.T) { ), ) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() @@ -184,7 +184,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { }, } - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() @@ -225,7 +225,7 @@ func TestInternalServices(t *testing.T) { ), ) - roundTripperManager := service.NewRoundTripperManager() + roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() diff --git a/pkg/server/service/roundtripper.go b/pkg/server/service/roundtripper.go index 68b3b0bbf..b155a5632 100644 --- a/pkg/server/service/roundtripper.go +++ b/pkg/server/service/roundtripper.go @@ -11,6 +11,10 @@ import ( "sync" "time" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" traefiktls "github.com/traefik/traefik/v2/pkg/tls" @@ -26,11 +30,18 @@ func (t *h2cTransportWrapper) RoundTrip(req *http.Request) (*http.Response, erro return t.Transport.RoundTrip(req) } +// SpiffeX509Source allows to retrieve a x509 SVID and bundle. +type SpiffeX509Source interface { + x509svid.Source + x509bundle.Source +} + // NewRoundTripperManager creates a new RoundTripperManager. -func NewRoundTripperManager() *RoundTripperManager { +func NewRoundTripperManager(spiffeX509Source SpiffeX509Source) *RoundTripperManager { return &RoundTripperManager{ - roundTrippers: make(map[string]http.RoundTripper), - configs: make(map[string]*dynamic.ServersTransport), + roundTrippers: make(map[string]http.RoundTripper), + configs: make(map[string]*dynamic.ServersTransport), + spiffeX509Source: spiffeX509Source, } } @@ -39,6 +50,8 @@ type RoundTripperManager struct { rtLock sync.RWMutex roundTrippers map[string]http.RoundTripper configs map[string]*dynamic.ServersTransport + + spiffeX509Source SpiffeX509Source } // Update updates the roundtrippers configurations. @@ -59,7 +72,7 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans } var err error - r.roundTrippers[configName], err = createRoundTripper(newConfig) + r.roundTrippers[configName], err = r.createRoundTripper(newConfig) if err != nil { log.WithoutContext().Errorf("Could not configure HTTP Transport %s, fallback on default transport: %v", configName, err) r.roundTrippers[configName] = http.DefaultTransport @@ -72,7 +85,7 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans } var err error - r.roundTrippers[newConfigName], err = createRoundTripper(newConfig) + r.roundTrippers[newConfigName], err = r.createRoundTripper(newConfig) if err != nil { log.WithoutContext().Errorf("Could not configure HTTP Transport %s, fallback on default transport: %v", newConfigName, err) r.roundTrippers[newConfigName] = http.DefaultTransport @@ -102,7 +115,7 @@ func (r *RoundTripperManager) Get(name string) (http.RoundTripper, error) { // For the settings that can't be configured in Traefik it uses the default http.Transport settings. // An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHost in Traefik at this point in time. // Setting this value to the default of 100 could lead to confusing behavior and backwards compatibility issues. -func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) { +func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) { if cfg == nil { return nil, errors.New("no transport configuration given") } @@ -132,7 +145,24 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error transport.IdleConnTimeout = time.Duration(cfg.ForwardingTimeouts.IdleConnTimeout) } + if cfg.Spiffe != nil { + if r.spiffeX509Source == nil { + return nil, errors.New("SPIFFE is enabled for this transport, but not configured") + } + + spiffeAuthorizer, err := buildSpiffeAuthorizer(cfg.Spiffe) + if err != nil { + return nil, fmt.Errorf("unable to build SPIFFE authorizer: %w", err) + } + + transport.TLSClientConfig = tlsconfig.MTLSClientConfig(r.spiffeX509Source, r.spiffeX509Source, spiffeAuthorizer) + } + if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" { + if transport.TLSClientConfig != nil { + return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time") + } + transport.TLSClientConfig = &tls.Config{ ServerName: cfg.ServerName, InsecureSkipVerify: cfg.InsecureSkipVerify, @@ -173,3 +203,31 @@ func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool { return roots } + +func buildSpiffeAuthorizer(cfg *dynamic.Spiffe) (tlsconfig.Authorizer, error) { + switch { + case len(cfg.IDs) > 0: + spiffeIDs := make([]spiffeid.ID, 0, len(cfg.IDs)) + for _, rawID := range cfg.IDs { + id, err := spiffeid.FromString(rawID) + if err != nil { + return nil, fmt.Errorf("invalid SPIFFE ID: %w", err) + } + + spiffeIDs = append(spiffeIDs, id) + } + + return tlsconfig.AuthorizeOneOf(spiffeIDs...), nil + + case cfg.TrustDomain != "": + trustDomain, err := spiffeid.TrustDomainFromString(cfg.TrustDomain) + if err != nil { + return nil, fmt.Errorf("invalid SPIFFE trust domain: %w", err) + } + + return tlsconfig.AuthorizeMemberOf(trustDomain), nil + + default: + return tlsconfig.AuthorizeAny(), nil + } +} diff --git a/pkg/server/service/roundtripper_test.go b/pkg/server/service/roundtripper_test.go index a057166ab..37d543674 100644 --- a/pkg/server/service/roundtripper_test.go +++ b/pkg/server/service/roundtripper_test.go @@ -1,14 +1,24 @@ package service import ( + "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "math/big" "net" "net/http" "net/http/httptest" + "net/url" "sync/atomic" "testing" + "time" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" @@ -129,7 +139,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) { srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} srv.StartTLS() - rtManager := NewRoundTripperManager() + rtManager := NewRoundTripperManager(nil) dynamicConf := map[string]*dynamic.ServersTransport{ "test": { @@ -197,7 +207,7 @@ func TestMTLS(t *testing.T) { } srv.StartTLS() - rtManager := NewRoundTripperManager() + rtManager := NewRoundTripperManager(nil) dynamicConf := map[string]*dynamic.ServersTransport{ "test": { @@ -228,6 +238,141 @@ func TestMTLS(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) } +func TestSpiffeMTLS(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + trustDomain := spiffeid.RequireTrustDomainFromString("spiffe://traefik.test") + + pki, err := newFakeSpiffePKI(trustDomain) + require.NoError(t, err) + + clientSVID, err := pki.genSVID(spiffeid.RequireFromPath(trustDomain, "/client")) + require.NoError(t, err) + + serverSVID, err := pki.genSVID(spiffeid.RequireFromPath(trustDomain, "/server")) + require.NoError(t, err) + + serverSource := fakeSpiffeSource{ + svid: serverSVID, + bundle: pki.bundle, + } + + // go-spiffe's `tlsconfig.MTLSServerConfig` (that should be used here) does not set a certificate on + // the returned `tls.Config` and relies instead on `GetCertificate` being always called. + // But it turns out that `StartTLS` from `httptest.Server`, enforces a default certificate + // if no certificate is previously set on the configured TLS config. + // It makes the test server always serve the httptest default certificate, and not the SPIFFE certificate, + // as GetCertificate is in that case never called (there's a default cert, and SNI is not used). + // To bypass this issue, we're manually extracting the server ceritificate from the server SVID + // and use another initialization method that forces serving the server SPIFFE certificate. + serverCert, err := tlsconfig.GetCertificate(&serverSource)(nil) + require.NoError(t, err) + srv.TLS = tlsconfig.MTLSWebServerConfig( + serverCert, + &serverSource, + tlsconfig.AuthorizeAny(), + ) + srv.StartTLS() + defer srv.Close() + + clientSource := fakeSpiffeSource{ + svid: clientSVID, + bundle: pki.bundle, + } + + testCases := []struct { + desc string + config dynamic.Spiffe + clientSource SpiffeX509Source + wantStatusCode int + wantErrorMessage string + }{ + { + desc: "supports SPIFFE mTLS", + config: dynamic.Spiffe{}, + clientSource: &clientSource, + wantStatusCode: http.StatusOK, + }, + { + desc: "allows expected server SPIFFE ID", + config: dynamic.Spiffe{ + IDs: []string{"spiffe://traefik.test/server"}, + }, + clientSource: &clientSource, + wantStatusCode: http.StatusOK, + }, + { + desc: "blocks unexpected server SPIFFE ID", + config: dynamic.Spiffe{ + IDs: []string{"spiffe://traefik.test/not-server"}, + }, + clientSource: &clientSource, + wantErrorMessage: `unexpected ID "spiffe://traefik.test/server"`, + }, + { + desc: "allows expected server trust domain", + config: dynamic.Spiffe{ + TrustDomain: "spiffe://traefik.test", + }, + clientSource: &clientSource, + wantStatusCode: http.StatusOK, + }, + { + desc: "denies unexpected server trust domain", + config: dynamic.Spiffe{ + TrustDomain: "spiffe://not-traefik.test", + }, + clientSource: &clientSource, + wantErrorMessage: `unexpected trust domain "traefik.test"`, + }, + { + desc: "spiffe IDs allowlist takes precedence", + config: dynamic.Spiffe{ + IDs: []string{"spiffe://traefik.test/not-server"}, + TrustDomain: "spiffe://not-traefik.test", + }, + clientSource: &clientSource, + wantErrorMessage: `unexpected ID "spiffe://traefik.test/server"`, + }, + { + desc: "raises an error when spiffe is enabled on the transport but no workloadapi address is given", + config: dynamic.Spiffe{}, + clientSource: nil, + wantErrorMessage: `remote error: tls: bad certificate`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + rtManager := NewRoundTripperManager(test.clientSource) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + Spiffe: &test.config, + }, + } + + rtManager.Update(dynamicConf) + + tr, err := rtManager.Get("test") + require.NoError(t, err) + + client := http.Client{Transport: tr} + + resp, err := client.Get(srv.URL) + if test.wantErrorMessage != "" { + assert.ErrorContains(t, err, test.wantErrorMessage) + return + } + + require.NoError(t, err) + assert.Equal(t, test.wantStatusCode, resp.StatusCode) + }) + } +} + func TestDisableHTTP2(t *testing.T) { testCases := []struct { desc string @@ -269,7 +414,7 @@ func TestDisableHTTP2(t *testing.T) { srv.EnableHTTP2 = test.serverHTTP2 srv.StartTLS() - rtManager := NewRoundTripperManager() + rtManager := NewRoundTripperManager(nil) dynamicConf := map[string]*dynamic.ServersTransport{ "test": { @@ -293,3 +438,116 @@ func TestDisableHTTP2(t *testing.T) { }) } } + +// fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs. +type fakeSpiffePKI struct { + caPrivateKey *rsa.PrivateKey + + bundle *x509bundle.Bundle +} + +func newFakeSpiffePKI(trustDomain spiffeid.TrustDomain) (fakeSpiffePKI, error) { + caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fakeSpiffePKI{}, err + } + + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2000), + Subject: pkix.Name{ + Organization: []string{"spiffe"}, + }, + URIs: []*url.URL{spiffeid.RequireFromPath(trustDomain, "/ca").URL()}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + SubjectKeyId: []byte("ca"), + KeyUsage: x509.KeyUsageCertSign | + x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + PublicKey: caPrivateKey.Public(), + } + if err != nil { + return fakeSpiffePKI{}, err + } + + caCertDER, err := x509.CreateCertificate( + rand.Reader, + &caTemplate, + &caTemplate, + caPrivateKey.Public(), + caPrivateKey, + ) + if err != nil { + return fakeSpiffePKI{}, err + } + + bundle, err := x509bundle.ParseRaw( + trustDomain, + caCertDER, + ) + if err != nil { + return fakeSpiffePKI{}, err + } + + return fakeSpiffePKI{ + bundle: bundle, + caPrivateKey: caPrivateKey, + }, nil +} + +func (f *fakeSpiffePKI) genSVID(id spiffeid.ID) (*x509svid.SVID, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(200001), + URIs: []*url.URL{id.URL()}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + SubjectKeyId: []byte("svid"), + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageKeyAgreement | + x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + BasicConstraintsValid: true, + PublicKey: privateKey.PublicKey, + } + + certDER, err := x509.CreateCertificate( + rand.Reader, + &template, + f.bundle.X509Authorities()[0], + privateKey.Public(), + f.caPrivateKey, + ) + if err != nil { + return nil, err + } + + keyPKCS8, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, err + } + + return x509svid.ParseRaw(certDER, keyPKCS8) +} + +// fakeSpiffeSource allows retrieving staticly an SVID and its associated bundle. +type fakeSpiffeSource struct { + bundle *x509bundle.Bundle + svid *x509svid.SVID +} + +func (s *fakeSpiffeSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + return s.bundle, nil +} + +func (s *fakeSpiffeSource) GetX509SVID() (*x509svid.SVID, error) { + return s.svid, nil +}