Compare commits

..

28 Commits

Author SHA1 Message Date
Kevin Pollet 1268620453 Strip managedFields in informers
Co-authored-by: Piotr Maksymiuk <piotr.maksymiuk@docplanner.com>
2026-06-25 15:26:05 +02:00
Ali Amer b5e7a48bcd Support TLS and Backend TLS Policy SAN validation 2026-06-24 16:48:12 +02:00
Jenthe Noordsij ec80d1145c Increase timeout for Docker image sync job to 30 minutes 2026-06-12 17:12:06 +02:00
qwerty8811 2391520b50 Add optional X-Forwarded-Scheme and X-Scheme headers in forwarded headers middleware 2026-06-12 11:16:07 +02:00
qwerty8811 6cc3dd8d40 Add reportNodeInternalIPs option to report node internal IPs in Ingress status 2026-06-12 10:26:07 +02:00
Anatole Lucet bcf768ee09 Update Gateway API statuses once routing config is built
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2026-06-11 10:10:07 +02:00
kevinpollet 51b9a37615 Merge branch v3.7 into master 2026-06-10 17:05:29 +02:00
Kevin Pollet 26c96a3935 Prepare release v3.7.5 2026-06-10 16:46:07 +02:00
kevinpollet cb9e8ab510 Merge branch v3.6 into v3.7 2026-06-10 16:16:05 +02:00
Kevin Pollet b46e795f41 Prepare release v3.6.21 2026-06-10 16:02:11 +02:00
kevinpollet e53a37b869 Merge branch v2.11 into v3.6 2026-06-10 15:34:13 +02:00
Kevin Pollet ad1c1fc2f2 Prepare release v2.11.50 2026-06-10 15:28:05 +02:00
Romain 0209f984eb Fix snicheck for routers with no hosts
Co-authored-by: Gina A. <70909035+gndz07@users.noreply.github.com>
2026-06-10 15:16:06 +02:00
Kevin Pollet e043982244 Support BackendTLSPolicy for TLSRoute 2026-06-10 12:10:05 +02:00
Tim Schumacher 149e62d6db Bump to github.com/pires/go-proxyproto v0.12.0 2026-06-09 17:24:05 +02:00
Julien Salleyron 4ef4c09300 Fix routers with same host, different tlsoptions on different entryPoint
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2026-06-09 17:08:07 +02:00
Learloj d5ad3eb63b Pass endpointslice fencing on ingress-nginx provider 2026-06-09 16:28:05 +02:00
Baptiste Mayelle 8447bfc71e Reject cross-provider references with backendRefs.namespace 2026-06-09 16:24:05 +02:00
KirylJazzSax dc4b6fe2c6 Support Backend TLS policy for gRPC backends 2026-06-09 16:22:05 +02:00
Gina A. 15ecff2bbd Skip ingress when auth-secret resolution fails 2026-06-08 14:08:05 +02:00
kevinpollet 8773d7ead4 Merge branch v3.7 into master 2026-06-05 16:01:41 +02:00
romain 29406d4289 Merge current branch v3.7 into master 2026-05-27 14:51:50 +02:00
faukah eec68dce06 flake.nix: cleanup, refactor 2026-05-20 15:44:06 +02:00
Sheddy edd7d2eb33 Service-level Middleware Documentation 2026-05-04 13:56:05 +02:00
mmatur f7c0fdea57 Merge branch v3.7 into master 2026-04-30 16:47:39 +02:00
mmatur 9893e89628 Merge branch v3.7 into master 2026-04-22 14:40:14 +02:00
romain 786f7192e1 Merge branch v3.7 into master 2026-04-09 11:46:50 +02:00
kevinpollet 174e5d8111 Merge branch v3.7 into master 2026-03-26 14:05:54 +01:00
89 changed files with 4198 additions and 595 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
permissions:
packages: write
contents: read
+27
View File
@@ -1,3 +1,30 @@
## [v3.7.5](https://github.com/traefik/traefik/tree/v3.7.5) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.4...v3.7.5)
**Bug fixes:**
- **[k8s/ingress-nginx]** Skip ingress when auth-secret resolution fails ([#13323](https://github.com/traefik/traefik/pull/13323) @gndz07)
- **[k8s/ingress-nginx]** Pass endpointslice fencing on ingress-nginx provider ([#13290](https://github.com/traefik/traefik/pull/13290) @Learloj)
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v3.6.21](https://github.com/traefik/traefik/tree/v3.6.21) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.20...v3.6.21)
**Bug fixes:**
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v2.11.50](https://github.com/traefik/traefik/tree/v2.11.50) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.49...v2.11.50)
**Bug fixes:**
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
+6
View File
@@ -93,6 +93,12 @@ For a complete list of supported annotations and behavioral differences, see the
The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**.
!!! info "Legacy Scheme Headers"
If your applications still depend on ingress-nginx's legacy `X-Forwarded-Scheme` or `X-Scheme` headers,
enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the entrypoints that receive this traffic.
This keeps `X-Forwarded-Proto` unchanged and restores the compatibility headers at the entrypoint level for every provider.
---
## Prerequisites
+19
View File
@@ -9,6 +9,25 @@ This guide provides detailed migration steps for upgrading between different Tra
---
## v3.8.0
### `peerCertURI` option deprecation
Starting with `v3.8.0`, the `peerCertURI` option is deprecated in the `ServersTransport` and `ServersTransportTCP` configurations and will be removed in the next major version.
The new `peerCertSANs` option replaces it and supports multiple Subject Alternative Names (SANs) of type `URI` or `DNSName`.
Please check out the [ServersTransport](../reference/routing-configuration/http/load-balancing/serverstransport.md#opt-peerCertSANs) and [ServersTransportTCP](../reference/routing-configuration/tcp/serverstransport.md#opt-serverstransport-tls-peerCertSANs) documentation for more details.
#### Kubernetes CRD Provider
To use the new `peerCertSANs` field on `ServersTransport` and `ServersTransportTCP` resources with the Kubernetes CRD provider, you need to update your CRDs.
**Apply Updated CRDs:**
```shell
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.8/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
```
## v3.7.3
### Kubernetes Gateway API Provider
@@ -2493,9 +2493,25 @@ spec:
description: MinVersion defines the minimum TLS version to use when
contacting backend servers.
type: string
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: PeerCertURI defines the peer cert URI used to match against
SAN URI during the peer certificate verification.
description: |-
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
@@ -2647,10 +2663,26 @@ spec:
insecureSkipVerify:
description: InsecureSkipVerify disables TLS certificate verification.
type: boolean
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate
verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: |-
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets
@@ -123,9 +123,25 @@ spec:
description: MinVersion defines the minimum TLS version to use when
contacting backend servers.
type: string
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: PeerCertURI defines the peer cert URI used to match against
SAN URI during the peer certificate verification.
description: |-
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
@@ -93,10 +93,26 @@ spec:
insecureSkipVerify:
description: InsecureSkipVerify disables TLS certificate verification.
type: boolean
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate
verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: |-
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets
@@ -85,6 +85,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-entrypoints-name-address" href="#opt-entrypoints-name-address" title="#opt-entrypoints-name-address">entrypoints._name_.address</a> | Entry point address. | |
| <a id="opt-entrypoints-name-allowacmebypass" href="#opt-entrypoints-name-allowacmebypass" title="#opt-entrypoints-name-allowacmebypass">entrypoints._name_.allowacmebypass</a> | Enables handling of ACME TLS and HTTP challenges with custom routers. | false |
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
| <a id="opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" href="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" title="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders">entrypoints._name_.forwardedheaders.addxforwardedschemeheaders</a> | Add the X-Forwarded-Scheme and X-Scheme headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
@@ -395,6 +396,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-labelselector" href="#opt-providers-kubernetesingress-labelselector" title="#opt-providers-kubernetesingress-labelselector">providers.kubernetesingress.labelselector</a> | Kubernetes Ingress label selector to use. | |
| <a id="opt-providers-kubernetesingress-namespaces" href="#opt-providers-kubernetesingress-namespaces" title="#opt-providers-kubernetesingress-namespaces">providers.kubernetesingress.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesingress-nativelbbydefault" href="#opt-providers-kubernetesingress-nativelbbydefault" title="#opt-providers-kubernetesingress-nativelbbydefault">providers.kubernetesingress.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing mode by default. | false |
| <a id="opt-providers-kubernetesingress-reportnodeinternalips" href="#opt-providers-kubernetesingress-reportnodeinternalips" title="#opt-providers-kubernetesingress-reportnodeinternalips">providers.kubernetesingress.reportnodeinternalips</a> | Report node internal IPs in Ingress status. | false |
| <a id="opt-providers-kubernetesingress-strictprefixmatching" href="#opt-providers-kubernetesingress-strictprefixmatching" title="#opt-providers-kubernetesingress-strictprefixmatching">providers.kubernetesingress.strictprefixmatching</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). | false |
| <a id="opt-providers-kubernetesingress-throttleduration" href="#opt-providers-kubernetesingress-throttleduration" title="#opt-providers-kubernetesingress-throttleduration">providers.kubernetesingress.throttleduration</a> | Ingress refresh throttle duration | 0 |
| <a id="opt-providers-kubernetesingress-token" href="#opt-providers-kubernetesingress-token" title="#opt-providers-kubernetesingress-token">providers.kubernetesingress.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
@@ -89,6 +89,7 @@ additionalArguments:
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
| <a id="opt-forwardedHeaders-connection" href="#opt-forwardedHeaders-connection" title="#opt-forwardedHeaders-connection">`forwardedHeaders.`<br />`connection`</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No |
| <a id="opt-forwardedHeaders-addXForwardedSchemeHeaders" href="#opt-forwardedHeaders-addXForwardedSchemeHeaders" title="#opt-forwardedHeaders-addXForwardedSchemeHeaders">`forwardedHeaders.`<br />`addXForwardedSchemeHeaders`</a> | Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No |
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.`<br />`insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.`<br />`trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
@@ -392,6 +393,37 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
--entryPoints.web.forwardedHeaders.connection=foobar
```
??? info "`forwardedHeaders.addXForwardedSchemeHeaders`"
Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme` next to `X-Forwarded-Proto`.
This is primarily useful when migrating from ingress-nginx and your applications still rely on these legacy headers.
When enabled, these compatibility headers follow the same value as `X-Forwarded-Proto`.
```yaml tab="File (YAML)"
## Static configuration
entryPoints:
websecure:
address: ":443"
forwardedHeaders:
addXForwardedSchemeHeaders: true
```
```toml tab="File (TOML)"
## Static configuration
[entryPoints]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.forwardedHeaders]
addXForwardedSchemeHeaders = true
```
```bash tab="CLI"
## Static configuration
--entryPoints.websecure.address=:443
--entryPoints.websecure.forwardedHeaders.addXForwardedSchemeHeaders=true
```
### HTTP3
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
@@ -58,12 +58,13 @@ which in turn creates the resulting routers, services, handlers, etc.
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-hostname" href="#opt-providers-kubernetesIngress-ingressEndpoint-hostname" title="#opt-providers-kubernetesIngress-ingressEndpoint-hostname">`providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`</a> | Hostname used for Kubernetes Ingress endpoints. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-ip" href="#opt-providers-kubernetesIngress-ingressEndpoint-ip" title="#opt-providers-kubernetesIngress-ingressEndpoint-ip">`providers.kubernetesIngress.`<br />`ingressEndpoint.ip`</a> | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-publishedService" href="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService" title="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService">`providers.kubernetesIngress.`<br />`ingressEndpoint.publishedService`</a> | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice). | "" | No |
| <a id="opt-providers-kubernetesIngress-reportNodeInternalIPs" href="#opt-providers-kubernetesIngress-reportNodeInternalIPs" title="#opt-providers-kubernetesIngress-reportNodeInternalIPs">`providers.kubernetesIngress.reportNodeInternalIPs`</a> | Report node internal IPs in Ingress status.<br />Incompatible with `ingressEndpoint` and `disableClusterScopeResources`.<br />More information [here](#reportnodeinternalips). | false | No |
| <a id="opt-providers-kubernetesIngress-throttleDuration" href="#opt-providers-kubernetesIngress-throttleDuration" title="#opt-providers-kubernetesIngress-throttleDuration">`providers.kubernetesIngress.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
| <a id="opt-providers-kubernetesIngress-allowEmptyServices" href="#opt-providers-kubernetesIngress-allowEmptyServices" title="#opt-providers-kubernetesIngress-allowEmptyServices">`providers.kubernetesIngress.allowEmptyServices`</a> | Allows creating a route to reach a service that has no endpoint available.<br />It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
| <a id="opt-providers-kubernetesIngress-allowExternalNameServices" href="#opt-providers-kubernetesIngress-allowExternalNameServices" title="#opt-providers-kubernetesIngress-allowExternalNameServices">`providers.kubernetesIngress.allowExternalNameServices`</a> | Allows the `Ingress` to reference ExternalName services. | false | No |
| <a id="opt-providers-kubernetesIngress-crossProviderNamespaces" href="#opt-providers-kubernetesIngress-crossProviderNamespaces" title="#opt-providers-kubernetesIngress-crossProviderNamespaces">`providers.kubernetesIngress.crossProviderNamespaces`</a> | List of namespaces from which Ingresses or Services are allowed to use `traefik.ingress.kubernetes.io/router.middlewares`, `traefik.ingress.kubernetes.io/router.tls.options`, or `traefik.ingress.kubernetes.io/service.serverstransport` annotations.<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| <a id="opt-providers-kubernetesIngress-nativeLBByDefault" href="#opt-providers-kubernetesIngress-nativeLBByDefault" title="#opt-providers-kubernetesIngress-nativeLBByDefault">`providers.kubernetesIngress.nativeLBByDefault`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.<br />It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services. | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services and is incompatible with `reportNodeInternalIPs`. | false | No |
| <a id="opt-providers-kubernetesIngress-strictPrefixMatching" href="#opt-providers-kubernetesIngress-strictPrefixMatching" title="#opt-providers-kubernetesIngress-strictPrefixMatching">`providers.kubernetesIngress.strictPrefixMatching`</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
<!-- markdownlint-enable MD013 -->
@@ -138,6 +139,31 @@ providers:
--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
```
### `reportNodeInternalIPs`
When set to `true`, Traefik reports the internal IPs of all nodes in the cluster into the `status.loadBalancer.ingress` field of each managed Ingress resource.
This is the equivalent of ingress-nginx's `--report-node-internal-ip-address` flag and is the recommended approach for bare-metal Kubernetes deployments where Traefik runs as a DaemonSet without a cloud LoadBalancer or MetalLB.
This option requires cluster-scope access to Node resources and is mutually exclusive with `ingressEndpoint` and `disableClusterScopeResources`.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
reportNodeInternalIPs: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
reportNodeInternalIPs = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.reportnodeinternalips=true
```
## Routing Configuration
See the dedicated section in [routing](../../../../reference/routing-configuration/kubernetes/ingress.md).
@@ -23,7 +23,11 @@ http:
- "/path/to/rootca2.pem"
maxIdleConnsPerHost: 100
disableHTTP2: true
peerCertURI: "spiffe://example.org/peer"
peerCertSANs:
- type: DNSName
value: foo.com
- type: URI
value: spiffe://example.org/peer
forwardingTimeouts:
dialTimeout: "30s"
responseHeaderTimeout: "10s"
@@ -50,7 +54,7 @@ http:
rootcas = ["/path/to/rootca1.pem", "/path/to/rootca2.pem"]
maxIdleConnsPerHost = 100
disableHTTP2 = true
peerCertURI = "spiffe://example.org/peer"
peerCertSANs = [{type = "DNSName", value = "foo.com"}, {type = "URI", value = "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"
@@ -108,12 +112,14 @@ labels:
| <a id="opt-certificates" href="#opt-certificates" title="#opt-certificates">`certificates`</a> | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | [] | No |
| <a id="opt-insecureSkipVerify" href="#opt-insecureSkipVerify" title="#opt-insecureSkipVerify">`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-rootcas" href="#opt-rootcas" title="#opt-rootcas">`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | [] | No |
| <a id="opt-cipherSuites" href="#opt-cipherSuites" title="#opt-cipherSuites">`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
| <a id="opt-minVersion" href="#opt-minVersion" title="#opt-minVersion">`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-maxVersion" href="#opt-maxVersion" title="#opt-maxVersion">`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-cipherSuites" href="#opt-cipherSuites" title="#opt-cipherSuites">`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
| <a id="opt-minVersion" href="#opt-minVersion" title="#opt-minVersion">`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-maxVersion" href="#opt-maxVersion" title="#opt-maxVersion">`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-maxIdleConnsPerHost" href="#opt-maxIdleConnsPerHost" title="#opt-maxIdleConnsPerHost">`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
| <a id="opt-disableHTTP2" href="#opt-disableHTTP2" title="#opt-disableHTTP2">`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
| <a id="opt-peerCertURI" href="#opt-peerCertURI" title="#opt-peerCertURI">`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| <a id="opt-peerCertSANs" href="#opt-peerCertSANs" title="#opt-peerCertSANs">`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
| <a id="opt-peerCertSANs-type" href="#opt-peerCertSANs-type" title="#opt-peerCertSANs-type">`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-peerCertSANs-value" href="#opt-peerCertSANs-value" title="#opt-peerCertSANs-value">`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-forwardingTimeouts-dialTimeout" href="#opt-forwardingTimeouts-dialTimeout" title="#opt-forwardingTimeouts-dialTimeout">`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />0 = no timeout | 30s | No |
| <a id="opt-forwardingTimeouts-responseHeaderTimeout" href="#opt-forwardingTimeouts-responseHeaderTimeout" title="#opt-forwardingTimeouts-responseHeaderTimeout">`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />0 = no timeout | 0s | No |
| <a id="opt-forwardingTimeouts-idleConnTimeout" href="#opt-forwardingTimeouts-idleConnTimeout" title="#opt-forwardingTimeouts-idleConnTimeout">`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />0 = no timeout | 90s | No |
@@ -53,35 +53,25 @@ spec:
## Configuration Options
| Field | Description | Default | Required |
|:------|:----------------------------------------------------------|:---------------------|:---------|
| <a id="opt-serverstransport-serverName" href="#opt-serverstransport-serverName" title="#opt-serverstransport-serverName">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
| <a id="opt-serverstransport-insecureSkipVerify" href="#opt-serverstransport-insecureSkipVerify" title="#opt-serverstransport-insecureSkipVerify">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-serverstransport-rootcas" href="#opt-serverstransport-rootcas" title="#opt-serverstransport-rootcas">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| <a id="opt-serverstransport-certificatesSecrets" href="#opt-serverstransport-certificatesSecrets" title="#opt-serverstransport-certificatesSecrets">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
| <a id="opt-serverstransport-maxIdleConnsPerHost" href="#opt-serverstransport-maxIdleConnsPerHost" title="#opt-serverstransport-maxIdleConnsPerHost">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
| <a id="opt-serverstransport-disableHTTP2" href="#opt-serverstransport-disableHTTP2" title="#opt-serverstransport-disableHTTP2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
| <a id="opt-serverstransport-peerCertURI" href="#opt-serverstransport-peerCertURI" title="#opt-serverstransport-peerCertURI">`serverstransport.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout" href="#opt-serverstransport-forwardingTimeouts-dialTimeout" title="#opt-serverstransport-forwardingTimeouts-dialTimeout">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
| <a id="opt-serverstransport-serverName-2" href="#opt-serverstransport-serverName-2" title="#opt-serverstransport-serverName-2">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
| <a id="opt-serverstransport-insecureSkipVerify-2" href="#opt-serverstransport-insecureSkipVerify-2" title="#opt-serverstransport-insecureSkipVerify-2">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-serverstransport-rootcas-2" href="#opt-serverstransport-rootcas-2" title="#opt-serverstransport-rootcas-2">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| <a id="opt-serverstransport-certificatesSecrets-2" href="#opt-serverstransport-certificatesSecrets-2" title="#opt-serverstransport-certificatesSecrets-2">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
| <a id="opt-serverstransport-cipherSuites" href="#opt-serverstransport-cipherSuites" title="#opt-serverstransport-cipherSuites">`serverstransport.`<br />`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
| <a id="opt-serverstransport-minVersion" href="#opt-serverstransport-minVersion" title="#opt-serverstransport-minVersion">`serverstransport.`<br />`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-serverstransport-maxVersion" href="#opt-serverstransport-maxVersion" title="#opt-serverstransport-maxVersion">`serverstransport.`<br />`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-serverstransport-maxIdleConnsPerHost-2" href="#opt-serverstransport-maxIdleConnsPerHost-2" title="#opt-serverstransport-maxIdleConnsPerHost-2">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
| <a id="opt-serverstransport-disableHTTP2-2" href="#opt-serverstransport-disableHTTP2-2" title="#opt-serverstransport-disableHTTP2-2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
| <a id="opt-serverstransport-peerCertURI-2" href="#opt-serverstransport-peerCertURI-2" title="#opt-serverstransport-peerCertURI-2">`serverstransport.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout-2" href="#opt-serverstransport-forwardingTimeouts-dialTimeout-2" title="#opt-serverstransport-forwardingTimeouts-dialTimeout-2">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout-2" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout-2" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout-2">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
| <a id="opt-serverstransport-spiffe-ids-2" href="#opt-serverstransport-spiffe-ids-2" title="#opt-serverstransport-spiffe-ids-2">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| <a id="opt-serverstransport-spiffe-trustDomain-2" href="#opt-serverstransport-spiffe-trustDomain-2" title="#opt-serverstransport-spiffe-trustDomain-2">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
| Field | Description | Default | Required |
|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------|:--------|:---------|
| <a id="opt-serverstransport-serverName" href="#opt-serverstransport-serverName" title="#opt-serverstransport-serverName">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
| <a id="opt-serverstransport-insecureSkipVerify" href="#opt-serverstransport-insecureSkipVerify" title="#opt-serverstransport-insecureSkipVerify">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-serverstransport-rootcas" href="#opt-serverstransport-rootcas" title="#opt-serverstransport-rootcas">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| <a id="opt-serverstransport-certificatesSecrets" href="#opt-serverstransport-certificatesSecrets" title="#opt-serverstransport-certificatesSecrets">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
| <a id="opt-serverstransport-maxIdleConnsPerHost" href="#opt-serverstransport-maxIdleConnsPerHost" title="#opt-serverstransport-maxIdleConnsPerHost">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
| <a id="opt-serverstransport-disableHTTP2" href="#opt-serverstransport-disableHTTP2" title="#opt-serverstransport-disableHTTP2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
| <a id="opt-serverstransport-peerCertSANs" href="#opt-serverstransport-peerCertSANs" title="#opt-serverstransport-peerCertSANs">`serverstransport.`<br />`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
| <a id="opt-serverstransport-peerCertSANs-type" href="#opt-serverstransport-peerCertSANs-type" title="#opt-serverstransport-peerCertSANs-type">`serverstransport.`<br />`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-serverstransport-peerCertSANs-value" href="#opt-serverstransport-peerCertSANs-value" title="#opt-serverstransport-peerCertSANs-value">`serverstransport.`<br />`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout" href="#opt-serverstransport-forwardingTimeouts-dialTimeout" title="#opt-serverstransport-forwardingTimeouts-dialTimeout">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
| <a id="opt-serverstransport-cipherSuites" href="#opt-serverstransport-cipherSuites" title="#opt-serverstransport-cipherSuites">`serverstransport.`<br />`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
| <a id="opt-serverstransport-minVersion" href="#opt-serverstransport-minVersion" title="#opt-serverstransport-minVersion">`serverstransport.`<br />`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
| <a id="opt-serverstransport-maxVersion" href="#opt-serverstransport-maxVersion" title="#opt-serverstransport-maxVersion">`serverstransport.`<br />`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
!!! note "CA Secret"
The CA secret must contain a base64 encoded certificate under either a tls.ca or a ca.crt key.
@@ -48,7 +48,9 @@ spec:
| <a id="opt-terminationDelay" href="#opt-terminationDelay" title="#opt-terminationDelay">`terminationDelay`</a> | Defines the delay to wait before fully terminating the connection, after one connected peer has closed its writing capability. | 100ms | No |
| <a id="opt-tls-serverName" href="#opt-tls-serverName" title="#opt-tls-serverName">`tls.serverName`</a> | ServerName used to contact the server. | "" | No |
| <a id="opt-tls-insecureSkipVerify" href="#opt-tls-insecureSkipVerify" title="#opt-tls-insecureSkipVerify">`tls.insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-tls-peerCertURI" href="#opt-tls-peerCertURI" title="#opt-tls-peerCertURI">`tls.peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| <a id="opt-tls-peerCertSANs" href="#opt-tls-peerCertSANs" title="#opt-tls-peerCertSANs">`tls.peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
| <a id="opt-tls-peerCertSANs-type" href="#opt-tls-peerCertSANs-type" title="#opt-tls-peerCertSANs-type">`tls.peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-tls-peerCertSANs-value" href="#opt-tls-peerCertSANs-value" title="#opt-tls-peerCertSANs-value">`tls.peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-tls-rootCAsSecrets" href="#opt-tls-rootCAsSecrets" title="#opt-tls-rootCAsSecrets">`tls.rootCAsSecrets`</a> | Defines the set of root certificate authorities to use when verifying server certificates.<br />The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key. | "" | No |
| <a id="opt-tls-certificatesSecrets" href="#opt-tls-certificatesSecrets" title="#opt-tls-certificatesSecrets">`tls.certificatesSecrets`</a> | Certificates to present to the server for mTLS. | "" | No |
| <a id="opt-spiffe" href="#opt-spiffe" title="#opt-spiffe">`spiffe`</a> | Configures [SPIFFE](../../../../install-configuration/tls/spiffe.md) options. | "" | No |
@@ -41,6 +41,7 @@ creating the corresponding routers, services, middlewares, and other components
Important differences in default behaviors:
- **Request buffering**: NGINX enables `proxy-request-buffering` by default, while Traefik requires explicit opt-in via the provider's `proxyRequestBuffering` option.
- **Legacy scheme headers**: If your applications depend on `X-Forwarded-Scheme` or `X-Scheme`, enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the relevant entrypoints.
To ensure consistent behavior during migration,
review and configure Traefik's provider-level options to match your current NGINX ConfigMap settings.
@@ -24,7 +24,11 @@ tcp:
insecureSkipVerify: true
rootcas:
- "/path/to/rootca.pem"
peerCertURI: "spiffe://example.org/peer"
peerCertSANs:
- type: DNSName
value: foo.com
- type: URI
value: spiffe://example.org/peer
spiffe:
ids:
- "spiffe://example.org/id1"
@@ -43,7 +47,7 @@ tcp:
certificates = ["/path/to/cert1.pem", "/path/to/cert2.pem"]
insecureSkipVerify = true
rootcas = ["/path/to/rootca.pem"]
peerCertURI = "spiffe://example.org/peer"
peerCertSANs = [{type = "DNSName", value = "foo.com"}, {type = "URI", value = "spiffe://example.org/peer"}]
[tcp.serversTransports.mytransport.spiffe]
ids = ["spiffe://example.org/id1", "spiffe://example.org/id2"]
@@ -96,7 +100,9 @@ labels:
| <a id="opt-serverstransport-tls-certificates" href="#opt-serverstransport-tls-certificates" title="#opt-serverstransport-tls-certificates">`serverstransport.`<br />`tls`<br />`.certificates`</a> | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | | No |
| <a id="opt-serverstransport-tls-insecureSkipVerify" href="#opt-serverstransport-tls-insecureSkipVerify" title="#opt-serverstransport-tls-insecureSkipVerify">`serverstransport.`<br />`tls`<br />`.insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
| <a id="opt-serverstransport-tls-rootcas" href="#opt-serverstransport-tls-rootcas" title="#opt-serverstransport-tls-rootcas">`serverstransport.`<br />`tls`<br />`.rootcas`</a> | Defines the root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| <a id="opt-serverstransport-tls-peerCertURI" href="#opt-serverstransport-tls-peerCertURI" title="#opt-serverstransport-tls-peerCertURI">`serverstransport.`<br />`tls.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | false | No |
| <a id="opt-serverstransport-tls-peerCertSANs" href="#opt-serverstransport-tls-peerCertSANs" title="#opt-serverstransport-tls-peerCertSANs">`serverstransport.`<br />`tls.`<br />`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
| <a id="opt-serverstransport-tls-peerCertSANs-type" href="#opt-serverstransport-tls-peerCertSANs-type" title="#opt-serverstransport-tls-peerCertSANs-type">`serverstransport.`<br />`tls.`<br />`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-serverstransport-tls-peerCertSANs-value" href="#opt-serverstransport-tls-peerCertSANs-value" title="#opt-serverstransport-tls-peerCertSANs-value">`serverstransport.`<br />`tls.`<br />`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
| <a id="opt-serverstransport-spiffe" href="#opt-serverstransport-spiffe" title="#opt-serverstransport-spiffe">`serverstransport.`<br />`spiffe`</a> | Defines the SPIFFE configuration. An empty `spiffe` section enables SPIFFE (that allows any SPIFFE ID). | | No |
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe`<br />`.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe`<br />`.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
@@ -50,6 +50,7 @@
insecure = true
trustedIPs = ["foobar", "foobar"]
connection = ["foobar", "foobar"]
addXForwardedSchemeHeaders = true
[entryPoints.EntryPoint0.http]
middlewares = ["foobar", "foobar"]
encodeQuerySemicolons = true
@@ -61,6 +61,7 @@ entryPoints:
connection:
- foobar
- foobar
addXForwardedSchemeHeaders: true
http:
redirections:
entryPoint:
Generated
+7 -44
View File
@@ -1,37 +1,16 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"type": "github"
"lastModified": 1778036283,
"narHash": "sha256-GW2cEd/cLcVbbCes8iQuoY2qGIeCA7UiaD351hpkXfI=",
"rev": "ed67bc86e84e51d4a88e73c7fd36006dc876476f",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre993032.ed67bc86e84e/nixexprs.tar.xz"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
}
},
"nixpkgs-golangci": {
@@ -68,26 +47,10 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-golangci": "nixpkgs-golangci",
"nixpkgs-kct": "nixpkgs-kct"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+26 -25
View File
@@ -3,7 +3,7 @@
inputs = {
# Main nixpkgs (used for gnused)
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
# Pinned nixpkgs for kubernetes-controller-tools
# Search: https://www.nixhub.io/packages/kubernetes-controller-tools
@@ -12,33 +12,34 @@
# Pinned nixpkgs for golangci-lint
# Search: https://www.nixhub.io/packages/golangci-lint
nixpkgs-golangci.url = "github:NixOS/nixpkgs/80d901ec0377e19ac3f7bb8c035201e2e098cc97";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, nixpkgs-kct, nixpkgs-golangci, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
outputs =
{
nixpkgs,
nixpkgs-kct,
nixpkgs-golangci,
...
}:
let
inherit (nixpkgs.lib) genAttrs;
forEachSystem = genAttrs nixpkgs.lib.systems.flakeExposed;
pkgs-kct = import nixpkgs-kct {
inherit system;
};
pkgs-golangci = import nixpkgs-golangci {
inherit system;
};
in
{
devShells.default = pkgs.mkShell {
pkgsForEach = nixpkgs.legacyPackages;
pkgsKctForEach = nixpkgs-kct.legacyPackages;
pkgsGolangCiForEach = nixpkgs-golangci.legacyPackages;
in
{
devShells = forEachSystem (system: {
default = pkgsForEach.${system}.mkShell {
packages = [
pkgs-kct.kubernetes-controller-tools
pkgs.gnused
pkgs-golangci.golangci-lint
pkgsForEach.${system}.gnused
pkgsKctForEach.${system}.kubernetes-controller-tools
pkgsGolangCiForEach.${system}.golangci-lint
];
};
}
);
}
});
formatter = forEachSystem (system: pkgsForEach.${system}.nixfmt);
};
}
+1 -1
View File
@@ -53,7 +53,7 @@ require (
github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pires/go-proxyproto v0.8.1
github.com/pires/go-proxyproto v0.12.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
+2 -2
View File
@@ -1748,8 +1748,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -0,0 +1,101 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints.websecure]
address = ":4443"
[entryPoints.websecure2]
address = ":4444"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
# --- Same host, same options, same entryPoint: no conflict, the options are applied. ---
[http.routers.same-1]
rule = "Host(`same.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-1.tls]
options = "tls12"
[http.routers.same-2]
rule = "Host(`same.www.snitest.com`) && PathPrefix(`/same`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-2.tls]
options = "tls12"
# --- Same host, different options, same entryPoint: conflict, fallback to default options. ---
[http.routers.conflict-1]
rule = "Host(`conflict.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-1.tls]
options = "tls12"
[http.routers.conflict-2]
rule = "Host(`conflict.www.snitest.com`) && PathPrefix(`/conflict`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-2.tls]
options = "tls13"
# --- Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options. ---
[http.routers.cross-ep1]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.cross-ep1.tls]
options = "tls12"
[http.routers.cross-ep2]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure2"]
service = "service1"
[http.routers.cross-ep2.tls]
options = "tls13"
# --- Domain fronting (Host header != SNI): same options follow the header, different options are rejected. ---
[http.routers.df-a]
rule = "Host(`df-a.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-a.tls]
options = "tls12"
[http.routers.df-b]
rule = "Host(`df-b.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-b.tls]
options = "tls12"
[http.routers.df-c]
rule = "Host(`df-c.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-c.tls]
options = "tls13"
[http.services.service1]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
[tls.options]
[tls.options.tls12]
maxVersion = "VersionTLS12"
[tls.options.tls13]
minVersion = "VersionTLS13"
+35 -3
View File
@@ -2494,9 +2494,25 @@ spec:
description: MinVersion defines the minimum TLS version to use when
contacting backend servers.
type: string
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: PeerCertURI defines the peer cert URI used to match against
SAN URI during the peer certificate verification.
description: |-
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
@@ -2648,10 +2664,26 @@ spec:
insecureSkipVerify:
description: InsecureSkipVerify disables TLS certificate verification.
type: boolean
peerCertSANs:
description: PeerCertSANs defines the peer cert Subject Alternative
Names used to match against SAN during the peer certificate
verification.
items:
description: SAN represents a Subject Alternative Name.
properties:
type:
description: SANType is the type of the Subject Alternative
Name.
type: string
value:
type: string
type: object
type: array
peerCertURI:
description: |-
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
type: string
rootCAs:
description: RootCAs defines a list of CA certificate Secrets
@@ -30,10 +30,11 @@ profiles:
result: success
statistics:
Failed: 0
Passed: 20
Passed: 21
Skipped: 0
supportedFeatures:
- BackendTLSPolicy
- BackendTLSPolicySANValidation
- GatewayPort8080
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendProtocolWebSocket
@@ -48,7 +49,6 @@ profiles:
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- BackendTLSPolicySANValidation
- GatewayAddressEmpty
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
+148 -1
View File
@@ -415,7 +415,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
// with unknown tls option
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"))
require.NoError(s.T(), err)
}
@@ -1262,6 +1262,153 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
}
}
// TestWithTLSOptionsConflict checks how TLS options are resolved when several routers
// target the same host (SNI), across the different conflict situations:
// - same options on the same entryPoint: no conflict, the options are applied;
// - different options on the same entryPoint: conflict, fallback to the default options;
// - different options on different entryPoints: no conflict, each entryPoint keeps its
// own options (they are selected independently on each listener);
// - domain fronting (Host header != SNI): allowed when both resolve to the same options,
// rejected with a 421 otherwise.
//
// The effective TLS options are probed through the negotiated TLS version: the "tls12"
// options cap the version to TLS 1.2, while the "tls13" options require at least TLS 1.3.
func (s *HTTPSSuite) TestWithTLSOptionsConflict() {
backend := startTestServer("9010", http.StatusOK, "server1")
defer backend.Close()
file := s.adaptFile("fixtures/https/https_tls_options_conflict.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`cross.www.snitest.com`)"))
require.NoError(s.T(), err)
testCases := []struct {
desc string
addr string // entryPoint address to reach
hostHeader string
serverName string // SNI
minVersion uint16 // 0 means the crypto/tls library default
maxVersion uint16 // 0 means the crypto/tls library default
// expectHandshakeError is set when the TLS handshake itself is expected to fail
// (i.e. the probed options reject the client's TLS version). Otherwise
// expectedStatusCode is asserted on the HTTP response.
expectHandshakeError bool
expectedStatusCode int
}{
// Same host, same options, same entryPoint: no conflict, the "tls12" options are applied.
{
desc: "same options / same entryPoint: TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "same options / same entryPoint: TLS 1.3 client is rejected (maxVersion TLS1.2 enforced)",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
// Same host, different options, same entryPoint: conflict, both routers fall back to the default options.
{
desc: "conflicting options / same entryPoint: TLS 1.3 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "conflicting options / same entryPoint: TLS 1.2 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
// Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options.
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.3 client is rejected",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.3 client is accepted",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.2 client is rejected",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectHandshakeError: true,
},
// Domain fronting (Host header != SNI) on the same entryPoint.
{
desc: "domain fronting / same options: request follows the Host header (200)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-b.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "domain fronting / different options: request is misdirected (421)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-c.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusMisdirectedRequest,
},
}
for _, test := range testCases {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: test.serverName,
MinVersion: test.minVersion,
MaxVersion: test.maxVersion,
}
req, err := http.NewRequest(http.MethodGet, "https://"+test.addr+"/", nil)
require.NoError(s.T(), err)
req.Host = test.hostHeader
if test.expectHandshakeError {
_, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}).Do(req)
assert.ErrorContains(s.T(), err, "tls:", "test %q should fail the TLS handshake", test.desc)
continue
}
err = try.RequestWithTransport(req, 2*time.Second, &http.Transport{TLSClientConfig: tlsConfig}, try.StatusCodeIs(test.expectedStatusCode))
assert.NoError(s.T(), err, "test %q failed with: %v", test.desc, err)
}
}
// TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration.
func (s *HTTPSSuite) TestWithInvalidTLSOption() {
backend := startTestServer("9010", http.StatusOK, "server1")
+3 -3
View File
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
s.traefikCmd(withConfigFile(file))
// All errors
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"]`))
require.NoError(s.T(), err)
// router3 has an error because it uses an unknown entrypoint
@@ -957,11 +957,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
require.NoError(s.T(), err)
// router4 is enabled, but in warning state because its tls options conf was messed up
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
require.NoError(s.T(), err)
// router5 is disabled because its middleware conf is broken
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router5@file", 1000*time.Millisecond, try.BodyContains())
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router5@file", 1000*time.Millisecond, try.BodyContains())
require.NoError(s.T(), err)
}
+4 -2
View File
@@ -537,8 +537,10 @@ type ServersTransport struct {
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"`
PeerCertURI string `description:"Defines the 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:"Defines the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty"`
PeerCertSANs []traefiktls.SAN `description:"Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification." json:"peerCertSANs,omitempty" toml:"peerCertSANs,omitempty" yaml:"peerCertSANs,omitempty"`
Spiffe *Spiffe `description:"Defines the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// +k8s:deepcopy-gen=true
+4 -2
View File
@@ -207,8 +207,10 @@ type TLSClientConfig 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"`
PeerCertURI string `description:"Defines the 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:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty"`
PeerCertSANs []traefiktls.SAN `description:"Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification." json:"peerCertSANs,omitempty" toml:"peerCertSANs,omitempty" yaml:"peerCertSANs,omitempty"`
Spiffe *Spiffe `description:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// +k8s:deepcopy-gen=true
@@ -1855,6 +1855,11 @@ func (in *ServersTransport) DeepCopyInto(out *ServersTransport) {
*out = new(ForwardingTimeouts)
**out = **in
}
if in.PeerCertSANs != nil {
in, out := &in.PeerCertSANs, &out.PeerCertSANs
*out = make([]tls.SAN, len(*in))
copy(*out, *in)
}
if in.Spiffe != nil {
in, out := &in.Spiffe, &out.Spiffe
*out = new(Spiffe)
@@ -2522,6 +2527,11 @@ func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) {
*out = make(tls.Certificates, len(*in))
copy(*out, *in)
}
if in.PeerCertSANs != nil {
in, out := &in.PeerCertSANs, &out.PeerCertSANs
*out = make([]tls.SAN, len(*in))
copy(*out, *in)
}
if in.Spiffe != nil {
in, out := &in.Spiffe, &out.Spiffe
*out = new(Spiffe)
+5 -4
View File
@@ -150,10 +150,11 @@ type TLSConfig struct {
// ForwardedHeaders Trust client forwarding headers.
type ForwardedHeaders struct {
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AddXForwardedSchemeHeaders bool `description:"Add the X-Forwarded-Scheme and X-Scheme headers." json:"addXForwardedSchemeHeaders,omitempty" toml:"addXForwardedSchemeHeaders,omitempty" yaml:"addXForwardedSchemeHeaders,omitempty" export:"true"`
}
// ProxyProtocol contains Proxy-Protocol configuration.
@@ -18,12 +18,14 @@ const (
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
xForwardedScheme = "X-Forwarded-Scheme"
xForwardedServer = "X-Forwarded-Server"
XForwardedURI = "X-Forwarded-Uri"
XForwardedMethod = "X-Forwarded-Method"
XForwardedPrefix = "X-Forwarded-Prefix"
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
xScheme = "X-Scheme"
xRealIP = "X-Real-Ip"
connection = "Connection"
upgrade = "Upgrade"
@@ -34,6 +36,7 @@ const (
// that Go's HTTP server preserves (e.g. X_Forwarded_Proto).
var XHeadersSet = map[string]struct{}{
XForwardedProto: {},
xForwardedScheme: {},
XForwardedFor: {},
XForwardedHost: {},
XForwardedPort: {},
@@ -43,6 +46,7 @@ var XHeadersSet = map[string]struct{}{
XForwardedPrefix: {},
xForwardedTLSClientCert: {},
xForwardedTLSClientCertInfo: {},
xScheme: {},
xRealIP: {},
}
@@ -70,17 +74,18 @@ func isManagedXHeader(key string) bool {
// Unless insecure is set,
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
type XForwarded struct {
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
ipChecker *ip.Checker
next http.Handler
hostname string
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
addXForwardedSchemeHeaders bool
ipChecker *ip.Checker
next http.Handler
hostname string
}
// NewXForwarded creates a new XForwarded.
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, addXForwardedSchemeHeaders bool, next http.Handler) (*XForwarded, error) {
var ipChecker *ip.Checker
if len(trustedIPs) > 0 {
var err error
@@ -101,13 +106,14 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
}
return &XForwarded{
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
ipChecker: ipChecker,
next: next,
hostname: hostname,
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
addXForwardedSchemeHeaders: addXForwardedSchemeHeaders,
ipChecker: ipChecker,
next: next,
hostname: hostname,
}, nil
}
@@ -168,6 +174,12 @@ func (x *XForwarded) rewrite(outreq *http.Request) {
unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq))
}
if x.addXForwardedSchemeHeaders {
scheme := unsafeHeader(outreq.Header).Get(XForwardedProto)
unsafeHeader(outreq.Header).Set(xForwardedScheme, scheme)
unsafeHeader(outreq.Header).Set(xScheme, scheme)
}
if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" {
unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host)
}
@@ -17,12 +17,14 @@ func TestServeHTTP(t *testing.T) {
insecure bool
trustedIps []string
connectionHeaders []string
addSchemeHeaders bool
incomingHeaders map[string][]string
remoteAddr string
expectedHeaders map[string]string
tls bool
websocket bool
host string
absentHeaders []string
}{
{
desc: "all Empty",
@@ -230,6 +232,24 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "https",
},
},
{
desc: "xForwardedScheme headers with tls",
tls: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedScheme headers disabled keeps legacy headers absent",
tls: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
},
absentHeaders: []string{xForwardedScheme, xScheme},
},
{
desc: "xForwardedProto with websocket",
tls: false,
@@ -238,6 +258,16 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "ws",
},
},
{
desc: "xForwardedScheme headers with websocket",
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "ws",
xForwardedScheme: "ws",
xScheme: "ws",
},
},
{
desc: "xForwardedProto with websocket and tls",
tls: true,
@@ -246,6 +276,17 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers with websocket and tls",
tls: true,
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "wss",
xForwardedScheme: "wss",
xScheme: "wss",
},
},
{
desc: "xForwardedProto with websocket and tls and already x-forwarded-proto with wss",
tls: true,
@@ -257,6 +298,21 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers overwrite in insecure mode",
insecure: true,
addSchemeHeaders: true,
incomingHeaders: map[string][]string{
XForwardedProto: {"https"},
xForwardedScheme: {"external-https"},
xScheme: {"external-https"},
},
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedPort with explicit port",
host: "foo.com:8080",
@@ -643,7 +699,7 @@ func TestServeHTTP(t *testing.T) {
}
}
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, test.addSchemeHeaders,
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
require.NoError(t, err)
@@ -656,6 +712,10 @@ func TestServeHTTP(t *testing.T) {
for k, v := range test.expectedHeaders {
assert.Equal(t, v, req.Header.Get(k))
}
for _, header := range test.absentHeaders {
assert.NotContains(t, req.Header, http.CanonicalHeaderKey(header))
}
})
}
}
@@ -782,7 +842,7 @@ func TestConnection(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, false, nil)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
+24 -4
View File
@@ -462,7 +462,12 @@ func Test_buildConfiguration(t *testing.T) {
KeyFile: "key",
},
},
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
PeerCertSANs: []tls.SAN{
{
Type: tls.SANURIType,
Value: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
},
},
},
},
},
@@ -557,7 +562,12 @@ func Test_buildConfiguration(t *testing.T) {
KeyFile: "key",
},
},
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
PeerCertSANs: []tls.SAN{
{
Type: tls.SANURIType,
Value: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
},
},
},
},
},
@@ -2626,7 +2636,12 @@ func Test_buildConfiguration(t *testing.T) {
KeyFile: "key",
},
},
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/Test",
PeerCertSANs: []tls.SAN{
{
Type: tls.SANURIType,
Value: "spiffe:///ns/ns/dc/dc1/svc/Test",
},
},
},
},
},
@@ -3279,7 +3294,12 @@ func Test_buildConfiguration(t *testing.T) {
KeyFile: "key",
},
},
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/Test",
PeerCertSANs: []tls.SAN{
{
Type: tls.SANURIType,
Value: "spiffe:///ns/ns/dc/dc1/svc/Test",
},
},
},
},
},
+12 -2
View File
@@ -69,7 +69,12 @@ func (c *connectCert) serversTransport(item itemData) *dynamic.ServersTransport
Certificates: traefiktls.Certificates{
c.getLeaf(),
},
PeerCertURI: spiffeID,
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANURIType,
Value: spiffeID,
},
},
}
}
@@ -91,7 +96,12 @@ func (c *connectCert) tcpServersTransport(item itemData) *dynamic.TCPServersTran
Certificates: traefiktls.Certificates{
c.getLeaf(),
},
PeerCertURI: spiffeID,
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANURIType,
Value: spiffeID,
},
},
},
}
}
+4 -4
View File
@@ -177,7 +177,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
for _, ns := range namespaces {
factoryCrd := traefikinformers.NewSharedInformerFactoryWithOptions(c.csCrd, resyncPeriod, traefikinformers.WithNamespace(ns), traefikinformers.WithTweakListOptions(matchesLabelSelector))
factoryCrd := traefikinformers.NewSharedInformerFactoryWithOptions(c.csCrd, resyncPeriod, traefikinformers.WithNamespace(ns), traefikinformers.WithTweakListOptions(matchesLabelSelector), traefikinformers.WithTransform(k8s.StripManagedFields))
_, err := factoryCrd.Traefik().V1alpha1().IngressRoutes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -219,7 +219,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
return nil, err
}
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -233,7 +233,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
return nil, err
}
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -271,7 +271,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
if !c.disableClusterScopeInformer {
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
_, err := c.clusterScopeFactory.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -141,6 +141,11 @@ spec:
maxIdleConnsPerHost: 42
disableHTTP2: true
peerCertURI: foo://bar
peerCertSANs:
- type: DNSName
value: foo.com
- type: URI
value: foo://bar
rootCAsSecrets:
- root-ca0
- root-ca1
@@ -28,6 +28,7 @@ package v1alpha1
import (
dynamic "github.com/traefik/traefik/v3/pkg/config/dynamic"
tls "github.com/traefik/traefik/v3/pkg/tls"
)
// ServersTransportSpecApplyConfiguration represents a declarative configuration of the ServersTransportSpec type for use
@@ -60,7 +61,11 @@ type ServersTransportSpecApplyConfiguration struct {
// DisableHTTP2 disables HTTP/2 for connections with backend servers.
DisableHTTP2 *bool `json:"disableHTTP2,omitempty"`
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
//
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI *string `json:"peerCertURI,omitempty"`
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
PeerCertSANs []tls.SAN `json:"peerCertSANs,omitempty"`
// Spiffe defines the SPIFFE configuration.
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
}
@@ -178,6 +183,16 @@ func (b *ServersTransportSpecApplyConfiguration) WithPeerCertURI(value string) *
return b
}
// WithPeerCertSANs adds the given value to the PeerCertSANs field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the PeerCertSANs field.
func (b *ServersTransportSpecApplyConfiguration) WithPeerCertSANs(values ...tls.SAN) *ServersTransportSpecApplyConfiguration {
for i := range values {
b.PeerCertSANs = append(b.PeerCertSANs, values[i])
}
return b
}
// WithSpiffe sets the Spiffe field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Spiffe field is set to the value of the last call.
@@ -28,6 +28,7 @@ package v1alpha1
import (
dynamic "github.com/traefik/traefik/v3/pkg/config/dynamic"
tls "github.com/traefik/traefik/v3/pkg/tls"
)
// TLSClientConfigApplyConfiguration represents a declarative configuration of the TLSClientConfig type for use
@@ -47,9 +48,12 @@ type TLSClientConfigApplyConfiguration struct {
RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"`
// CertificatesSecrets defines a list of secret storing client certificates for mTLS.
CertificatesSecrets []string `json:"certificatesSecrets,omitempty"`
// MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
//
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI *string `json:"peerCertURI,omitempty"`
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
PeerCertSANs []tls.SAN `json:"peerCertSANs,omitempty"`
// Spiffe defines the SPIFFE configuration.
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
}
@@ -117,6 +121,16 @@ func (b *TLSClientConfigApplyConfiguration) WithPeerCertURI(value string) *TLSCl
return b
}
// WithPeerCertSANs adds the given value to the PeerCertSANs field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the PeerCertSANs field.
func (b *TLSClientConfigApplyConfiguration) WithPeerCertSANs(values ...tls.SAN) *TLSClientConfigApplyConfiguration {
for i := range values {
b.PeerCertSANs = append(b.PeerCertSANs, values[i])
}
return b
}
// WithSpiffe sets the Spiffe field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Spiffe field is set to the value of the last call.
@@ -541,6 +541,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
MaxIdleConnsPerHost: serversTransport.Spec.MaxIdleConnsPerHost,
ForwardingTimeouts: forwardingTimeout,
PeerCertURI: serversTransport.Spec.PeerCertURI,
PeerCertSANs: serversTransport.Spec.PeerCertSANs,
Spiffe: serversTransport.Spec.Spiffe,
}
}
@@ -5472,6 +5472,16 @@ func TestLoadIngressRoutes(t *testing.T) {
PingTimeout: ptypes.Duration(42 * time.Second),
},
PeerCertURI: "foo://bar",
PeerCertSANs: []tls.SAN{
{
Type: tls.SANDNSNameType,
Value: "foo.com",
},
{
Type: tls.SANURIType,
Value: "foo://bar",
},
},
Spiffe: &dynamic.Spiffe{
IDs: []string{
"spiffe://foo/buz",
@@ -2,6 +2,7 @@ package v1alpha1
import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
@@ -53,7 +54,11 @@ type ServersTransportSpec struct {
// DisableHTTP2 disables HTTP/2 for connections with backend servers.
DisableHTTP2 bool `json:"disableHTTP2,omitempty"`
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
//
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI string `json:"peerCertURI,omitempty"`
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
PeerCertSANs []traefiktls.SAN `json:"peerCertSANs,omitempty"`
// Spiffe defines the SPIFFE configuration.
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
}
@@ -2,6 +2,7 @@ package v1alpha1
import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
@@ -59,9 +60,12 @@ type TLSClientConfig struct {
RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"`
// CertificatesSecrets defines a list of secret storing client certificates for mTLS.
CertificatesSecrets []string `json:"certificatesSecrets,omitempty"`
// MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
//
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
PeerCertURI string `json:"peerCertURI,omitempty"`
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
PeerCertSANs []traefiktls.SAN `json:"peerCertSANs,omitempty"`
// Spiffe defines the SPIFFE configuration.
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
}
@@ -1618,6 +1618,11 @@ func (in *ServersTransportSpec) DeepCopyInto(out *ServersTransportSpec) {
*out = new(ForwardingTimeouts)
(*in).DeepCopyInto(*out)
}
if in.PeerCertSANs != nil {
in, out := &in.PeerCertSANs, &out.PeerCertSANs
*out = make([]tls.SAN, len(*in))
copy(*out, *in)
}
if in.Spiffe != nil {
in, out := &in.Spiffe, &out.Spiffe
*out = new(dynamic.Spiffe)
@@ -1871,6 +1876,11 @@ func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.PeerCertSANs != nil {
in, out := &in.PeerCertSANs, &out.PeerCertSANs
*out = make([]tls.SAN, len(*in))
copy(*out, *in)
}
if in.Spiffe != nil {
in, out := &in.Spiffe, &out.Spiffe
*out = new(dynamic.Spiffe)
+5 -13
View File
@@ -148,20 +148,20 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
options.LabelSelector = c.labelSelector
}
c.factoryNamespace = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
c.factoryNamespace = kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
_, err := c.factoryNamespace.Core().V1().Namespaces().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
c.factoryGatewayClass = gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithTweakListOptions(labelSelectorOptions))
c.factoryGatewayClass = gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithTweakListOptions(labelSelectorOptions), gateinformers.WithTransform(k8s.StripManagedFields))
_, err = c.factoryGatewayClass.Gateway().V1().GatewayClasses().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
for _, ns := range namespaces {
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -171,7 +171,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
return nil, err
}
factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns))
factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns), gateinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryGateway.Gateway().V1().Gateways().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -209,7 +209,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
}
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -727,14 +727,6 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
continue
}
// Keep statuses added by Traefik for other ancestors.
// A BackendTLSPolicy can target services attached to different listeners.
if !slices.ContainsFunc(status.Ancestors, func(s gatev1.PolicyAncestorStatus) bool {
return reflect.DeepEqual(s.AncestorRef, ancestorStatus.AncestorRef)
}) {
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
}
}
if len(ancestorStatuses) > 16 {
@@ -15,6 +15,7 @@ var SupportedFeatures = sync.OnceValue(func() []features.FeatureName {
Insert(features.HTTPRouteExtendedFeatures.Intersection(extendedHTTPRouteFeatures()).UnsortedList()...).
Insert(features.ReferenceGrantCoreFeatures.UnsortedList()...).
Insert(features.BackendTLSPolicyCoreFeatures.UnsortedList()...).
Insert(features.BackendTLSPolicyExtendedFeatures.Intersection(extendedBackendTLSPolicyFeatures()).UnsortedList()...).
Insert(features.GRPCRouteCoreFeatures.UnsortedList()...).
Insert(features.TLSRouteCoreFeatures.UnsortedList()...).
Insert(features.TLSRouteExtendedFeatures.Intersection(extendedTLSRouteFeatures()).UnsortedList()...)
@@ -56,3 +57,10 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] {
features.HTTPRouteBackendRequestHeaderModificationFeature,
)
}
// extendedBackendTLSPolicyFeatures returns the supported extended BackendTLSPolicy features.
func extendedBackendTLSPolicyFeatures() sets.Set[features.Feature] {
return sets.New(
features.BackendTLSPolicySanValidationFeature,
)
}
@@ -0,0 +1,108 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: web
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -0,0 +1,64 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: web
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
subjectAltNames:
- type: Hostname
hostname: whoami.default.svc.cluster.local
- type: URI
uri: spiffe://cluster.local/ns/default/sa/whoami
@@ -0,0 +1,60 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
@@ -0,0 +1,55 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: Exact
value: /bar
backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 80
- name: whoami
port: 80
weight: 1
group: ""
kind: Service
@@ -0,0 +1,70 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: Exact
value: /bar
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
subjectAltNames:
- type: Hostname
hostname: whoami.default.svc.cluster.local
- type: URI
uri: spiffe://cluster.local/ns/default/sa/whoami
@@ -0,0 +1,51 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tcp
protocol: TCP
port: 9000
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
group: ""
kind: Service
@@ -0,0 +1,67 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
tls:
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
@@ -0,0 +1,124 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -0,0 +1,80 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
subjectAltNames:
- type: Hostname
hostname: whoami.default.svc.cluster.local
- type: URI
uri: spiffe://cluster.local/ns/default/sa/whoami
@@ -0,0 +1,78 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
+135 -38
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
@@ -21,7 +22,7 @@ import (
)
// TODO: as described in the specification https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.GRPCRoute, we should check for hostname conflicts between HTTP and GRPC routes.
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListGRPCRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
@@ -39,9 +40,8 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -77,7 +77,7 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames)
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -85,23 +85,12 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.GRPCRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateGRPCRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update GRPCRoute status")
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -168,7 +157,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadGRPCService(conf, routerName, routeRule, route)
router.Service, serviceCondition = p.loadGRPCService(listener, conf, routerName, routeRule, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -181,7 +170,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute) (string, *metav1.Condition) {
func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -190,7 +179,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
var wrr dynamic.WeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range routeRule.BackendRefs {
svcName, svc, errCondition := p.loadGRPCBackendRef(route, backendRef)
svcName, svc, errCondition := p.loadGRPCBackendRef(listener, conf, route, backendRef, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
condition = errCondition
@@ -219,7 +208,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
return name, condition
}
func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (string, *dynamic.Service, *metav1.Condition) {
func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, statusReport *statusReport) (string, *dynamic.Service, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -271,11 +260,16 @@ func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr + "-grpc")
lb, errCondition := p.loadGRPCServers(namespace, route, backendRef)
lb, st, errCondition := p.loadGRPCServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.HTTP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
}
@@ -325,10 +319,10 @@ func (p *Provider) loadGRPCMiddlewares(conf *dynamic.Configuration, namespace, r
return middlewareNames, nil
}
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -338,26 +332,128 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
}
}
protocol, err := getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only \"kubernetes.io/h2c\" and \"https\" appProtocol is supported", namespace, backendRef.Name),
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.ServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
// If a ServersTransport is set, it means a BackendTLSPolicy matched the service port, and we can safely assume the protocol is HTTPS.
// When no ServersTransport is set, we need to determine the protocol based on the service port.
protocol := "https"
if serversTransport == nil {
protocol, err = getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: %s", namespace, backendRef.Name, err),
}
}
}
@@ -369,7 +465,8 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port)))),
})
}
return lb, nil
return lb, serversTransport, nil
}
func buildGRPCMatchRule(hostnames []gatev1.Hostname, match gatev1.GRPCRouteMatch) (string, int) {
+59 -38
View File
@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -23,7 +24,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
@@ -41,9 +42,8 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -79,7 +79,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames)
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -87,23 +87,12 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update HTTPRoute status")
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -176,7 +165,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path)
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -191,7 +180,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -202,7 +191,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
for _, backendRef := range routeRule.BackendRefs {
// TODO in loadService we need to always return a non-nil serviceName even when there is an error which is not the
// usual defacto.
svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef, pathMatch)
svcName, errCondition := p.loadService(listener, conf, route, backendRef, pathMatch, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
log.Ctx(ctx).Error().
@@ -229,7 +218,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
// loadService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -240,6 +229,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
@@ -304,7 +304,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, st, errCondition := p.loadHTTPServers(ctx, namespace, route, backendRef, listener)
lb, st, errCondition := p.loadHTTPServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, errCondition
}
@@ -431,7 +431,7 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace)
}
func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, nil, &metav1.Condition{
@@ -508,12 +508,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
},
)
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update conflicting BackendTLSPolicy status")
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
@@ -540,12 +535,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
})
}
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update BackendTLSPolicy status")
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something wen wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
@@ -595,6 +585,37 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
ServerName: string(policy.Spec.Validation.Hostname),
}
if len(policy.Spec.Validation.SubjectAltNames) > 0 {
// Per the Gateway API specification the Hostname should only be used for authentication
// and not for certificate validation. Thus, if SubjectAltNames is specified, we ignore
// the Hostname validation by setting the InsecureSkipVerify option to true.
st.InsecureSkipVerify = true
for _, san := range policy.Spec.Validation.SubjectAltNames {
switch san.Type {
case gatev1.URISubjectAltNameType:
st.PeerCertSANs = append(st.PeerCertSANs, tls.SAN{
Type: tls.SANURIType,
Value: string(san.URI),
})
case gatev1.HostnameSubjectAltNameType:
st.PeerCertSANs = append(st.PeerCertSANs, tls.SAN{
Type: tls.SANDNSNameType,
Value: string(san.Hostname),
})
default:
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: fmt.Sprintf("Unsupported SubjectAltName type %q; only URI and Hostname types are supported", san.Type),
}
}
}
}
if policy.Spec.Validation.WellKnownCACertificates != nil {
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
@@ -606,7 +627,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -619,7 +640,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
var caCRT string
switch caCertRef.Kind {
case "ConfigMap":
case kindConfigMap:
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
@@ -632,7 +653,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
}
caCRT = configmap.Data["ca.crt"]
case "Secret":
case kindSecret:
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
+37 -37
View File
@@ -49,6 +49,8 @@ const (
kindTCPRoute = "TCPRoute"
kindTLSRoute = "TLSRoute"
kindService = "Service"
kindConfigMap = "ConfigMap"
kindSecret = "Secret"
appProtocolHTTP = "http"
appProtocolHTTPS = "https"
@@ -222,20 +224,29 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
// Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events.
// This is fine, because we don't treat different event types differently.
// But if we do in the future, we'll need to track more information about the dropped events.
conf := p.loadConfigurationFromGateways(ctxLog)
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
conf, statusReport, err := p.loadConfigurationFromGateways(ctxLog)
if err != nil {
logger.Error().Err(err).Msg("Unable to load configuration from Gateways")
} else {
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
}
}
// Flush regardless of whether the dynamic configuration changed: the
// statusReport is independent of confHash and may carry writes even
// when the data plane has nothing new to consume (e.g. a GatewayClass
// that's now Accepted but has no Gateway pointing at it yet).
statusReport.Flush(ctxLog, p.client)
}
// If we're throttling,
@@ -302,7 +313,8 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration {
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.Configuration, *statusReport, error) {
statusReport := newStatusReport()
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@@ -325,14 +337,12 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
addresses, err := p.gatewayAddresses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses")
return nil
return nil, nil, fmt.Errorf("getting gateway addresses: %w", err)
}
gatewayClasses, err := p.client.ListGatewayClasses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses")
return nil
return nil, nil, fmt.Errorf("listing gateway classes: %w", err)
}
var supportedFeatures []gatev1.SupportedFeature
@@ -363,13 +373,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
SupportedFeatures: supportedFeatures,
}
if err := p.client.UpdateGatewayClassStatus(ctx, gatewayClass.Name, status); err != nil {
log.Ctx(ctx).
Warn().
Err(err).
Str("gateway_class", gatewayClass.Name).
Msg("Unable to update GatewayClass status")
}
statusReport.RecordGatewayClassStatus(gatewayClass.Name, status)
}
var gateways []*gatev1.Gateway
@@ -390,14 +394,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...)
}
p.loadHTTPRoutes(ctx, gatewayListeners, conf)
p.loadHTTPRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadGRPCRoutes(ctx, gatewayListeners, conf)
p.loadGRPCRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadTLSRoutes(ctx, gatewayListeners, conf)
p.loadTLSRoutes(ctx, gatewayListeners, conf, statusReport)
if p.ExperimentalChannel {
p.loadTCPRoutes(ctx, gatewayListeners, conf)
p.loadTCPRoutes(ctx, gatewayListeners, conf, statusReport)
}
for _, gateway := range gateways {
@@ -428,14 +432,10 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
Msg("Gateway Not Accepted")
}
if err = p.client.UpdateGatewayStatus(ctx, ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update Gateway status")
}
statusReport.RecordGatewayStatus(ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus)
}
return conf
return conf, statusReport, nil
}
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
@@ -591,7 +591,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
var errCertConditions []metav1.Condition
listenerTLSCerts := make(map[string]*tls.CertAndStores)
for _, certificateRef := range listener.TLS.CertificateRefs {
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
if certificateRef.Kind == nil || *certificateRef.Kind != kindSecret || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -604,7 +604,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
}
certificateNamespace := string(ptr.Deref(certificateRef.Namespace, gatev1.Namespace(gateway.Namespace)))
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, kindSecret, string(certificateRef.Name), certificateNamespace); err != nil {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -95,7 +95,10 @@ func TestGatewayClassLabelSelector(t *testing.T) {
client: client,
}
_ = p.loadConfigurationFromGateways(t.Context())
_, statusReport, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
statusReport.Flush(t.Context(), p.client)
gw, err := gwClient.GatewayV1().Gateways("default").Get(t.Context(), "traefik-external", metav1.GetOptions{})
require.NoError(t, err)
@@ -2471,6 +2474,78 @@ func TestLoadHTTPRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute and BackendTLSPolicy with subjectAltNames",
paths: []string{"services.yml", "httproute/with_backend_tls_policy_san.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3": {
EntryPoints: []string{"web"},
Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-wrr",
Rule: `Host("foo.com") && Path("/bar")`,
Priority: 100008,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-http-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-http-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-http-80",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-http-80": {
ServerName: "whoami",
InsecureSkipVerify: true,
PeerCertSANs: []tls.SAN{
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute and BackendTLSPolicy with System CA",
paths: []string{"services.yml", "httproute/with_backend_tls_policy_system.yml"},
@@ -2753,7 +2828,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3216,7 +3293,9 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) {
p.RegisterBackendFuncs(group, kind, backendFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3502,7 +3581,265 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) {
p.RegisterFilterFuncs(group, kind, filterFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
}
func TestLoadGRPCRoutes(t *testing.T) {
testCases := []struct {
desc string
paths []string
expected *dynamic.Configuration
entryPoints map[string]Entrypoint
}{
{
desc: "Simple GRPCRoute and BackendTLSPolicy with CA certificate",
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
EntryPoints: []string{"web"},
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80-grpc",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80-grpc": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-80-grpc",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-80-grpc": {
ServerName: "whoami",
RootCAs: []types.FileOrContent{
"CA1",
"CA2",
"CA1-secret",
"CA2-secret",
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple GRPCRoute and BackendTLSPolicy with subjectAltNames",
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy_san.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
EntryPoints: []string{"web"},
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80-grpc",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80-grpc": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-80-grpc",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-80-grpc": {
ServerName: "whoami",
InsecureSkipVerify: true,
PeerCertSANs: []tls.SAN{
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple GRPCRoute and BackendTLSPolicy with System CA",
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy_system.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
EntryPoints: []string{"web"},
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80-grpc",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80-grpc": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-80-grpc",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-80-grpc": {
ServerName: "whoami",
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
if test.expected == nil {
return
}
k8sObjects, gwObjects := readResources(t, test.paths)
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubeClient, gwClient)
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
<-eventCh
}
p := Provider{
EntryPoints: test.entryPoints,
client: client,
}
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3794,7 +4131,9 @@ func TestLoadGRPCRoutes_filterExtensionRef(t *testing.T) {
p.RegisterFilterFuncs(group, kind, filterFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -4715,7 +5054,9 @@ func TestLoadTCPRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -5976,6 +6317,266 @@ func TestLoadTLSRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple TLSRoute and BackendTLSPolicy with CA certificate",
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy.yml"},
entryPoints: map[string]Entrypoint{"tls": {
Address: ":9001",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:80",
},
{
Address: "10.10.0.2:80",
},
},
ServersTransport: "default-whoami-80",
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{
"default-whoami-80": {
TLS: &dynamic.TLSClientConfig{
ServerName: "whoami",
RootCAs: []types.FileOrContent{
"CA1",
"CA2",
"CA1-secret",
"CA2-secret",
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
{
desc: "Simple TLSRoute and BackendTLSPolicy with subjectAltNames",
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_san.yml"},
entryPoints: map[string]Entrypoint{"tls": {
Address: ":9001",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:80",
},
{
Address: "10.10.0.2:80",
},
},
ServersTransport: "default-whoami-80",
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{
"default-whoami-80": {
TLS: &dynamic.TLSClientConfig{
ServerName: "whoami",
InsecureSkipVerify: true,
PeerCertSANs: []tls.SAN{
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
{
desc: "Simple TLSRoute and BackendTLSPolicy with System CA",
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_system.yml"},
entryPoints: map[string]Entrypoint{"tls": {
Address: ":9001",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:80",
},
{
Address: "10.10.0.2:80",
},
},
ServersTransport: "default-whoami-80",
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{
"default-whoami-80": {
TLS: &dynamic.TLSClientConfig{
ServerName: "whoami",
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
}
for _, test := range testCases {
@@ -6009,7 +6610,9 @@ func TestLoadTLSRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -6999,7 +7602,9 @@ func TestLoadMixedRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -7297,7 +7902,9 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -8425,20 +9032,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8459,7 +9068,8 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
router, ok := conf.HTTP.Routers["httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3"]
require.True(t, ok)
@@ -8488,20 +9098,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8524,7 +9136,8 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
ExperimentalChannel: true,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
router, ok := conf.TCP.Routers["tcproute-default-tcp-app-1-gw-default-my-gateway-ep-tcp-0-e3b0c44298fc1c149afb"]
require.True(t, ok)
@@ -8560,20 +9173,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8595,7 +9210,8 @@ func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
fmt.Println(conf.TCP.Routers)
+136
View File
@@ -0,0 +1,136 @@
package gateway
import (
"context"
"reflect"
"github.com/rs/zerolog/log"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
// statusReport collects the status writes produced by a single rebuild so they
// can be flushed to the apiserver after the dynamic configuration has been published.
type statusReport struct {
gatewayClasses map[string]gatev1.GatewayClassStatus
gateways map[ktypes.NamespacedName]gatev1.GatewayStatus
httpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
grpcRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tcpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tlsRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
backendTLSPolicies map[ktypes.NamespacedName]gatev1.PolicyStatus
}
func newStatusReport() *statusReport {
return &statusReport{
gatewayClasses: map[string]gatev1.GatewayClassStatus{},
gateways: map[ktypes.NamespacedName]gatev1.GatewayStatus{},
httpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
grpcRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tcpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tlsRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
backendTLSPolicies: map[ktypes.NamespacedName]gatev1.PolicyStatus{},
}
}
// Flush sends every status write collected during the
// routing configuration build to the Kubernetes API server.
func (r *statusReport) Flush(ctx context.Context, client *clientWrapper) {
logger := log.Ctx(ctx)
for name, status := range r.gatewayClasses {
if err := client.UpdateGatewayClassStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway_class", name).Msg("Unable to update GatewayClass status")
}
}
for name, status := range r.gateways {
if err := client.UpdateGatewayStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway", name.Name).Str("namespace", name.Namespace).Msg("Unable to update Gateway status")
}
}
for name, routeStatus := range r.httpRoutes {
status := gatev1.HTTPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateHTTPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("http_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update HTTPRoute status")
}
}
for name, routeStatus := range r.grpcRoutes {
status := gatev1.GRPCRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateGRPCRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("grpc_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update GRPCRoute status")
}
}
for name, routeStatus := range r.tcpRoutes {
status := gatev1alpha2.TCPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTCPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tcp_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TCPRoute status")
}
}
for name, routeStatus := range r.tlsRoutes {
status := gatev1.TLSRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTLSRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tls_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TLSRoute status")
}
}
for name, policyStatus := range r.backendTLSPolicies {
if err := client.UpdateBackendTLSPolicyStatus(ctx, name, policyStatus); err != nil {
logger.Warn().Err(err).Str("backend_tls_policy", name.Name).Str("namespace", name.Namespace).Msg("Unable to update BackendTLSPolicy status")
}
}
}
func (r *statusReport) RecordGatewayClassStatus(gatewayClassName string, status gatev1.GatewayClassStatus) {
r.gatewayClasses[gatewayClassName] = status
}
func (r *statusReport) RecordGatewayStatus(gateway ktypes.NamespacedName, status gatev1.GatewayStatus) {
r.gateways[gateway] = status
}
func (r *statusReport) RecordHTTPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.httpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.httpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordGRPCRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.grpcRoutes[route] = gatev1.RouteStatus{
Parents: append(r.grpcRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTCPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tcpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tcpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTLSRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tlsRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tlsRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordBackendTLSPolicyStatus(policy ktypes.NamespacedName, status gatev1.PolicyAncestorStatus) {
var ancestors []gatev1.PolicyAncestorStatus
// Keep existing ancestor statuses, except if it matches the status to merge.
for _, existing := range r.backendTLSPolicies[policy].Ancestors {
if reflect.DeepEqual(existing.AncestorRef, status.AncestorRef) {
continue
}
ancestors = append(ancestors, existing)
}
r.backendTLSPolicies[policy] = gatev1.PolicyStatus{
Ancestors: append(ancestors, status), // Add the new status to the existing ancestors statuses.
}
}
@@ -0,0 +1,152 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func TestStatusReport_RecordGatewayClassStatus(t *testing.T) {
report := newStatusReport()
accepted := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassConditionStatusAccepted)}},
}
report.RecordGatewayClassStatus("traefik", accepted)
assert.Equal(t, accepted, report.gatewayClasses["traefik"])
// A later record for the same GatewayClass overwrites the previous one.
unsupported := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassReasonUnsupportedVersion)}},
}
report.RecordGatewayClassStatus("traefik", unsupported)
assert.Equal(t, unsupported, report.gatewayClasses["traefik"])
}
func TestStatusReport_RecordGatewayStatus(t *testing.T) {
report := newStatusReport()
gateway := ktypes.NamespacedName{Namespace: "default", Name: "my-gateway"}
accepted := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionAccepted)}},
}
report.RecordGatewayStatus(gateway, accepted)
assert.Equal(t, accepted, report.gateways[gateway])
// A later record for the same Gateway overwrites the previous one.
programmed := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionProgrammed)}},
}
report.RecordGatewayStatus(gateway, programmed)
assert.Equal(t, programmed, report.gateways[gateway])
}
func TestStatusReport_RecordHTTPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordHTTPRouteStatus(route, gatewayParent)
report.RecordHTTPRouteStatus(route, otherParent)
// Each parentRef accumulates as a distinct parent status.
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.httpRoutes[route].Parents)
}
func TestStatusReport_RecordGRPCRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordGRPCRouteStatus(route, gatewayParent)
report.RecordGRPCRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.grpcRoutes[route].Parents)
}
func TestStatusReport_RecordTCPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTCPRouteStatus(route, gatewayParent)
report.RecordTCPRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tcpRoutes[route].Parents)
}
func TestStatusReport_RecordTLSRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTLSRouteStatus(route, gatewayParent)
report.RecordTLSRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tlsRoutes[route].Parents)
}
func TestStatusReport_RecordBackendTLSPolicyStatus(t *testing.T) {
gatewayAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: controllerName,
}
otherAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "other-gateway"},
ControllerName: controllerName,
}
testCases := []struct {
desc string
records []gatev1.PolicyAncestorStatus
expected []gatev1.PolicyAncestorStatus
}{
{
desc: "distinct ancestor refs accumulate",
records: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
expected: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
},
{
desc: "same ancestor ref is replaced, not duplicated",
records: []gatev1.PolicyAncestorStatus{
gatewayAncestor,
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
expected: []gatev1.PolicyAncestorStatus{
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
report := newStatusReport()
policy := ktypes.NamespacedName{Namespace: "default", Name: "my-policy"}
for _, record := range test.records {
report.RecordBackendTLSPolicyStatus(policy, record)
}
assert.Equal(t, test.expected, report.backendTLSPolicies[policy].Ancestors)
})
}
}
+19 -20
View File
@@ -19,7 +19,7 @@ import (
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTCPRoutes()
if err != nil {
@@ -28,19 +28,13 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tcp_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1alpha2.RouteParentStatus{
parentStatus := gatev1alpha2.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -77,18 +71,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
routeStatus := gatev1alpha2.TCPRouteStatus{
RouteStatus: gatev1alpha2.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TCPRoute status")
statusReport.RecordTCPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
@@ -221,6 +204,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
@@ -347,4 +341,9 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) {
to.TCP.Services = map[string]*dynamic.TCPService{}
}
maps.Copy(to.TCP.Services, from.TCP.Services)
if to.TCP.ServersTransports == nil {
to.TCP.ServersTransports = map[string]*dynamic.TCPServersTransport{}
}
maps.Copy(to.TCP.ServersTransports, from.TCP.ServersTransports)
}
+254 -30
View File
@@ -5,12 +5,15 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
@@ -18,7 +21,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTLSRoutes()
if err != nil {
@@ -27,18 +30,13 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tls_route", route.Name).
Str("namespace", route.Namespace).Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -73,14 +71,14 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
}
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames)
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
}
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
statusReport.RecordTLSRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
// When there is at least one TLS listener, we add a default deny-all route to avoid accepting traffic for undefined hosts.
@@ -96,21 +94,10 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
}
}
routeStatus := gatev1.TLSRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TLSRoute status")
}
}
}
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
@@ -171,7 +158,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadTLSWRRService(conf, routerName, routeRule.BackendRefs, route)
router.Service, serviceCondition = p.loadTLSWRRService(listener, conf, routerName, routeRule.BackendRefs, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -183,7 +170,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
// loadTLSWRRService is generating a WRR service, even when there is only one target.
func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) {
func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.TCP.Services[name]; ok {
return name, nil
@@ -192,7 +179,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
var wrr dynamic.TCPWeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTLSService(route, backendRef)
svcName, svc, errCondition := p.loadTLSService(listener, conf, route, backendRef, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
@@ -226,7 +213,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
return name, condition
}
func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, statusReport *statusReport) (string, *dynamic.TCPService, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -237,6 +224,17 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
@@ -283,18 +281,23 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, errCondition := p.loadTLSServers(namespace, route, backendRef)
lb, st, errCondition := p.loadTLSServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.TCP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
}
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.TCPServersLoadBalancer, *dynamic.TCPServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
if err != nil {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -304,8 +307,116 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
}
}
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
}
}
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.TCPServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadTCPServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -323,7 +434,120 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
Address: net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port))),
})
}
return lb, nil
return lb, serversTransport, nil
}
func (p *Provider) loadTCPServersTransport(namespace string, policy *gatev1.BackendTLSPolicy) (*dynamic.TCPServersTransport, metav1.Condition) {
st := &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: string(policy.Spec.Validation.Hostname),
},
}
if len(policy.Spec.Validation.SubjectAltNames) > 0 {
// Per the Gateway API specification the Hostname should only be used for authentication
// and not for certificate validation. Thus, if SubjectAltNames is specified, we ignore
// the Hostname validation by setting the InsecureSkipVerify option to true.
st.TLS.InsecureSkipVerify = true
for _, san := range policy.Spec.Validation.SubjectAltNames {
switch san.Type {
case gatev1.URISubjectAltNameType:
st.TLS.PeerCertSANs = append(st.TLS.PeerCertSANs, tls.SAN{
Type: tls.SANURIType,
Value: string(san.URI),
})
case gatev1.HostnameSubjectAltNameType:
st.TLS.PeerCertSANs = append(st.TLS.PeerCertSANs, tls.SAN{
Type: tls.SANDNSNameType,
Value: string(san.Hostname),
})
default:
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: fmt.Sprintf("Unsupported SubjectAltName type %q; only URI and Hostname types are supported", san.Type),
}
}
}
}
if policy.Spec.Validation.WellKnownCACertificates != nil {
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: "Only ConfigMaps and Secrets are supported",
}
}
var caCRT string
switch caCertRef.Kind {
case kindConfigMap:
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = configmap.Data["ca.crt"]
case kindSecret:
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting secret %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = string(secret.Data["ca.crt"])
}
if caCRT == "" {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)),
}
}
st.TLS.RootCAs = append(st.TLS.RootCAs, types.FileOrContent(caCRT))
}
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
}
func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
@@ -415,11 +415,13 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
logger.Error().
Err(err).
Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)).
Msg("Cannot resolve auth secret, skipping auth middleware")
} else {
loc.BasicAuth = basic
loc.DigestAuth = digest
Msg("Cannot resolve auth secret, skipping ingress")
// Skipping the ingress entirely when auth secret resolution fails,
// to match ingress-nginx behavior.
continue
}
loc.BasicAuth = basic
loc.DigestAuth = digest
}
// Pre-resolve custom headers ConfigMap.
@@ -158,7 +158,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
}
for _, ns := range c.watchedNamespaces {
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
if err != nil {
@@ -167,7 +167,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
c.factoriesIngress[ns] = factoryIngress
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -178,14 +178,14 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
}
c.factoriesKube[ns] = factoryKube
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
c.factoriesSecret[ns] = factorySecret
factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryConfigMap.Core().V1().ConfigMaps().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -226,7 +226,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
}
}
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
if !c.ignoreIngressClasses {
_, err = c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
@@ -0,0 +1,25 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-basicauth-secret-missing
namespace: default
annotations:
nginx.ingress.kubernetes.io/auth-type: "basic"
nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
nginx.ingress.kubernetes.io/auth-secret: "default/missing-basic-auth"
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
ingressClassName: nginx
rules:
- host: whoami.localhost
http:
paths:
- path: /basicauth
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
@@ -0,0 +1,69 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-endpoint-conditions
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: whoami.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: whoami
namespace: default
spec:
clusterIP: 10.10.10.1
ports:
- name: web
protocol: TCP
port: 80
targetPort: web
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami-abc
namespace: default
labels:
kubernetes.io/service-name: whoami
addressType: IPv4
ports:
- name: web
port: 80
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
serving: true
terminating: false
- addresses:
- 10.10.0.2
conditions:
ready: false
serving: true
terminating: true
- addresses:
- 10.10.0.3
conditions:
ready: false
serving: false
terminating: true
@@ -1329,6 +1329,37 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Basic Auth with missing secret — ingress is skipped entirely",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-basicauth-secret-missing.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"unavailable-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Forward Auth",
paths: []string{
@@ -15656,6 +15687,102 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Ingress with endpoint conditions",
paths: []string{
"ingressclasses.yml",
"ingresses/ingress-with-endpoint-conditions.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-endpoint-conditions-rule-0-path-0": {
EntryPoints: []string{"http"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Service: "default-ingress-with-endpoint-conditions-whoami-80",
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-retry"},
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-endpoint-conditions",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls": {
EntryPoints: []string{"https"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Service: "default-ingress-with-endpoint-conditions-whoami-80",
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-endpoint-conditions",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-endpoint-conditions-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
},
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry": {
Retry: &dynamic.Retry{Attempts: 3},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-endpoint-conditions-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80", Fenced: true},
},
Strategy: dynamic.BalancerStrategyWRR,
PassHostHeader: ptr.To(true),
ServersTransport: "default-ingress-with-endpoint-conditions",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"unavailable-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-endpoint-conditions": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Auth TLS secret missing — ingress is skipped entirely",
paths: []string{
@@ -294,7 +294,8 @@ func buildService(backend *backend, serversTransportName string) *dynamic.Servic
svc := &dynamic.Service{LoadBalancer: lb}
for _, ep := range backend.Endpoints {
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
URL: fmt.Sprintf("http://%s", ep.Address),
URL: fmt.Sprintf("http://%s", ep.Address),
Fenced: ep.Fenced,
})
}
@@ -320,7 +321,8 @@ func buildServiceWithLocConfig(backend *backend, serversTransportName string, lo
svc := &dynamic.Service{LoadBalancer: lb}
for _, ep := range backend.Endpoints {
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
Fenced: ep.Fenced,
})
}
+4 -4
View File
@@ -158,7 +158,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
for _, ns := range namespaces {
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector))
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector), kinformers.WithTransform(k8s.StripManagedFields))
_, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
if err != nil {
@@ -167,7 +167,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
c.factoriesIngress[ns] = factoryIngress
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -178,7 +178,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
c.factoriesKube[ns] = factoryKube
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
@@ -213,7 +213,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
}
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
_, err := c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
if err != nil {
@@ -0,0 +1,75 @@
---
kind: Node
apiVersion: v1
metadata:
name: node1
status:
addresses:
- type: InternalIP
address: 10.0.0.1
- type: ExternalIP
address: 1.2.3.4
---
kind: Node
apiVersion: v1
metadata:
name: node2
status:
addresses:
- type: InternalIP
address: 10.0.0.2
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: foo
namespace: default
spec:
rules:
- host: "*.foo.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: default
spec:
ports:
- port: 80
clusterIP: 10.0.0.1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: service1-abc
namespace: default
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
+37 -4
View File
@@ -48,6 +48,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ReportNodeInternalIPs bool `description:"Report node internal IPs in Ingress status." json:"reportNodeInternalIPs,omitempty" toml:"reportNodeInternalIPs,omitempty" yaml:"reportNodeInternalIPs,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
@@ -72,6 +73,14 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
if p.ReportNodeInternalIPs && p.IngressEndpoint != nil {
return errors.New("reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
}
if p.ReportNodeInternalIPs && p.DisableClusterScopeResources {
return errors.New("reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
}
return nil
}
@@ -246,6 +255,26 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
ingresses := client.GetIngresses()
var nodeIngressStatus []netv1.IngressLoadBalancerIngress
if p.ReportNodeInternalIPs {
nodes, _, err := client.GetNodes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Error while getting nodes for ingress status")
} else {
for _, node := range nodes {
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeInternalIP {
nodeIngressStatus = append(nodeIngressStatus, netv1.IngressLoadBalancerIngress{IP: address.Address})
}
}
}
if len(nodeIngressStatus) == 0 {
log.Ctx(ctx).Error().Msg("No nodes with internal IP address found for ingress status")
}
}
}
certConfigs := make(map[string]*tls.CertAndStores)
for _, ingress := range ingresses {
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
@@ -255,7 +284,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
if err := p.updateIngressStatus(ingress, client); err != nil {
if err := p.updateIngressStatus(ingress, client, nodeIngressStatus); err != nil {
logger.Error().Err(err).Msg("Error while updating ingress status")
}
@@ -442,7 +471,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
return conf
}
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) error {
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client, nodeIngressStatus []netv1.IngressLoadBalancerIngress) error {
if len(nodeIngressStatus) > 0 {
return k8sClient.UpdateIngressStatus(ing, nodeIngressStatus)
}
// Only process if an EndpointIngress has been configured.
if p.IngressEndpoint == nil {
return nil
@@ -623,12 +656,12 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
nodes, _, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr
}
if !nodesExists || len(nodes) == 0 {
if len(nodes) == 0 {
return nil, fmt.Errorf("nodes not found in namespace %s", namespace)
}
@@ -3094,6 +3094,77 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
return k8sObjects
}
func TestProviderInit(t *testing.T) {
p := Provider{
ReportNodeInternalIPs: true,
IngressEndpoint: &EndpointIngress{IP: "1.2.3.4"},
}
assert.EqualError(t, p.Init(), "reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
p2 := Provider{
ReportNodeInternalIPs: true,
DisableClusterScopeResources: true,
}
assert.EqualError(t, p2.Init(), "reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
p3 := Provider{ReportNodeInternalIPs: true}
assert.NoError(t, p3.Init())
}
func TestReportNodeInternalIPs(t *testing.T) {
testCases := []struct {
desc string
client clientMock
expectedEmpty bool
}{
{
desc: "nodes present",
client: newClientMock(generateTestFilename("Node Internal IP")),
},
{
desc: "GetNodes API error",
client: clientMock{apiNodesError: errors.New("api nodes error")},
expectedEmpty: true,
},
{
desc: "no nodes found",
client: clientMock{nodes: []*corev1.Node{}},
expectedEmpty: true,
},
{
desc: "nodes exist but none have an internal IP",
client: clientMock{
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "1.2.3.4"},
},
},
},
},
},
expectedEmpty: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{ReportNodeInternalIPs: true}
conf := p.loadConfigurationFromIngresses(t.Context(), test.client)
if test.expectedEmpty {
assert.Empty(t, conf.HTTP.Routers)
assert.Empty(t, conf.HTTP.Services)
} else {
assert.NotEmpty(t, conf.HTTP.Routers)
assert.NotEmpty(t, conf.HTTP.Services)
}
})
}
}
func TestStrictPrefixMatchingRule(t *testing.T) {
tests := []struct {
path string
+16
View File
@@ -0,0 +1,16 @@
package k8s
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// StripManagedFields drops metadata.managedFields as objects enter the informer cache.
// Traefik never reads them, and they inflate the cache footprint and the cost of copying
// and comparing cached objects, which matters under heavy resource churn.
func StripManagedFields(obj any) (any, error) {
object, ok := obj.(metav1.Object)
if !ok {
return obj, nil
}
object.SetManagedFields(nil)
return obj, nil
}
@@ -0,0 +1,43 @@
package k8s
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestStripManagedFields(t *testing.T) {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "whoami",
ManagedFields: []metav1.ManagedFieldsEntry{
{
Manager: "kubectl",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "v1",
Time: &metav1.Time{Time: time.Now()},
},
},
},
}
got, err := StripManagedFields(service)
require.NoError(t, err)
assert.Same(t, service, got)
assert.Nil(t, service.ManagedFields)
assert.Equal(t, "whoami", service.Name)
}
func TestStripManagedFieldsIgnoresNonKubernetesObjects(t *testing.T) {
obj := struct{ Name string }{Name: "whoami"}
got, err := StripManagedFields(obj)
require.NoError(t, err)
assert.Equal(t, obj, got)
}
+2 -2
View File
@@ -122,13 +122,13 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
for _, ns := range namespaces {
factory := knativenetworkinginformers.NewSharedInformerFactoryWithOptions(c.csKnativeNetworking, resyncPeriod, knativenetworkinginformers.WithNamespace(ns), knativenetworkinginformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = c.labelSelector
}))
}), knativenetworkinginformers.WithTransform(k8s.StripManagedFields))
_, err := factory.Networking().V1alpha1().Ingresses().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
+101 -50
View File
@@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
Str(logs.RouterName, routerName).
Strs(logs.EntryPointName, defaultEntryPoints).
Msg("No entryPoint defined for this router, using the default one(s) instead")
router.EntryPoints = defaultEntryPoints
router.EntryPoints = slices.Clone(defaultEntryPoints)
}
// The `ruleSyntax` option is deprecated.
@@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
log.Debug().
Str(logs.RouterName, routerName).
Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints)
router.EntryPoints = defaultEntryPoints
router.EntryPoints = slices.Clone(defaultEntryPoints)
}
conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router
}
@@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
return conf
}
func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
return cfg
// resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per
// entryPoint basis.
//
// TLS options conflicts (i.e. the same host served with different TLS options) can
// only be detected and arbitrated within a single TLS listener, that is to say within
// a single entryPoint. To honor that, routers are grouped per entryPoint and the
// conflict detection is run independently for each entryPoint.
//
// A router keeps its original name, and its resolved TLS options, for the entryPoints
// on which it does not conflict. For each entryPoint on which it conflicts, that
// entryPoint is removed from the router and a dedicated copy is emitted, with its
// TLSOptions reset to the default one, named following the "ep-conflicted-name@provider" pattern.
func resolveHTTPTLSOptions(routers map[string]*dynamic.Router) map[string]*dynamic.Router {
if len(routers) == 0 {
return routers
}
rts := make(map[string]*dynamic.Router)
newRouters := make(map[string]*dynamic.Router)
// Keyed by domain, then by options reference.
// The actual source of truth for what TLS options will actually be used for the connection.
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
// options that occur for a given host name, so that later on we can set relevant
// errors and logging for all the routers concerned (i.e. wrongly configured).
tlsOptionsForHostSNI := map[string]map[string][]string{}
for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
if routerHTTPConfig.TLS == nil {
// Split every router per entryPoint.
// Routers always have at least one entryPoint at this stage, as they are
// defaulted in mergeConfiguration before applyModel and this resolution run.
routersByEntryPoint := map[string]map[string]*dynamic.Router{}
for name, router := range routers {
if router.TLS == nil {
newRouters[name] = router
continue
}
ctxRouter := provider.AddInContext(context.Background(), routerHTTPName)
logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger()
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName {
router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.TLS.Options)
}
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
for _, ep := range router.EntryPoints {
if routersByEntryPoint[ep] == nil {
routersByEntryPoint[ep] = map[string]*dynamic.Router{}
}
routersByEntryPoint[ep][name] = router
}
}
// Resolve the TLS options independently for each entryPoint.
conflictingRouters := make(map[string][]string, len(routersByEntryPoint))
for ep, epRouters := range routersByEntryPoint {
conflictingRouters[ep] = findConflictingRouters(ep, epRouters)
}
for name, router := range routers {
router.EntryPoints = slices.DeleteFunc(router.EntryPoints, func(ep string) bool {
deleted := slices.Contains(conflictingRouters[ep], name)
if deleted {
rt := router.DeepCopy()
rt.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
rt.EntryPoints = []string{ep}
// The new name is not collision free but has very small possibility to collide.
// TODO: rework this naming whenever we'll introduce a resource reference mechanism not based on a string.
newRouters[ep+"-conflicted-"+name] = rt
}
return deleted
})
if len(router.EntryPoints) > 0 {
newRouters[name] = router
}
}
return newRouters
}
// findConflictingRouters returns the names of the routers, among the given
// single-entryPoint routers, that serve a host (SNI) also served by another router
// with a different resolved TLS option. Such routers are arbitrated by falling back
// to the default TLS options.
func findConflictingRouters(ep string, routers map[string]*dynamic.Router) []string {
var conflicting []string
// For each host (SNI, already lower-cased by the domain parsing), the routers
// serving it grouped by their resolved TLS option. A host with more than one
// group is served with conflicting TLS options.
routersByHostAndOption := map[string]map[string][]string{}
for name, router := range routers {
if router.TLS == nil {
continue
}
domains, err := httpmuxer.ParseDomains(router.Rule)
if err != nil {
logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
continue
}
if len(domains) == 0 {
rts[routerHTTPName].TLS.ResolvedOptions = "default"
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
// The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI,
// so if it is not the default one, it is a conflict.
if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName {
conflicting = append(conflicting, name)
continue
}
for _, domain := range domains {
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string][]string)
if routersByHostAndOption[domain] == nil {
routersByHostAndOption[domain] = map[string][]string{}
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
option := router.TLS.ResolvedOptions
routersByHostAndOption[domain][option] = append(routersByHostAndOption[domain][option], name)
}
}
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
for optionsName, v := range tlsConfigs {
log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
for _, s := range v {
rts[s].TLS.ResolvedOptions = optionsName
}
}
for domain, routersByOption := range routersByHostAndOption {
if len(routersByOption) == 1 {
continue
}
// multiple tlsConfigs
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
for _, s := range v {
rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
routers = append(routers, s)
}
var routersInConflict []string
for _, names := range routersByOption {
conflicting = append(conflicting, names...)
routersInConflict = append(routersInConflict, names...)
}
log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
log.Error().Msgf("On EntryPoint %q, Host %q is served by multiple routers with different TLS options, default TLSOptions will be applied for the following routers: %v", ep, domain, routersInConflict)
}
cfg.HTTP.Routers = rts
return cfg
return conflicting
}
func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
+111
View File
@@ -5,6 +5,7 @@ import (
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/tls"
@@ -1230,3 +1231,113 @@ func Test_applyModel(t *testing.T) {
})
}
}
func Test_resolveHTTPTLSOptions(t *testing.T) {
testCases := []struct {
desc string
routers map[string]*dynamic.Router
expected map[string]string // router name -> ResolvedOptions
unexpectedRouters []string
}{
{
desc: "same host, different options, different entryPoints: no conflict",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsB@file",
},
},
{
desc: "same host, different options, same entryPoint: conflict falls back to default",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
"ep-a-conflicted-router-b@file": "default",
},
unexpectedRouters: []string{"router-a@file", "router-b@file"},
},
{
desc: "same host, same options, same entryPoint: keeps the configured options",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsA@file",
},
},
{
desc: "router spanning two entryPoints, conflict on one only: router is duplicated",
routers: map[string]*dynamic.Router{
"shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}},
"other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}},
},
expected: map[string]string{
"ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a
"shared@file": "optsX@file", // alone on ep-b
"ep-a-conflicted-other@file": "default",
},
unexpectedRouters: []string{"other@file"},
},
{
desc: "no domain in rule, non-default options: forced to default and renamed",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
},
unexpectedRouters: []string{"router-a@file"},
},
{
desc: "no domain in rule, implicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
{
desc: "no domain in rule, explicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{
Options: "default",
}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := resolveHTTPTLSOptions(test.routers)
for name, want := range test.expected {
rt, ok := got[name]
require.True(t, ok, "router %q is missing", name)
require.NotNil(t, rt.TLS, "router %q has no TLS config", name)
assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints)
}
for _, name := range test.unexpectedRouters {
_, ok := got[name]
require.False(t, ok, "router %q is present", name)
}
})
}
}
+3 -1
View File
@@ -176,7 +176,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
conf = applyModel(conf)
conf = resolveHTTPTLSOptions(conf)
if conf.HTTP != nil {
conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers)
}
for _, listener := range c.configurationListeners {
listener(conf)
+1 -2
View File
@@ -173,8 +173,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.")
routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
routerHTTPConfig.AddError(errors.New("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"), false)
}
// Even though the error is seemingly ignored (aside from logging it),
+1
View File
@@ -643,6 +643,7 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
configuration.ForwardedHeaders.TrustedIPs,
configuration.ForwardedHeaders.Connection,
configuration.ForwardedHeaders.NotAppendXForwardedFor,
configuration.ForwardedHeaders.AddXForwardedSchemeHeaders,
next)
if err != nil {
return nil, err
+13 -2
View File
@@ -171,7 +171,7 @@ 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 != "" || len(cfg.CipherSuites) > 0 || cfg.MaxVersion != "" || cfg.MinVersion != "" {
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" || len(cfg.PeerCertSANs) > 0 || 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")
}
@@ -223,9 +223,20 @@ func (t *TransportManager) createTLSConfig(cfg *dynamic.ServersTransport) (*tls.
MaxVersion: maxVersion,
}
peerCertSANs := make([]traefiktls.SAN, len(cfg.PeerCertSANs))
copy(peerCertSANs, cfg.PeerCertSANs)
if cfg.PeerCertURI != "" {
log.Warn().Msg("PeerCertURI option is deprecated, please use PeerCertSANs instead")
peerCertSANs = append(peerCertSANs, traefiktls.SAN{
Type: traefiktls.SANURIType,
Value: cfg.PeerCertURI,
})
}
if len(peerCertSANs) > 0 {
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return traefiktls.VerifyPeerCertificate(cfg.PeerCertURI, config, rawCerts)
return traefiktls.VerifyPeerCertificate(peerCertSANs, config.RootCAs, rawCerts)
}
}
}
+168 -89
View File
@@ -26,11 +26,11 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
// LocalhostCert is a PEM-encoded TLS cert
// localhostCert is a PEM-encoded TLS cert
// for host example.com, www.example.com
// expiring at Jan 29 16:00:00 2084 GMT.
// go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host example.com,www.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIICDDCCAXWgAwIBAgIQH20JmcOlcRWHNuf62SYwszANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
@@ -44,8 +44,8 @@ we7mX8lv763u0XuCWPxbHszhclI6FFjoQef0Z1NYLRm8ZRq58QqWDFZ3E6wdDK+B
+OWvkW+hRavo6R9LzIZPfbv8yBo4M9PK/DXw8hLqH7VkkI+Gh793iH7Ugd4A7wvT
-----END CERTIFICATE-----`)
// LocalhostKey is the private key for localhostCert.
var LocalhostKey = []byte(`-----BEGIN PRIVATE KEY-----
// localhostKey is the private key for localhostCert.
var localhostKey = []byte(`-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALSog3LcXiirq+IO
eWkMMTknTyJJEaCCDoTKUkoEpl+mEQba5+ArvwO5+Xf7tvQuURjYB5kfC+Ic4OoL
1rqKGPVlhCTT92MCH4546EURa771P9U/yBUVqsWpPwM6nL6GsbtgmK9QjPBvh+Yl
@@ -130,7 +130,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
}
}
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
@@ -141,7 +141,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
},
}
@@ -164,7 +164,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
dynamicConf = map[string]*dynamic.ServersTransport{
"test": {
ServerName: "www.example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
},
}
@@ -188,7 +188,7 @@ func TestValidCipherSuites(t *testing.T) {
rw.WriteHeader(http.StatusOK)
}))
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
srv.TLS = &tls.Config{
@@ -204,7 +204,7 @@ func TestValidCipherSuites(t *testing.T) {
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
},
}
@@ -225,7 +225,7 @@ func TestValidTLSVersions(t *testing.T) {
rw.WriteHeader(http.StatusOK)
}))
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
srv.TLS = &tls.Config{
@@ -243,7 +243,7 @@ func TestValidTLSVersions(t *testing.T) {
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
MaxVersion: "VersionTLS12",
MinVersion: "VersionTLS11",
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
@@ -270,7 +270,7 @@ func TestInvalidTLSConfig(t *testing.T) {
desc: "invalid CipherSuite name",
transport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA385"},
},
},
@@ -278,7 +278,7 @@ func TestInvalidTLSConfig(t *testing.T) {
desc: "invalid MinVersion",
transport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
MinVersion: "VersionTLS09",
},
},
@@ -286,7 +286,7 @@ func TestInvalidTLSConfig(t *testing.T) {
desc: "invalid MaxVersion",
transport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
MaxVersion: "VersionTLS16",
},
},
@@ -294,7 +294,7 @@ func TestInvalidTLSConfig(t *testing.T) {
desc: "MinVersion above MaxVersion",
transport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
MinVersion: "VersionTLS13",
MaxVersion: "VersionTLS12",
},
@@ -325,7 +325,7 @@ func TestNoCipherSuitesUsesDefaults(t *testing.T) {
desc: "no cipher config with ServerName and RootCAs",
transport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
},
serverCipher: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
@@ -338,7 +338,7 @@ func TestNoCipherSuitesUsesDefaults(t *testing.T) {
},
}
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
for _, test := range testCases {
@@ -375,7 +375,7 @@ func TestMTLS(t *testing.T) {
rw.WriteHeader(http.StatusOK)
}))
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
clientPool := x509.NewCertPool()
@@ -397,7 +397,7 @@ func TestMTLS(t *testing.T) {
"test": {
ServerName: "example.com",
// For TLS
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
// For mTLS
Certificates: traefiktls.Certificates{
@@ -629,6 +629,155 @@ func TestDisableHTTP2(t *testing.T) {
}
}
func TestKerberosRoundTripper(t *testing.T) {
testCases := []struct {
desc string
originalRoundTripperHeaders map[string][]string
expectedStatusCode []int
expectedDedicatedCount int
expectedOriginalCount int
}{
{
desc: "without special header",
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
expectedOriginalCount: 3,
},
{
desc: "with Negotiate (Kerberos)",
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"Negotiate"}},
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
expectedOriginalCount: 1,
expectedDedicatedCount: 2,
},
{
desc: "with NTLM",
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"NTLM"}},
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
expectedOriginalCount: 1,
expectedDedicatedCount: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
origCount := 0
dedicatedCount := 0
rt := kerberosRoundTripper{
new: func() http.RoundTripper {
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
dedicatedCount++
return &http.Response{
StatusCode: http.StatusOK,
}, nil
})
},
OriginalRoundTripper: roundTripperFn(func(req *http.Request) (*http.Response, error) {
origCount++
return &http.Response{
StatusCode: http.StatusUnauthorized,
Header: test.originalRoundTripperHeaders,
}, nil
}),
}
ctx := AddTransportOnContext(t.Context())
for _, expected := range test.expectedStatusCode {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1", http.NoBody)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, expected, resp.StatusCode)
}
require.Equal(t, test.expectedOriginalCount, origCount)
require.Equal(t, test.expectedDedicatedCount, dedicatedCount)
})
}
}
func TestPeerCertSANs(t *testing.T) {
testCases := []struct {
desc string
serversTransport *dynamic.ServersTransport
wantError bool
}{
{
desc: "matches cert SAN",
serversTransport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANDNSNameType,
Value: "www.example.com",
},
},
},
wantError: false,
},
{
desc: "does not match cert SAN",
serversTransport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANDNSNameType,
Value: "wrong.example.com",
},
},
},
wantError: true,
},
{
desc: "does not match deprecated PeerCertURI",
serversTransport: &dynamic.ServersTransport{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertURI: "foo://bar",
},
wantError: true,
},
}
// LocalhostCert is valid for "example.com" — this is the cert the test server serves.
// We set ServerName to "other.example.com" to prove SNI and cert auth are decoupled.
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}))
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
srv.StartTLS()
t.Cleanup(srv.Close)
transportManager := NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{
"test": test.serversTransport,
})
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
_, err = client.Get(srv.URL)
if test.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs.
type fakeSpiffePKI struct {
caPrivateKey *rsa.PrivateKey
@@ -744,73 +893,3 @@ type roundTripperFn func(req *http.Request) (*http.Response, error)
func (r roundTripperFn) RoundTrip(request *http.Request) (*http.Response, error) {
return r(request)
}
func TestKerberosRoundTripper(t *testing.T) {
testCases := []struct {
desc string
originalRoundTripperHeaders map[string][]string
expectedStatusCode []int
expectedDedicatedCount int
expectedOriginalCount int
}{
{
desc: "without special header",
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
expectedOriginalCount: 3,
},
{
desc: "with Negotiate (Kerberos)",
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"Negotiate"}},
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
expectedOriginalCount: 1,
expectedDedicatedCount: 2,
},
{
desc: "with NTLM",
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"NTLM"}},
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
expectedOriginalCount: 1,
expectedDedicatedCount: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
origCount := 0
dedicatedCount := 0
rt := kerberosRoundTripper{
new: func() http.RoundTripper {
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
dedicatedCount++
return &http.Response{
StatusCode: http.StatusOK,
}, nil
})
},
OriginalRoundTripper: roundTripperFn(func(req *http.Request) (*http.Response, error) {
origCount++
return &http.Response{
StatusCode: http.StatusUnauthorized,
Header: test.originalRoundTripperHeaders,
}, nil
}),
}
ctx := AddTransportOnContext(t.Context())
for _, expected := range test.expectedStatusCode {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1", http.NoBody)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, expected, resp.StatusCode)
}
require.Equal(t, test.expectedOriginalCount, origCount)
require.Equal(t, test.expectedDedicatedCount, dedicatedCount)
})
}
}
+13 -2
View File
@@ -175,7 +175,7 @@ func (d *DialerManager) Build(config *dynamic.TCPServersLoadBalancer, isTLS bool
tlsConfig = tlsconfig.MTLSClientConfig(d.spiffeX509Source, d.spiffeX509Source, authorizer)
}
if st.TLS.InsecureSkipVerify || len(st.TLS.RootCAs) > 0 || len(st.TLS.ServerName) > 0 || len(st.TLS.Certificates) > 0 || st.TLS.PeerCertURI != "" {
if st.TLS.InsecureSkipVerify || len(st.TLS.RootCAs) > 0 || len(st.TLS.ServerName) > 0 || len(st.TLS.Certificates) > 0 || st.TLS.PeerCertURI != "" || len(st.TLS.PeerCertSANs) > 0 {
if tlsConfig != nil {
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
}
@@ -187,9 +187,20 @@ func (d *DialerManager) Build(config *dynamic.TCPServersLoadBalancer, isTLS bool
Certificates: st.TLS.Certificates.GetCertificates(),
}
peerCertSANs := make([]traefiktls.SAN, len(st.TLS.PeerCertSANs))
copy(peerCertSANs, st.TLS.PeerCertSANs)
if st.TLS.PeerCertURI != "" {
log.Warn().Msg("PeerCertURI option is deprecated, please use PeerCertSANs instead")
peerCertSANs = append(peerCertSANs, traefiktls.SAN{
Type: traefiktls.SANURIType,
Value: st.TLS.PeerCertURI,
})
}
if len(peerCertSANs) > 0 {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return traefiktls.VerifyPeerCertificate(st.TLS.PeerCertURI, tlsConfig, rawCerts)
return traefiktls.VerifyPeerCertificate(peerCertSANs, tlsConfig.RootCAs, rawCerts)
}
}
}
+96 -12
View File
@@ -11,6 +11,8 @@ import (
"io"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
@@ -27,11 +29,11 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
// LocalhostCert is a PEM-encoded TLS cert
// localhostCert is a PEM-encoded TLS cert
// for host example.com, www.example.com
// expiring at Jan 29 16:00:00 2084 GMT.
// go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host example.com,www.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIICDDCCAXWgAwIBAgIQH20JmcOlcRWHNuf62SYwszANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
@@ -45,8 +47,8 @@ we7mX8lv763u0XuCWPxbHszhclI6FFjoQef0Z1NYLRm8ZRq58QqWDFZ3E6wdDK+B
+OWvkW+hRavo6R9LzIZPfbv8yBo4M9PK/DXw8hLqH7VkkI+Gh793iH7Ugd4A7wvT
-----END CERTIFICATE-----`)
// LocalhostKey is the private key for localhostCert.
var LocalhostKey = []byte(`-----BEGIN PRIVATE KEY-----
// localhostKey is the private key for localhostCert.
var localhostKey = []byte(`-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALSog3LcXiirq+IO
eWkMMTknTyJJEaCCDoTKUkoEpl+mEQba5+ArvwO5+Xf7tvQuURjYB5kfC+Ic4OoL
1rqKGPVlhCTT92MCH4546EURa771P9U/yBUVqsWpPwM6nL6GsbtgmK9QjPBvh+Yl
@@ -178,7 +180,7 @@ func TestNoTLS(t *testing.T) {
}
func TestTLS(t *testing.T) {
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
backendListener, err := net.Listen("tcp", ":0")
@@ -199,7 +201,7 @@ func TestTLS(t *testing.T) {
"test": {
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
},
},
}
@@ -228,7 +230,7 @@ func TestTLS(t *testing.T) {
}
func TestTLSWithInsecureSkipVerify(t *testing.T) {
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
backendListener, err := net.Listen("tcp", ":0")
@@ -249,7 +251,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
"test": {
TLS: &dynamic.TLSClientConfig{
ServerName: "bad-domain.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
InsecureSkipVerify: true,
},
},
@@ -279,7 +281,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
}
func TestMTLS(t *testing.T) {
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
clientPool := x509.NewCertPool()
@@ -311,7 +313,7 @@ func TestMTLS(t *testing.T) {
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
// For TLS
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
// For mTLS
Certificates: traefiktls.Certificates{
@@ -595,7 +597,7 @@ func TestProxyProtocolWithTLS(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
backendListener, err := net.Listen("tcp", ":0")
@@ -651,7 +653,7 @@ func TestProxyProtocolWithTLS(t *testing.T) {
"test": {
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
InsecureSkipVerify: true,
},
ProxyProtocol: &dynamic.ProxyProtocol{
@@ -741,6 +743,88 @@ func TestProxyProtocolDisabled(t *testing.T) {
assert.Equal(t, "PONG", string(buf[:4]))
}
func TestPeerCertSANs(t *testing.T) {
testCases := []struct {
desc string
serversTransport *dynamic.TCPServersTransport
wantError bool
}{
{
desc: "matches cert SAN",
serversTransport: &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANDNSNameType,
Value: "www.example.com",
},
},
},
},
wantError: false,
},
{
desc: "does not match cert SAN",
serversTransport: &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertSANs: []traefiktls.SAN{
{
Type: traefiktls.SANDNSNameType,
Value: "wrong.example.com",
},
},
},
},
wantError: true,
},
{
desc: "does not match deprecated PeerCertURI",
serversTransport: &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
PeerCertURI: "foo://bar",
},
},
wantError: true,
},
}
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}))
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
srv.StartTLS()
t.Cleanup(srv.Close)
dialerManager := NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{
"test": test.serversTransport,
})
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", srv.Listener.Addr().String(), nil)
if test.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
_ = conn.Close()
}
})
}
}
type fakeClientConn struct {
remoteAddr *net.TCPAddr
localAddr *net.TCPAddr
+37 -19
View File
@@ -5,7 +5,9 @@ import (
"crypto/x509"
"errors"
"fmt"
"net/url"
"os"
"slices"
"strings"
"github.com/rs/zerolog/log"
@@ -144,34 +146,50 @@ func (f FileOrContent) Read() ([]byte, error) {
return content, nil
}
// VerifyPeerCertificate verifies the chain certificates and their URI.
func VerifyPeerCertificate(uri string, cfg *tls.Config, rawCerts [][]byte) error {
// TODO: Refactor to avoid useless verifyChain (ex: when insecureskipverify is false)
cert, err := verifyChain(cfg.RootCAs, rawCerts)
if err != nil {
return err
}
// SANType is the type of the Subject Alternative Name.
type SANType string
if len(uri) > 0 {
return verifyServerCertMatchesURI(uri, cert)
}
const (
// SANDNSNameType specifies hostname-based SAN.
SANDNSNameType SANType = "DNSName"
return nil
// SANURIType specifies URI-based SAN, e.g. SPIFFE id.
SANURIType SANType = "URI"
)
// +k8s:deepcopy-gen=true
// SAN represents a Subject Alternative Name.
type SAN struct {
Type SANType `json:"type,omitempty" toml:"type,omitempty" yaml:"type,omitempty"`
Value string `json:"value,omitempty" toml:"value,omitempty" yaml:"value,omitempty"`
}
// verifyServerCertMatchesURI verifies that the given certificate contains the specified URI in its SANs.
func verifyServerCertMatchesURI(uri string, cert *x509.Certificate) error {
if cert == nil {
return errors.New("peer certificate mismatch: no peer certificate presented")
// VerifyPeerCertificate verifies the chain certificates and their URI.
func VerifyPeerCertificate(sans []SAN, rootCAs *x509.CertPool, rawCerts [][]byte) error {
// TODO: Refactor to avoid useless verifyChain (ex: when insecureskipverify is false)
cert, err := verifyChain(rootCAs, rawCerts)
if err != nil {
return fmt.Errorf("verifying chain: %w", err)
}
for _, certURI := range cert.URIs {
if strings.EqualFold(certURI.String(), uri) {
return nil
for _, san := range sans {
switch san.Type {
case SANURIType:
if slices.ContainsFunc(cert.URIs, func(uri *url.URL) bool {
return strings.EqualFold(san.Value, uri.String())
}) {
return nil
}
case SANDNSNameType:
if err := cert.VerifyHostname(san.Value); err == nil {
return nil
}
}
}
return fmt.Errorf("peer certificate mismatch: no SAN URI in peer certificate matches %s", uri)
return errors.New("no matching SAN in peer certificate")
}
// verifyChain performs standard TLS verification without enforcing remote hostname matching.
+173 -26
View File
@@ -1,55 +1,145 @@
package tls
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_verifyServerCertMatchesURI(t *testing.T) {
func Test_VerifyPeerCertificate(t *testing.T) {
pki := newTestPKI(t)
tests := []struct {
desc string
uri string
cert *x509.Certificate
expErr require.ErrorAssertionFunc
desc string
sans []SAN
rawCerts [][]byte
rootCAs *x509.CertPool
expErr require.ErrorAssertionFunc
}{
{
desc: "returns error when certificate is nil",
uri: "spiffe://foo.com",
expErr: require.Error,
desc: "returns error when no certificates are provided",
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
rawCerts: nil,
rootCAs: pki.caPool,
expErr: require.Error,
},
{
desc: "returns error when certificate has no URIs",
uri: "spiffe://foo.com",
cert: &x509.Certificate{URIs: nil},
expErr: require.Error,
desc: "returns error when certificate has no URIs",
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, nil)},
rootCAs: pki.caPool,
expErr: require.Error,
},
{
desc: "returns error when no URI matches",
uri: "spiffe://foo.com",
cert: &x509.Certificate{URIs: []*url.URL{
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
{Scheme: "spiffe", Host: "other.org"},
}},
expErr: require.Error,
})},
rootCAs: pki.caPool,
expErr: require.Error,
},
{
desc: "returns nil when URI matches",
uri: "spiffe://foo.com",
cert: &x509.Certificate{URIs: []*url.URL{
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
{Scheme: "spiffe", Host: "foo.com"},
}},
expErr: require.NoError,
})},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when one of the URI matches",
uri: "spiffe://foo.com",
cert: &x509.Certificate{URIs: []*url.URL{
desc: "returns nil when one of the URIs matches",
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
{Scheme: "spiffe", Host: "example.org"},
{Scheme: "spiffe", Host: "foo.com"},
}},
expErr: require.NoError,
})},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns error when certificate has no DNS names",
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, nil)},
rootCAs: pki.caPool,
expErr: require.Error,
},
{
desc: "returns error when no DNS name matches",
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"other.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.Error,
},
{
desc: "returns nil when DNS name matches",
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when DNS name matches a wildcard",
sans: []SAN{{Type: SANDNSNameType, Value: "bar.foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"*.foo.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when one of the DNS names matches",
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"example.com", "foo.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when DNS name matches case-insensitively",
sans: []SAN{{Type: SANDNSNameType, Value: "FOO.COM"}},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when URI matches in mixed sans",
sans: []SAN{
{Type: SANURIType, Value: "spiffe://foo.com"},
{Type: SANDNSNameType, Value: "foo.com"},
},
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
{Scheme: "spiffe", Host: "foo.com"},
})},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns nil when DNS name matches in mixed sans",
sans: []SAN{
{Type: SANURIType, Value: "spiffe://foo.com"},
{Type: SANDNSNameType, Value: "foo.com"},
},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
rootCAs: pki.caPool,
expErr: require.NoError,
},
{
desc: "returns error when neither URI nor DNS name matches in mixed sans",
sans: []SAN{
{Type: SANURIType, Value: "spiffe://foo.com"},
{Type: SANDNSNameType, Value: "foo.com"},
},
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"other.com"}, []*url.URL{
{Scheme: "spiffe", Host: "other.org"},
})},
rootCAs: pki.caPool,
expErr: require.Error,
},
}
@@ -57,8 +147,65 @@ func Test_verifyServerCertMatchesURI(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := verifyServerCertMatchesURI(test.uri, test.cert)
err := VerifyPeerCertificate(test.sans, test.rootCAs, test.rawCerts)
test.expErr(t, err)
})
}
}
type testPKI struct {
caPool *x509.CertPool
caCert *x509.Certificate
caKey *rsa.PrivateKey
}
func newTestPKI(t *testing.T) *testPKI {
t.Helper()
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{Organization: []string{"Test CA"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
}
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
require.NoError(t, err)
caCert, err := x509.ParseCertificate(caCertDER)
require.NoError(t, err)
pool := x509.NewCertPool()
pool.AddCert(caCert)
return &testPKI{caPool: pool, caCert: caCert, caKey: caKey}
}
func (p *testPKI) newLeafCertDER(t *testing.T, dnsNames []string, uris []*url.URL) []byte {
t.Helper()
leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{Organization: []string{"Test Leaf"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: dnsNames,
URIs: uris,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, p.caCert, &leafKey.PublicKey, p.caKey)
require.NoError(t, err)
return certDER
}
+16
View File
@@ -134,6 +134,22 @@ func (in *Options) DeepCopy() *Options {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SAN) DeepCopyInto(out *SAN) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SAN.
func (in *SAN) DeepCopy() *SAN {
if in == nil {
return nil
}
out := new(SAN)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Store) DeepCopyInto(out *Store) {
*out = *in
+3 -3
View File
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v3.7.4
# example new bugfix v3.7.5
CurrentRef = "v3.7"
PreviousRef = "v3.7.3"
PreviousRef = "v3.7.4"
BaseBranch = "v3.7"
FutureCurrentRefName = "v3.7.4"
FutureCurrentRefName = "v3.7.5"
ThresholdPreviousRef = 10000
ThresholdCurrentRef = 10000