diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index e0e2139e5..b1c8cb4f5 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -378,6 +378,9 @@ serverName = "foobar" insecureSkipVerify = true rootCAs = ["foobar", "foobar"] + cipherSuites = ["foobar", "foobar"] + minVersion = "foobar" + maxVersion = "foobar" maxIdleConnsPerHost = 42 disableHTTP2 = true peerCertURI = "foobar" @@ -402,6 +405,9 @@ serverName = "foobar" insecureSkipVerify = true rootCAs = ["foobar", "foobar"] + cipherSuites = ["foobar", "foobar"] + minVersion = "foobar" + maxVersion = "foobar" maxIdleConnsPerHost = 42 disableHTTP2 = true peerCertURI = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index e2ab16e54..392a927a4 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -437,6 +437,11 @@ http: keyFile: foobar - certFile: foobar keyFile: foobar + cipherSuites: + - foobar + - foobar + minVersion: foobar + maxVersion: foobar maxIdleConnsPerHost: 42 forwardingTimeouts: dialTimeout: 42s @@ -462,6 +467,11 @@ http: keyFile: foobar - certFile: foobar keyFile: foobar + cipherSuites: + - foobar + - foobar + minVersion: foobar + maxVersion: foobar maxIdleConnsPerHost: 42 forwardingTimeouts: dialTimeout: 42s 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 ef7c35ea0..a2af326f9 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -2281,6 +2281,12 @@ spec: items: type: string type: array + cipherSuites: + description: CipherSuites defines the cipher suites to use when contacting + backend servers. + items: + type: string + type: array disableHTTP2: description: DisableHTTP2 disables HTTP/2 for connections with backend servers. @@ -2341,6 +2347,14 @@ spec: to keep per-host. minimum: -1 type: integer + maxVersion: + description: MaxVersion defines the maximum TLS version to use when + contacting backend servers. + type: string + minVersion: + description: MinVersion defines the minimum TLS version to use when + contacting backend servers. + type: string peerCertURI: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index a9d2984b4..7a2b9229e 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -237,6 +237,8 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/serversTransports/ServersTransport0/certificates/0/keyFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/certificates/1/certFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/certificates/1/keyFile` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/cipherSuites/0` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/cipherSuites/1` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/disableHTTP2` | `true` | | `traefik/http/serversTransports/ServersTransport0/forwardingTimeouts/dialTimeout` | `42s` | | `traefik/http/serversTransports/ServersTransport0/forwardingTimeouts/idleConnTimeout` | `42s` | @@ -245,6 +247,8 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/serversTransports/ServersTransport0/forwardingTimeouts/responseHeaderTimeout` | `42s` | | `traefik/http/serversTransports/ServersTransport0/insecureSkipVerify` | `true` | | `traefik/http/serversTransports/ServersTransport0/maxIdleConnsPerHost` | `42` | +| `traefik/http/serversTransports/ServersTransport0/maxVersion` | `foobar` | +| `traefik/http/serversTransports/ServersTransport0/minVersion` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/peerCertURI` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/rootCAs/0` | `foobar` | | `traefik/http/serversTransports/ServersTransport0/rootCAs/1` | `foobar` | @@ -256,6 +260,8 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/serversTransports/ServersTransport1/certificates/0/keyFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/certificates/1/certFile` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/certificates/1/keyFile` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/cipherSuites/0` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/cipherSuites/1` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/disableHTTP2` | `true` | | `traefik/http/serversTransports/ServersTransport1/forwardingTimeouts/dialTimeout` | `42s` | | `traefik/http/serversTransports/ServersTransport1/forwardingTimeouts/idleConnTimeout` | `42s` | @@ -264,6 +270,8 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/serversTransports/ServersTransport1/forwardingTimeouts/responseHeaderTimeout` | `42s` | | `traefik/http/serversTransports/ServersTransport1/insecureSkipVerify` | `true` | | `traefik/http/serversTransports/ServersTransport1/maxIdleConnsPerHost` | `42` | +| `traefik/http/serversTransports/ServersTransport1/maxVersion` | `foobar` | +| `traefik/http/serversTransports/ServersTransport1/minVersion` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/peerCertURI` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/rootCAs/0` | `foobar` | | `traefik/http/serversTransports/ServersTransport1/rootCAs/1` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml index 3e22e0107..b8fa0ee01 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml @@ -49,6 +49,12 @@ spec: items: type: string type: array + cipherSuites: + description: CipherSuites defines the cipher suites to use when contacting + backend servers. + items: + type: string + type: array disableHTTP2: description: DisableHTTP2 disables HTTP/2 for connections with backend servers. @@ -109,6 +115,14 @@ spec: to keep per-host. minimum: -1 type: integer + maxVersion: + description: MaxVersion defines the maximum TLS version to use when + contacting backend servers. + type: string + minVersion: + description: MinVersion defines the minimum TLS version to use when + contacting backend servers. + type: string peerCertURI: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. diff --git a/docs/content/reference/routing-configuration/http/load-balancing/serverstransport.md b/docs/content/reference/routing-configuration/http/load-balancing/serverstransport.md index 5774e60df..0c1d22fd9 100644 --- a/docs/content/reference/routing-configuration/http/load-balancing/serverstransport.md +++ b/docs/content/reference/routing-configuration/http/load-balancing/serverstransport.md @@ -35,6 +35,11 @@ http: - "spiffe://example.org/id1" - "spiffe://example.org/id2" trustDomain: "example.org" + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + minVersion: VersionTLS12 + maxVersion: VersionTLS12 ``` ```toml tab="Structured (TOML)" @@ -46,6 +51,9 @@ http: maxIdleConnsPerHost = 100 disableHTTP2 = true peerCertURI = "spiffe://example.org/peer" + cipherSuites = ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"] + minVersion = "VersionTLS12" + maxVersion = "VersionTLS12" [http.serversTransports.mytransport.forwardingTimeouts] dialTimeout = "30s" @@ -100,6 +108,9 @@ labels: | `certificates` | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | [] | No | | `insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | false | No | | `rootcas` | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | [] | No | +| `cipherSuites` | Defines the cipher suites to use when contacting backend servers. | [] | No | +| `minVersion` | Defines the minimum TLS version to use when contacting backend servers. | "" | No | +| `maxVersion` | Defines the maximum TLS version to use when contacting backend servers. | "" | No | | `maxIdleConnsPerHost` | Maximum idle (keep-alive) connections to keep per-host. | 200 | No | | `disableHTTP2` | Disables HTTP/2 for connections with servers. | false | No | | `peerCertURI` | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No | diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/serverstransport.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/serverstransport.md index 63f5c04a2..bf86e1563 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/serverstransport.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/serverstransport.md @@ -67,6 +67,21 @@ spec: | `serverstransport.`
`forwardingTimeouts.idleConnTimeout`
| Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.
Zero means no timeout. | 90s | No | | `serverstransport.`
`spiffe.ids`
| Allow SPIFFE IDs.
This takes precedence over the SPIFFE TrustDomain. | | No | | `serverstransport.`
`spiffe.trustDomain`
| Allow SPIFFE trust domain. | "" | No | +| `serverstransport.`
`serverName`
| Defines the server name that will be used for SNI. | | No | +| `serverstransport.`
`insecureSkipVerify`
| Controls whether the server's certificate chain and host name is verified. | false | No | +| `serverstransport.`
`rootcas`
| Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No | +| `serverstransport.`
`certificatesSecrets`
| Certificates to present to the server for mTLS. | | No | +| `serverstransport.`
`cipherSuites`
| Defines the cipher suites to use when contacting backend servers. | [] | No | +| `serverstransport.`
`minVersion`
| Defines the minimum TLS version to use when contacting backend servers. | "" | No | +| `serverstransport.`
`maxVersion`
| Defines the maximum TLS version to use when contacting backend servers. | "" | No | +| `serverstransport.`
`maxIdleConnsPerHost`
| Maximum idle (keep-alive) connections to keep per-host. | 200 | No | +| `serverstransport.`
`disableHTTP2`
| Disables HTTP/2 for connections with servers. | false | No | +| `serverstransport.`
`peerCertURI`
| Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No | +| `serverstransport.`
`forwardingTimeouts.dialTimeout`
| Amount of time to wait until a connection to a server can be established.
Zero means no timeout. | 30s | No | +| `serverstransport.`
`forwardingTimeouts.responseHeaderTimeout`
| Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).
Zero means no timeout | 0s | No | +| `serverstransport.`
`forwardingTimeouts.idleConnTimeout`
| Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.
Zero means no timeout. | 90s | No | +| `serverstransport.`
`spiffe.ids`
| Allow SPIFFE IDs.
This takes precedence over the SPIFFE TrustDomain. | | No | +| `serverstransport.`
`spiffe.trustDomain`
| Allow SPIFFE trust domain. | "" | No | !!! note "CA Secret" The CA secret must contain a base64 encoded certificate under either a tls.ca or a ca.crt key. diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 9e00b358f..ec728baaa 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -380,6 +380,9 @@ serverName = "foobar" insecureSkipVerify = true rootCAs = ["foobar", "foobar"] + cipherSuites = ["foobar", "foobar"] + minVersion = "foobar" + maxVersion = "foobar" maxIdleConnsPerHost = 42 disableHTTP2 = true peerCertURI = "foobar" @@ -404,6 +407,9 @@ serverName = "foobar" insecureSkipVerify = true rootCAs = ["foobar", "foobar"] + cipherSuites = ["foobar", "foobar"] + minVersion = "foobar" + maxVersion = "foobar" maxIdleConnsPerHost = 42 disableHTTP2 = true peerCertURI = "foobar" diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index fdb8f2c1e..0b6033287 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -443,6 +443,11 @@ http: keyFile: foobar - certFile: foobar keyFile: foobar + cipherSuites: + - foobar + - foobar + minVersion: foobar + maxVersion: foobar maxIdleConnsPerHost: 42 forwardingTimeouts: dialTimeout: 42s @@ -468,6 +473,11 @@ http: keyFile: foobar - certFile: foobar keyFile: foobar + cipherSuites: + - foobar + - foobar + minVersion: foobar + maxVersion: foobar maxIdleConnsPerHost: 42 forwardingTimeouts: dialTimeout: 42s diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index e9fa28b55..505c1ac13 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1869,6 +1869,11 @@ Register the `TLSStore` kind in the Kubernetes cluster before creating `TLSStore - spiffe://trust-domain/id1 - spiffe://trust-domain/id2 trustDomain: "spiffe://trust-domain" # [14] + cipherSuites: # [15] + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + minVersion: VersionTLS11 # [16] + maxVersion: VersionTLS12 # [17] ``` | Ref | Attribute | Purpose | @@ -1887,6 +1892,9 @@ Register the `TLSStore` kind in the Kubernetes cluster before creating `TLSStore | [12] | `spiffe` | The spiffe configuration. | | [13] | `ids` | Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). | | [14] | `trustDomain` | Defines the allowed SPIFFE trust domain. | +| [15] | `cipherSuites` | Defines the cipher suites to use when contacting backend servers. | +| [16] | `minVersion` | Defines the minimum TLS version to use when contacting backend servers. | +| [17] | `maxVersion` | Defines the maximum TLS version to use when contacting backend servers. | !!! info "CA Secret" diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 0c736b1b1..67db47694 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -800,6 +800,129 @@ data: ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= ``` +#### `cipherSuites` + +_Optional_ + +`cipherSuites` defines the cipher suites to use when contacting backend servers. + +This option allows you to control the cryptographic algorithms used for backend connections, which is useful for: + +- Connecting to legacy backends that only support specific cipher suites +- Enforcing security policies (e.g., requiring Perfect Forward Secrecy) +- Meeting compliance requirements + +If not specified, Go's default cipher suites are used. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport] + cipherSuites = ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"] +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default +spec: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +``` + +#### `minVersion` + +_Optional_ + +`minVersion` defines the minimum TLS version to use when contacting backend servers. + +Use this option to enforce a minimum security level for backend connections. + +!!! info "Valid Values" + - `VersionTLS10` (discouraged - deprecated and insecure) + - `VersionTLS11` (discouraged - deprecated and insecure) + - `VersionTLS12` (recommended minimum) + - `VersionTLS13` (most secure) + +If not specified, Go's default minimum version is used. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + minVersion: VersionTLS12 +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport] + minVersion = "VersionTLS12" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default +spec: + minVersion: VersionTLS12 +``` + +#### `maxVersion` + +_Optional_ + +`maxVersion` defines the maximum TLS version to use when contacting backend servers. + +!!! warning "Use with Caution" + We discourage using this option to disable TLS 1.3. It should only be used for connecting to legacy backends that don't support newer TLS versions. + +!!! info "Valid Values" + - `VersionTLS10` + - `VersionTLS11` + - `VersionTLS12` + - `VersionTLS13` + +If not specified, Go's default maximum version (latest) is used. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + maxVersion: VersionTLS12 +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport] + maxVersion = "VersionTLS12" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default +spec: + maxVersion: VersionTLS12 +``` + #### `maxIdleConnsPerHost` _Optional, Default=2_ diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index ef7c35ea0..a2af326f9 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -2281,6 +2281,12 @@ spec: items: type: string type: array + cipherSuites: + description: CipherSuites defines the cipher suites to use when contacting + backend servers. + items: + type: string + type: array disableHTTP2: description: DisableHTTP2 disables HTTP/2 for connections with backend servers. @@ -2341,6 +2347,14 @@ spec: to keep per-host. minimum: -1 type: integer + maxVersion: + description: MaxVersion defines the maximum TLS version to use when + contacting backend servers. + type: string + minVersion: + description: MinVersion defines the minimum TLS version to use when + contacting backend servers. + type: string peerCertURI: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index afb503e44..6107a0e65 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -449,6 +449,9 @@ type ServersTransport struct { InsecureSkipVerify bool `description:"Disables SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` RootCAs []types.FileOrContent `description:"Defines a list of CA certificates used to validate server certificates." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` Certificates traefiktls.Certificates `description:"Defines a list of client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` + CipherSuites []string `description:"Defines the cipher suites to use when contacting backend servers." json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty" export:"true"` + MinVersion string `description:"Defines the minimum TLS version to use when contacting backend servers." json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"` + MaxVersion string `description:"Defines the maximum TLS version to use when contacting backend servers." json:"maxVersion,omitempty" toml:"maxVersion,omitempty" yaml:"maxVersion,omitempty" export:"true"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used. If negative, disables connection reuse." json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Defines the timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` DisableHTTP2 bool `description:"Disables HTTP/2 for connections with backend servers." json:"disableHTTP2,omitempty" toml:"disableHTTP2,omitempty" yaml:"disableHTTP2,omitempty" export:"true"` diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 94f95ff1e..e4bd3ae1e 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1611,6 +1611,11 @@ func (in *ServersTransport) DeepCopyInto(out *ServersTransport) { *out = make(tls.Certificates, len(*in)) copy(*out, *in) } + if in.CipherSuites != nil { + in, out := &in.CipherSuites, &out.CipherSuites + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.ForwardingTimeouts != nil { in, out := &in.ForwardingTimeouts, &out.ForwardingTimeouts *out = new(ForwardingTimeouts) diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml index 496c5af30..5b1fee8f0 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml @@ -169,6 +169,11 @@ spec: - spiffe://foo/buz - spiffe://bar/biz trustDomain: spiffe://lol + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + minVersion: VersionTLS11 + maxVersion: VersionTLS12 --- apiVersion: traefik.io/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 92b45a124..181724942 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -409,6 +409,49 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) }) } + var cipherSuites []string + if serversTransport.Spec.CipherSuites != nil { + for _, cipher := range serversTransport.Spec.CipherSuites { + if _, exists := tls.CipherSuites[cipher]; exists { + cipherSuites = append(cipherSuites, cipher) + } else { + logger.Error().Msgf("cipher suite not supported: %s, falling back to default CipherSuite.", cipher) + cipherSuites = nil + break + } + } + } + + var minVersion string + var minVersionID uint16 + if serversTransport.Spec.MinVersion != "" { + if id, exists := tls.MinVersion[serversTransport.Spec.MinVersion]; exists { + minVersion = serversTransport.Spec.MinVersion + minVersionID = id + } else { + logger.Error().Msgf("invalid TLS minimum version: %s", serversTransport.Spec.MinVersion) + } + } + + var maxVersion string + var maxVersionID uint16 + if serversTransport.Spec.MaxVersion != "" { + if id, exists := tls.MaxVersion[serversTransport.Spec.MaxVersion]; exists { + maxVersion = serversTransport.Spec.MaxVersion + maxVersionID = id + } else { + logger.Error().Msgf("invalid TLS maximum version: %s", serversTransport.Spec.MaxVersion) + } + } + + if serversTransport.Spec.MinVersion != "" && serversTransport.Spec.MaxVersion != "" { + if minVersionID >= maxVersionID { + log.Error().Msgf("CipherSuite MinVersion, %s, above or equal to the MaxVersion, %s. Falling back to default MaxVersion and MinVersion", serversTransport.Spec.MinVersion, serversTransport.Spec.MaxVersion) + minVersion = "VersionTLS12" + maxVersion = "" + } + } + forwardingTimeout := &dynamic.ForwardingTimeouts{} forwardingTimeout.SetDefaults() @@ -455,6 +498,9 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) InsecureSkipVerify: serversTransport.Spec.InsecureSkipVerify, RootCAs: rootCAs, Certificates: certs, + CipherSuites: cipherSuites, + MinVersion: minVersion, + MaxVersion: maxVersion, DisableHTTP2: serversTransport.Spec.DisableHTTP2, MaxIdleConnsPerHost: serversTransport.Spec.MaxIdleConnsPerHost, ForwardingTimeouts: forwardingTimeout, diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index eac9b5fc1..758844bbb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -4818,6 +4818,9 @@ func TestLoadIngressRoutes(t *testing.T) { {CertFile: "TESTCERT2", KeyFile: "TESTKEY2"}, {CertFile: "TESTCERT3", KeyFile: "TESTKEY3"}, }, + CipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}, + MinVersion: "VersionTLS11", + MaxVersion: "VersionTLS12", MaxIdleConnsPerHost: 42, DisableHTTP2: true, ForwardingTimeouts: &dynamic.ForwardingTimeouts{ diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go index 8cfbb92be..541710ef9 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go @@ -38,6 +38,12 @@ type ServersTransportSpec struct { RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"` // CertificatesSecrets defines a list of secret storing client certificates for mTLS. CertificatesSecrets []string `json:"certificatesSecrets,omitempty"` + // CipherSuites defines the cipher suites to use when contacting backend servers. + CipherSuites []string `json:"cipherSuites,omitempty"` + // MinVersion defines the minimum TLS version to use when contacting backend servers. + MinVersion string `json:"minVersion,omitempty"` + // MaxVersion defines the maximum TLS version to use when contacting backend servers. + MaxVersion string `json:"maxVersion,omitempty"` // MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. // +kubebuilder:validation:Minimum=-1 MaxIdleConnsPerHost int `json:"maxIdleConnsPerHost,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index 3137d46ec..6e50ab3dd 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -1470,6 +1470,11 @@ func (in *ServersTransportSpec) DeepCopyInto(out *ServersTransportSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.CipherSuites != nil { + in, out := &in.CipherSuites, &out.CipherSuites + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.ForwardingTimeouts != nil { in, out := &in.ForwardingTimeouts, &out.ForwardingTimeouts *out = new(ForwardingTimeouts) diff --git a/pkg/server/service/transport.go b/pkg/server/service/transport.go index ecce54010..87e50688d 100644 --- a/pkg/server/service/transport.go +++ b/pkg/server/service/transport.go @@ -169,16 +169,58 @@ func (t *TransportManager) createTLSConfig(cfg *dynamic.ServersTransport) (*tls. config = tlsconfig.MTLSClientConfig(t.spiffeX509Source, t.spiffeX509Source, spiffeAuthorizer) } - if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" { + if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" || len(cfg.CipherSuites) > 0 || cfg.MaxVersion != "" || cfg.MinVersion != "" { if config != nil { return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time") } + cipherSuites := make([]uint16, 0) + if cfg.CipherSuites != nil { + for _, cipher := range cfg.CipherSuites { + if cipherID, exists := traefiktls.CipherSuites[cipher]; exists { + cipherSuites = append(cipherSuites, cipherID) + } else { + log.Error().Msgf("Invalid cipher: %v, falling back to default CipherSuite.", cipher) + cipherSuites = nil + break + } + } + } + + var minVersion uint16 + if cfg.MinVersion != "" { + if value, exists := traefiktls.MinVersion[cfg.MinVersion]; exists { + minVersion = value + } else { + log.Error().Msgf("Invalid TLS minimum version: %s", cfg.MinVersion) + } + } + + var maxVersion uint16 + if cfg.MaxVersion != "" { + if value, exists := traefiktls.MaxVersion[cfg.MaxVersion]; exists { + maxVersion = value + } else { + log.Error().Msgf("Invalid TLS maximum version: %s", cfg.MaxVersion) + } + } + + if cfg.MinVersion != "" && cfg.MaxVersion != "" { + if minVersion >= maxVersion { + log.Error().Msgf("CipherSuite MinVersion, %s, above or equal to the MaxVersion, %s. Falling back to default MaxVersion and MinVersion", cfg.MinVersion, cfg.MaxVersion) + minVersion = tls.VersionTLS12 + maxVersion = 0 + } + } + config = &tls.Config{ ServerName: cfg.ServerName, InsecureSkipVerify: cfg.InsecureSkipVerify, RootCAs: createRootCACertPool(cfg.RootCAs), Certificates: cfg.Certificates.GetCertificates(), + CipherSuites: cipherSuites, + MinVersion: minVersion, + MaxVersion: maxVersion, } if cfg.PeerCertURI != "" { diff --git a/pkg/server/service/transport_test.go b/pkg/server/service/transport_test.go index 0fd3a8ab8..2510cd74f 100644 --- a/pkg/server/service/transport_test.go +++ b/pkg/server/service/transport_test.go @@ -1,6 +1,7 @@ package service import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -11,10 +12,12 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "sync/atomic" "testing" "time" + "github.com/rs/zerolog/log" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" @@ -183,6 +186,346 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) { assert.EqualValues(t, 2, count) } +func TestValidCipherSuites(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + }, + } + + transportManager.Update(dynamicConf) + require.NoError(t, err) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestValidTLSVersions(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MaxVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS11, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + MaxVersion: "VersionTLS12", + MinVersion: "VersionTLS11", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + }, + } + + transportManager.Update(dynamicConf) + require.NoError(t, err) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestInvalidMaxTLSVersions(t *testing.T) { + // Init log buffer to capture zerolog output + var logBuffer bytes.Buffer + // Capture zerolog output + log.Logger = log.Output(&logBuffer) + // Restore original logger after test + defer func() { + log.Logger = log.Output(os.Stderr) + }() + + // Define a function to run the test logic and gather logs + logtest := func() { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MaxVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + MaxVersion: "VersionTLS16", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + }, + } + + transportManager.Update(dynamicConf) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + } + + // Run the test + logtest() + // Set logs in variable as string + logged := logBuffer.String() + // Check logs content expected error message + assert.Contains(t, logged, "Invalid TLS maximum version: VersionTLS16") +} + +func TestInvalidMinTLSVersions(t *testing.T) { + // Init log buffer to capture zerolog output + var logBuffer bytes.Buffer + // Capture zerolog output + log.Logger = log.Output(&logBuffer) + // Restore original logger after test + defer func() { + log.Logger = log.Output(os.Stderr) + }() + + // Define a function to run the test logic and gather logs + logtest := func() { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS11, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + MinVersion: "VersionTLS09", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + }, + } + + transportManager.Update(dynamicConf) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + } + + // Run the test + logtest() + // Set logs in variable as string + logged := logBuffer.String() + // Check logs content expected error message + assert.Contains(t, logged, "Invalid TLS minimum version: VersionTLS09") +} + +func TestInvalidCipherSuites(t *testing.T) { + // Init log buffer to capture zerolog output + var logBuffer bytes.Buffer + // Capture zerolog output + log.Logger = log.Output(&logBuffer) + // Restore original logger after test + defer func() { + log.Logger = log.Output(os.Stderr) + }() + + // Define a function to run the test logic and gather logs + logtest := func() { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MaxVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + MaxVersion: "VersionTLS12", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA385", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + }, + } + + transportManager.Update(dynamicConf) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + } + + // Run the test + logtest() + // Set logs in variable as string + logged := logBuffer.String() + // Check logs content expected error message + assert.Contains(t, logged, "Invalid cipher: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA385, falling back to default CipherSuite.") +} + +func TestMinMaxCipherSuites(t *testing.T) { + // Init log buffer to capture zerolog output + var logBuffer bytes.Buffer + // Capture zerolog output + log.Logger = log.Output(&logBuffer) + // Restore original logger after test + defer func() { + log.Logger = log.Output(os.Stderr) + }() + + // Define a function to run the test logic and gather logs + logtest := func() { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + MinVersion: "VersionTLS12", + MaxVersion: "VersionTLS10", + CipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"}, + }, + } + + transportManager.Update(dynamicConf) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + } + + // Run the test + logtest() + // Set logs in variable as string + logged := logBuffer.String() + // Check logs content expected error message + assert.Contains(t, logged, "CipherSuite MinVersion, VersionTLS12, above or equal to the MaxVersion, VersionTLS10. Falling back to default MaxVersion and MinVersion") +} + +func TestEmptyCipherSuites(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + require.NoError(t, err) + + srv.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MaxVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS11, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + } + srv.StartTLS() + + transportManager := NewTransportManager(nil) + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + ServerName: "example.com", + RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)}, + }, + } + + transportManager.Update(dynamicConf) + tr, err := transportManager.GetRoundTripper("test") + require.NoError(t, err) + client := http.Client{Transport: tr} + _, err = client.Get(srv.URL) + require.Error(t, err) + + assert.ErrorContains(t, err, "remote error: tls: handshake failure") +} + func TestMTLS(t *testing.T) { srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK)