Support SPIFFE mTLS between Traefik and Backend servers

This commit is contained in:
Julien Levesy 2022-10-14 17:16:08 +02:00 committed by GitHub
parent 33f0aed5ea
commit b39ce8cc58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 736 additions and 24 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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':

6
go.mod
View file

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

18
go.sum
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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