Merge branch v3.6 into v3.7

This commit is contained in:
kevinpollet
2026-05-11 11:58:39 +02:00
25 changed files with 1365 additions and 194 deletions
+52
View File
@@ -9,6 +9,33 @@ This guide provides detailed migration steps for upgrading between different Tra
---
## v3.7.1
### Kubernetes providers: `crossProviderNamespaces`
In `v3.7.1`, a new `crossProviderNamespaces` option is available on the Kubernetes CRD, Ingress, and Gateway providers.
Traefik offers the possibility to reference resources from one provider to another (cross-provider references).
However, in the context of Kubernetes providers,
those references (e.g. `myservice@kubernetescrd`) allow a user to cross namespace boundaries,
as well as exposing `@internal` services, that only the operator should be able to expose.
This new `crossProviderNamespaces` option restricts in which namespaces Kubernetes resources are allowed to use cross-provider references.
The behavior is as follows:
| Value | Behavior |
|------------|-------------------------------------------------------------------------------------------|
| not set | All Kubernetes resources can declare cross-provider references. |
| `[]` | Every Kubernetes resource declaring a cross-provider reference is rejected. |
| `["ns-a"]` | Only Kubernetes resources in the listed namespaces can declare cross-provider references. |
Please check out the [Kubernetes CRD](../reference/install-configuration/providers/kubernetes/kubernetes-crd.md#opt-providers-kubernetesCRD-crossProviderNamespaces), [Kubernetes Ingress](../reference/install-configuration/providers/kubernetes/kubernetes-ingress.md#opt-providers-kubernetesIngress-crossProviderNamespaces),
and [Kubernetes Gateway](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesGateway-crossProviderNamespaces) provider documentation for more details.
---
## v3.7.0
### Ingress NGINX Provider
@@ -80,6 +107,31 @@ Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Ho
---
## v3.6.17
### Kubernetes providers: `crossProviderNamespaces`
In `v3.6.17`, a new `crossProviderNamespaces` option is available on the Kubernetes CRD, Ingress, and Gateway providers.
Traefik offers the possibility to reference resources from one provider to another (cross-provider references).
However, in the context of Kubernetes providers,
those references (e.g. `myservice@kubernetescrd`) allow a user to cross namespace boundaries,
as well as exposing `@internal` services, that only the operator should be able to expose.
This new `crossProviderNamespaces` option restricts in which namespaces Kubernetes resources are allowed to use cross-provider references.
The behavior is as follows:
| Value | Behavior |
|------------|-------------------------------------------------------------------------------------------|
| not set | All Kubernetes resources can declare cross-provider references. |
| `[]` | Every Kubernetes resource declaring a cross-provider reference is rejected. |
| `["ns-a"]` | Only Kubernetes resources in the listed namespaces can declare cross-provider references. |
Please check out the [Kubernetes CRD](../reference/install-configuration/providers/kubernetes/kubernetes-crd.md#opt-providers-kubernetesCRD-crossProviderNamespaces), [Kubernetes Ingress](../reference/install-configuration/providers/kubernetes/kubernetes-ingress.md#opt-providers-kubernetesIngress-crossProviderNamespaces),
and [Kubernetes Gateway](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesGateway-crossProviderNamespaces) provider documentation for more details.
## v3.6.16
### Docker provider: minimum Docker Engine version
@@ -353,6 +353,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetescrd-allowemptyservices" href="#opt-providers-kubernetescrd-allowemptyservices" title="#opt-providers-kubernetescrd-allowemptyservices">providers.kubernetescrd.allowemptyservices</a> | Allow the creation of services without endpoints. | false |
| <a id="opt-providers-kubernetescrd-allowexternalnameservices" href="#opt-providers-kubernetescrd-allowexternalnameservices" title="#opt-providers-kubernetescrd-allowexternalnameservices">providers.kubernetescrd.allowexternalnameservices</a> | Allow ExternalName services. | false |
| <a id="opt-providers-kubernetescrd-certauthfilepath" href="#opt-providers-kubernetescrd-certauthfilepath" title="#opt-providers-kubernetescrd-certauthfilepath">providers.kubernetescrd.certauthfilepath</a> | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| <a id="opt-providers-kubernetescrd-crossprovidernamespaces" href="#opt-providers-kubernetescrd-crossprovidernamespaces" title="#opt-providers-kubernetescrd-crossprovidernamespaces">providers.kubernetescrd.crossprovidernamespaces</a> | List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references. | |
| <a id="opt-providers-kubernetescrd-disableclusterscoperesources" href="#opt-providers-kubernetescrd-disableclusterscoperesources" title="#opt-providers-kubernetescrd-disableclusterscoperesources">providers.kubernetescrd.disableclusterscoperesources</a> | Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). | false |
| <a id="opt-providers-kubernetescrd-endpoint" href="#opt-providers-kubernetescrd-endpoint" title="#opt-providers-kubernetescrd-endpoint">providers.kubernetescrd.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
| <a id="opt-providers-kubernetescrd-ingressclass" href="#opt-providers-kubernetescrd-ingressclass" title="#opt-providers-kubernetescrd-ingressclass">providers.kubernetescrd.ingressclass</a> | Value of ingressClassName field or kubernetes.io/ingress.class annotation to watch for. | |
@@ -363,6 +364,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetescrd-token" href="#opt-providers-kubernetescrd-token" title="#opt-providers-kubernetescrd-token">providers.kubernetescrd.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
| <a id="opt-providers-kubernetesgateway" href="#opt-providers-kubernetesgateway" title="#opt-providers-kubernetesgateway">providers.kubernetesgateway</a> | Enables Kubernetes Gateway API provider. | false |
| <a id="opt-providers-kubernetesgateway-certauthfilepath" href="#opt-providers-kubernetesgateway-certauthfilepath" title="#opt-providers-kubernetesgateway-certauthfilepath">providers.kubernetesgateway.certauthfilepath</a> | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| <a id="opt-providers-kubernetesgateway-crossprovidernamespaces" href="#opt-providers-kubernetesgateway-crossprovidernamespaces" title="#opt-providers-kubernetesgateway-crossprovidernamespaces">providers.kubernetesgateway.crossprovidernamespaces</a> | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | |
| <a id="opt-providers-kubernetesgateway-endpoint" href="#opt-providers-kubernetesgateway-endpoint" title="#opt-providers-kubernetesgateway-endpoint">providers.kubernetesgateway.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
| <a id="opt-providers-kubernetesgateway-experimentalchannel" href="#opt-providers-kubernetesgateway-experimentalchannel" title="#opt-providers-kubernetesgateway-experimentalchannel">providers.kubernetesgateway.experimentalchannel</a> | Toggles Experimental Channel resources support (TCPRoute, TLSRoute...). | false |
| <a id="opt-providers-kubernetesgateway-labelselector" href="#opt-providers-kubernetesgateway-labelselector" title="#opt-providers-kubernetesgateway-labelselector">providers.kubernetesgateway.labelselector</a> | Kubernetes label selector to select specific GatewayClasses. | |
@@ -379,6 +381,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-allowemptyservices" href="#opt-providers-kubernetesingress-allowemptyservices" title="#opt-providers-kubernetesingress-allowemptyservices">providers.kubernetesingress.allowemptyservices</a> | Allow creation of services without endpoints. | false |
| <a id="opt-providers-kubernetesingress-allowexternalnameservices" href="#opt-providers-kubernetesingress-allowexternalnameservices" title="#opt-providers-kubernetesingress-allowexternalnameservices">providers.kubernetesingress.allowexternalnameservices</a> | Allow ExternalName services. | false |
| <a id="opt-providers-kubernetesingress-certauthfilepath" href="#opt-providers-kubernetesingress-certauthfilepath" title="#opt-providers-kubernetesingress-certauthfilepath">providers.kubernetesingress.certauthfilepath</a> | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| <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 declare Middlewares, TLSOptions, or ServersTransport references. | |
| <a id="opt-providers-kubernetesingress-disableclusterscoperesources" href="#opt-providers-kubernetesingress-disableclusterscoperesources" title="#opt-providers-kubernetesingress-disableclusterscoperesources">providers.kubernetesingress.disableclusterscoperesources</a> | Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). | false |
| <a id="opt-providers-kubernetesingress-disableingressclasslookup" href="#opt-providers-kubernetesingress-disableingressclasslookup" title="#opt-providers-kubernetesingress-disableingressclasslookup">providers.kubernetesingress.disableingressclasslookup</a> | Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources). | false |
| <a id="opt-providers-kubernetesingress-endpoint" href="#opt-providers-kubernetesingress-endpoint" title="#opt-providers-kubernetesingress-endpoint">providers.kubernetesingress.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
@@ -65,6 +65,7 @@ providers:
| <a id="opt-providers-kubernetesCRD-allowEmptyServices" href="#opt-providers-kubernetesCRD-allowEmptyServices" title="#opt-providers-kubernetesCRD-allowEmptyServices">`providers.kubernetesCRD.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-kubernetesCRD-allowCrossNamespace" href="#opt-providers-kubernetesCRD-allowCrossNamespace" title="#opt-providers-kubernetesCRD-allowCrossNamespace">`providers.kubernetesCRD.allowCrossNamespace`</a> | Allows the `IngressRoutes` to reference resources in namespaces other than theirs. | false | No |
| <a id="opt-providers-kubernetesCRD-allowExternalNameServices" href="#opt-providers-kubernetesCRD-allowExternalNameServices" title="#opt-providers-kubernetesCRD-allowExternalNameServices">`providers.kubernetesCRD.allowExternalNameServices`</a> | Allows the `IngressRoutes` to reference ExternalName services. | false | No |
| <a id="opt-providers-kubernetesCRD-crossProviderNamespaces" href="#opt-providers-kubernetesCRD-crossProviderNamespaces" title="#opt-providers-kubernetesCRD-crossProviderNamespaces">`providers.kubernetesCRD.crossProviderNamespaces`</a> | List of namespaces from which `IngressRoute`, `IngressRouteTCP`, `IngressRouteUDP`, and `TraefikService` are allowed to declare cross-provider references (e.g. `myservice@file`).<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| <a id="opt-providers-kubernetesCRD-nativeLBByDefault" href="#opt-providers-kubernetesCRD-nativeLBByDefault" title="#opt-providers-kubernetesCRD-nativeLBByDefault">`providers.kubernetesCRD.nativeLBByDefault`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `IngressRoute` 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-kubernetesCRD-disableClusterScopeResources" href="#opt-providers-kubernetesCRD-disableClusterScopeResources" title="#opt-providers-kubernetesCRD-disableClusterScopeResources">`providers.kubernetesCRD.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 IngressRoutes 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 |
@@ -82,6 +82,7 @@ providers:
| <a id="opt-providers-kubernetesGateway-statusAddress-ip" href="#opt-providers-kubernetesGateway-statusAddress-ip" title="#opt-providers-kubernetesGateway-statusAddress-ip">`providers.kubernetesGateway.`<br />`statusAddress.ip`</a> | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-namespace" href="#opt-providers-kubernetesGateway-statusAddress-service-namespace" title="#opt-providers-kubernetesGateway-statusAddress-service-namespace">`providers.kubernetesGateway.`<br />`statusAddress.service.namespace`</a> | The namespace of the Kubernetes service to copy status addresses from.<br />When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-name" href="#opt-providers-kubernetesGateway-statusAddress-service-name" title="#opt-providers-kubernetesGateway-statusAddress-service-name">`providers.kubernetesGateway.`<br />`statusAddress.service.name`</a> | The name of the Kubernetes service to copy status addresses from.<br />When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-crossProviderNamespaces" href="#opt-providers-kubernetesGateway-crossProviderNamespaces" title="#opt-providers-kubernetesGateway-crossProviderNamespaces">`providers.kubernetesGateway.crossProviderNamespaces`</a> | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.<br />When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
<!-- markdownlint-enable MD013 -->
@@ -45,25 +45,26 @@ which in turn creates the resulting routers, services, handlers, etc.
<!-- markdownlint-disable MD013 -->
| Field | Description | Default | Required |
| :------------------------------------------------------------------ | :------------- | :------ | :------- |
| <a id="opt-providers-providersThrottleDuration" href="#opt-providers-providersThrottleDuration" title="#opt-providers-providersThrottleDuration">`providers.providersThrottleDuration`</a> | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
| <a id="opt-providers-kubernetesIngress-endpoint" href="#opt-providers-kubernetesIngress-endpoint" title="#opt-providers-kubernetesIngress-endpoint">`providers.kubernetesIngress.endpoint`</a> | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
| <a id="opt-providers-kubernetesIngress-token" href="#opt-providers-kubernetesIngress-token" title="#opt-providers-kubernetesIngress-token">`providers.kubernetesIngress.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-certAuthFilePath" href="#opt-providers-kubernetesIngress-certAuthFilePath" title="#opt-providers-kubernetesIngress-certAuthFilePath">`providers.kubernetesIngress.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-namespaces" href="#opt-providers-kubernetesIngress-namespaces" title="#opt-providers-kubernetesIngress-namespaces">`providers.kubernetesIngress.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | | No |
| <a id="opt-providers-kubernetesIngress-labelselector" href="#opt-providers-kubernetesIngress-labelselector" title="#opt-providers-kubernetesIngress-labelselector">`providers.kubernetesIngress.labelselector`</a> | Allow filtering on `Ingress` objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressClass" href="#opt-providers-kubernetesIngress-ingressClass" title="#opt-providers-kubernetesIngress-ingressClass">`providers.kubernetesIngress.ingressClass`</a> | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.<br />If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
| <a id="opt-providers-kubernetesIngress-disableIngressClassLookup" href="#opt-providers-kubernetesIngress-disableIngressClassLookup" title="#opt-providers-kubernetesIngress-disableIngressClassLookup">`providers.kubernetesIngress.disableIngressClassLookup`</a> | Prevent to discover IngressClasses in the cluster.<br />It alleviates the requirement of giving Traefik the rights to look IngressClasses up.<br />Ignore Ingresses with IngressClass.<br />Annotations are not affected by this option. | false | No |
| <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-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-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 |
| Field | Description | Default | Required |
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| <a id="opt-providers-providersThrottleDuration" href="#opt-providers-providersThrottleDuration" title="#opt-providers-providersThrottleDuration">`providers.providersThrottleDuration`</a> | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
| <a id="opt-providers-kubernetesIngress-endpoint" href="#opt-providers-kubernetesIngress-endpoint" title="#opt-providers-kubernetesIngress-endpoint">`providers.kubernetesIngress.endpoint`</a> | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
| <a id="opt-providers-kubernetesIngress-token" href="#opt-providers-kubernetesIngress-token" title="#opt-providers-kubernetesIngress-token">`providers.kubernetesIngress.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-certAuthFilePath" href="#opt-providers-kubernetesIngress-certAuthFilePath" title="#opt-providers-kubernetesIngress-certAuthFilePath">`providers.kubernetesIngress.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-namespaces" href="#opt-providers-kubernetesIngress-namespaces" title="#opt-providers-kubernetesIngress-namespaces">`providers.kubernetesIngress.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | | No |
| <a id="opt-providers-kubernetesIngress-labelselector" href="#opt-providers-kubernetesIngress-labelselector" title="#opt-providers-kubernetesIngress-labelselector">`providers.kubernetesIngress.labelselector`</a> | Allow filtering on `Ingress` objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressClass" href="#opt-providers-kubernetesIngress-ingressClass" title="#opt-providers-kubernetesIngress-ingressClass">`providers.kubernetesIngress.ingressClass`</a> | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.<br />If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
| <a id="opt-providers-kubernetesIngress-disableIngressClassLookup" href="#opt-providers-kubernetesIngress-disableIngressClassLookup" title="#opt-providers-kubernetesIngress-disableIngressClassLookup">`providers.kubernetesIngress.disableIngressClassLookup`</a> | Prevent to discover IngressClasses in the cluster.<br />It alleviates the requirement of giving Traefik the rights to look IngressClasses up.<br />Ignore Ingresses with IngressClass.<br />Annotations are not affected by this option. | false | No |
| <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-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-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 |
| <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 -->
@@ -0,0 +1,19 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: whoamitcp
port: 8000
tls:
options:
name: foo@file
@@ -0,0 +1,18 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami
port: 80
serversTransport: foo@file
@@ -0,0 +1,70 @@
---
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: mirror-cp
namespace: foo
spec:
mirroring:
name: external-main@file
kind: TraefikService
mirrors:
- name: external-mirror@file
kind: TraefikService
percent: 50
---
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: weighted-cp
namespace: bar
spec:
weighted:
services:
- name: external-a@file
kind: TraefikService
weight: 1
- name: external-b@file
kind: TraefikService
weight: 1
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: ir-mirror
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`mirror.example.com`)
kind: Rule
services:
- name: mirror-cp
namespace: foo
kind: TraefikService
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: ir-weighted
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`weighted.example.com`)
kind: Rule
services:
- name: weighted-cp
namespace: bar
kind: TraefikService
@@ -0,0 +1,21 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami
port: 80
tls:
options:
name: foo@file
+64 -38
View File
@@ -57,6 +57,7 @@ type Provider struct {
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
CrossProviderNamespaces []string `description:"List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of ingressClassName field or kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
@@ -93,6 +94,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
}
if p.CrossProviderNamespaces != nil {
logger.Warn().Msgf("Cross-provider references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@@ -307,7 +312,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
continue
}
chain, err := createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain, p.AllowCrossNamespace)
chain, err := p.createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain)
if err != nil {
logger.Error().Err(err).Msg("Error while reading chain middleware")
continue
@@ -358,6 +363,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices,
crossProviderNamespaces: p.CrossProviderNamespaces,
}
for _, service := range client.GetTraefikServices() {
@@ -666,6 +672,7 @@ func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client,
allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices,
crossProviderNamespaces: p.CrossProviderNamespaces,
}
balancerName, balancerServerHTTP, err := cb.nameAndService(ctx, namespace, errorPage.Service.LoadBalancerSpec)
@@ -680,6 +687,26 @@ func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client,
}, balancerServerHTTP, nil
}
func (p *Provider) createChainMiddleware(ctx context.Context, parentNamespace string, chain *traefikv1alpha1.Chain) (*dynamic.Chain, error) {
if chain == nil {
return nil, nil
}
var mds []string
for _, mi := range chain.Middlewares {
ctxMid := log.Ctx(ctx).With().Str("middlewareRef", mi.Namespace+"/"+mi.Name).Logger().WithContext(ctx)
middlewareRef, err := resolveReference(ctxMid, parentNamespace, mi.Namespace, mi.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
if err != nil {
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
}
mds = append(mds, middlewareRef)
}
return &dynamic.Chain{Middlewares: mds}, nil
}
// getServicePort always returns a valid port, an error otherwise.
func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.ServicePort, error) {
if svc == nil {
@@ -1280,43 +1307,6 @@ func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
return credentials, nil
}
func createChainMiddleware(ctx context.Context, parentNamespace string, chain *traefikv1alpha1.Chain, allowCrossNamespace bool) (*dynamic.Chain, error) {
if chain == nil {
return nil, nil
}
var mds []string
for _, mi := range chain.Middlewares {
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross-namespace references.
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
}
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.Ctx(ctx).Warn().Msgf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
}
ns := parentNamespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(allowCrossNamespace, parentNamespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the chain namespace %s", mi.Namespace, mi.Name, parentNamespace)
}
ns = mi.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
}
return &dynamic.Chain{Middlewares: mds}, nil
}
func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options {
tlsOptionsCRDs := client.GetTLSOptions()
var tlsOptions map[string]tls.Options
@@ -1659,3 +1649,39 @@ func isNamespaceAllowed(allowCrossNamespace bool, parentNamespace, namespace str
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
return allowCrossNamespace || parentNamespace == namespace
}
// isCrossProviderNamespaceAllowed reports whether the given namespace is allowed to declare direct references to Traefik resources.
// A nil allowList means references are unrestricted, and an empty allowList disables them entirely.
func isCrossProviderNamespaceAllowed(allowList []string, namespace string) bool {
if allowList == nil {
return true
}
return slices.Contains(allowList, namespace)
}
func resolveReference(ctx context.Context, parentNs, ns, name string, crossProviderNamespaces []string, allowCrossNamespace bool) (string, error) {
if strings.Contains(name, providerNamespaceSeparator) {
if !allowCrossNamespace && strings.HasSuffix(name, providerNamespaceSeparator+ProviderName) {
return "", errors.New("when allowCrossNamespace is disabled, @kubernetescrd references are disallowed")
}
if !isCrossProviderNamespaceAllowed(crossProviderNamespaces, parentNs) {
return "", fmt.Errorf("namespace %q is not in crossProviderNamespaces", parentNs)
}
if ns != "" {
log.Ctx(ctx).Warn().Msgf("Namespace %q is ignored in cross-provider context", ns)
}
return name, nil
}
ns = namespaceOrParentNamespace(ns, parentNs)
if !isNamespaceAllowed(allowCrossNamespace, parentNs, ns) {
return "", errors.New("allowCrossNamespace is disabled, cross-namespace are disallowed")
}
return provider.Normalize(ns + "-" + name), nil
}
+42 -63
View File
@@ -61,6 +61,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
allowEmptyServices: p.AllowEmptyServices,
nativeLBByDefault: p.NativeLBByDefault,
disableClusterScopeResources: p.DisableClusterScopeResources,
crossProviderNamespaces: p.CrossProviderNamespaces,
}
parentRouterNames, err := resolveParentRouterNames(client, ingressRoute, p.AllowCrossNamespace)
@@ -82,7 +83,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
serviceKey := makeServiceKey(route.Match, ingressName)
mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.AllowCrossNamespace)
mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.CrossProviderNamespaces, p.AllowCrossNamespace)
if err != nil {
logger.Error().Err(err).Msg("Failed to create middleware keys")
continue
@@ -147,27 +148,14 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRoute.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference, (i.e. not a cross-provider reference)
ns := ingressRoute.Spec.TLS.Options.Namespace
if !strings.Contains(tlsOptionsName, providerNamespaceSeparator) {
if len(ns) == 0 {
ns = ingressRoute.Namespace
}
tlsOptionsName = makeID(ns, tlsOptionsName)
} else if len(ns) > 0 {
logger.
Warn().Str("TLSOption", ingressRoute.Spec.TLS.Options.Name).
Msgf("Namespace %q is ignored in cross-provider context", ns)
}
tlsOptions := ingressRoute.Spec.TLS.Options
ctxTLSOption := log.Ctx(ctx).With().Str("TLSOption", tlsOptions.Name).Logger().WithContext(ctx)
if !isNamespaceAllowed(p.AllowCrossNamespace, ingressRoute.Namespace, ns) {
logger.Error().Msgf("TLSOption %s/%s is not in the IngressRoute namespace %s",
ns, ingressRoute.Spec.TLS.Options.Name, ingressRoute.Namespace)
r.TLS.Options, err = resolveReference(ctxTLSOption, ingressRoute.Namespace, tlsOptions.Namespace, tlsOptions.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
if err != nil {
logger.Error().Err(err).Msgf("Invalid reference to TLSOption %q", ingressRoute.Spec.TLS.Options.Name)
continue
}
r.TLS.Options = tlsOptionsName
}
}
@@ -180,40 +168,18 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
return conf
}
func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []traefikv1alpha1.MiddlewareRef, allowCrossNamespace bool) ([]string, error) {
func makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []traefikv1alpha1.MiddlewareRef, crossProviderNamespaces []string, allowCrossNamespace bool) ([]string, error) {
var mds []string
for _, mi := range middlewares {
name := mi.Name
ctxMid := log.Ctx(ctx).With().Str(logs.MiddlewareName, mi.Name).Logger().WithContext(ctx)
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross-namespace references.
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
middlewareRef, err := resolveReference(ctxMid, ingRouteNamespace, mi.Namespace, mi.Name, crossProviderNamespaces, allowCrossNamespace)
if err != nil {
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
}
if strings.Contains(name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.Ctx(ctx).
Warn().Str(logs.MiddlewareName, mi.Name).
Msgf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, name)
continue
}
ns := namespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(allowCrossNamespace, namespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the parent namespace %s", mi.Namespace, mi.Name, namespace)
}
ns = mi.Namespace
}
mds = append(mds, provider.Normalize(makeID(ns, name)))
mds = append(mds, middlewareRef)
}
return mds, nil
@@ -270,6 +236,7 @@ type configBuilder struct {
allowEmptyServices bool
nativeLBByDefault bool
disableClusterScopeResources bool
crossProviderNamespaces []string
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@@ -514,7 +481,7 @@ func (c configBuilder) buildServersLB(ctx context.Context, svc traefikv1alpha1.L
service := &dynamic.Service{LoadBalancer: lb}
if len(svc.Middlewares) > 0 {
mds, err := makeMiddlewareKeys(ctx, svc.Namespace, svc.Middlewares, c.allowCrossNamespace)
mds, err := makeMiddlewareKeys(ctx, svc.Namespace, svc.Middlewares, c.crossProviderNamespaces, c.allowCrossNamespace)
if err != nil {
return nil, fmt.Errorf("could not create middleware keys: %w", err)
}
@@ -529,14 +496,18 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr
return "", nil
}
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
return "", fmt.Errorf("invalid reference to serversTransport %s: namespace-name@kubernetescrd format is not allowed when crossnamespace is disallowed", serversTransportName)
}
if strings.Contains(serversTransportName, providerNamespaceSeparator) {
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
return "", fmt.Errorf("invalid reference to serversTransport %s: namespace-name@kubernetescrd format is not allowed when crossnamespace is disallowed", serversTransportName)
}
if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) {
return "", fmt.Errorf("serversTransport %q reference is not allowed: namespace %q is not in crossProviderNamespaces", serversTransportName, parentNamespace)
}
return serversTransportName, nil
}
@@ -691,11 +662,17 @@ func (c configBuilder) loadServers(svc traefikv1alpha1.LoadBalancerSpec) ([]dyna
func (c configBuilder) nameAndService(ctx context.Context, parentNamespace string, service traefikv1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
svcCtx := log.Ctx(ctx).With().Str(logs.ServiceName, service.Name).Logger().WithContext(ctx)
service = *service.DeepCopy()
service.Namespace = namespaceOrFallback(service, parentNamespace)
if !strings.Contains(service.Name, providerNamespaceSeparator) {
service = *service.DeepCopy()
service.Namespace = namespaceOrParentNamespace(service.Namespace, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, service.Namespace) {
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, service.Namespace) {
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
}
}
if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) && strings.Contains(service.Name, providerNamespaceSeparator) {
return "", nil, fmt.Errorf("service %q reference is not allowed: namespace %q is not in crossProviderNamespaces", service.Name, parentNamespace)
}
switch service.Kind {
@@ -811,11 +788,12 @@ func fullServiceName(ctx context.Context, service traefikv1alpha1.LoadBalancerSp
return provider.Normalize(name) + providerNamespaceSeparator + pName
}
func namespaceOrFallback(lb traefikv1alpha1.LoadBalancerSpec, fallback string) string {
if lb.Namespace != "" {
return lb.Namespace
func namespaceOrParentNamespace(namespace, parentNamespace string) string {
if namespace != "" {
return namespace
}
return fallback
return parentNamespace
}
// getTLSHTTP mutates tlsConfigs.
@@ -823,6 +801,7 @@ func getTLSHTTP(ctx context.Context, ingressRoute *traefikv1alpha1.IngressRoute,
if ingressRoute.Spec.TLS == nil {
return nil
}
if ingressRoute.Spec.TLS.SecretName == "" {
log.Ctx(ctx).Debug().Msg("No secret name provided")
return nil
+14 -42
View File
@@ -114,27 +114,14 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
}
if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRouteTCP.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference (i.e. not a cross-provider reference)
ns := ingressRouteTCP.Spec.TLS.Options.Namespace
if !strings.Contains(tlsOptionsName, providerNamespaceSeparator) {
if len(ns) == 0 {
ns = ingressRouteTCP.Namespace
}
tlsOptionsName = makeID(ns, tlsOptionsName)
} else if len(ns) > 0 {
logger.Warn().
Str("TLSOption", ingressRouteTCP.Spec.TLS.Options.Name).
Msgf("Namespace %q is ignored in cross-provider context", ns)
}
tlsOptions := ingressRouteTCP.Spec.TLS.Options
ctxTLSOption := log.Ctx(ctx).With().Str("TLSOption", tlsOptions.Name).Logger().WithContext(ctx)
if !isNamespaceAllowed(p.AllowCrossNamespace, ingressRouteTCP.Namespace, ns) {
logger.Error().Msgf("TLSOption %s/%s is not in the IngressRouteTCP namespace %s",
ns, ingressRouteTCP.Spec.TLS.Options.Name, ingressRouteTCP.Namespace)
r.TLS.Options, err = resolveReference(ctxTLSOption, ingressRouteTCP.Namespace, tlsOptions.Namespace, tlsOptions.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
if err != nil {
logger.Error().Err(err).Msgf("Invalid reference to TLSOption %q", ingressRouteTCP.Spec.TLS.Options.Name)
continue
}
r.TLS.Options = tlsOptionsName
}
}
@@ -149,39 +136,24 @@ func (p *Provider) makeMiddlewareTCPKeys(ctx context.Context, ingRouteTCPNamespa
var mds []string
for _, mi := range middlewares {
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.Ctx(ctx).Warn().
Str(logs.MiddlewareName, mi.Name).
Msgf("Namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
ctxMid := log.Ctx(ctx).With().Str(logs.MiddlewareName, mi.Name).Logger().WithContext(ctx)
middlewareRef, err := resolveReference(ctxMid, ingRouteTCPNamespace, mi.Namespace, mi.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
if err != nil {
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
}
ns := ingRouteTCPNamespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteTCPNamespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the IngressRouteTCP namespace %s", mi.Namespace, mi.Name, ingRouteTCPNamespace)
}
ns = mi.Namespace
}
mds = append(mds, provider.Normalize(makeID(ns, mi.Name)))
mds = append(mds, middlewareRef)
}
return mds, nil
}
func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace string, service traefikv1alpha1.ServiceTCP) (*dynamic.TCPService, error) {
ns := parentNamespace
if len(service.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, service.Namespace) {
return nil, fmt.Errorf("tcp service %s/%s is not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
}
ns := namespaceOrParentNamespace(service.Namespace, parentNamespace)
ns = service.Namespace
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, ns) {
return nil, fmt.Errorf("tcp service %s/%s is not in the parent resource namespace %s", ns, service.Name, parentNamespace)
}
servers, err := p.loadTCPServers(client, ns, service)
+399 -10
View File
@@ -259,7 +259,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
},
{
desc: "Simple Ingress Route, with foo entrypoint and crossprovider middleware",
paths: []string{"tcp/services.yml", "tcp/with_middleware_crossprovider.yml"},
paths: []string{"tcp/services.yml", "tcp/with_middleware_cross_provider.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
@@ -1813,12 +1813,13 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
func TestLoadIngressRoutes(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
paths []string
expected *dynamic.Configuration
allowCrossNamespace bool
allowEmptyServices bool
desc string
ingressClass string
paths []string
expected *dynamic.Configuration
allowCrossNamespace bool
allowEmptyServices bool
crossProviderNamespaces []string
}{
{
desc: "Empty",
@@ -2104,9 +2105,10 @@ func TestLoadIngressRoutes(t *testing.T) {
},
},
{
desc: "Simple Ingress Route with middleware crossprovider",
allowCrossNamespace: true,
paths: []string{"services.yml", "with_middleware_crossprovider.yml"},
desc: "Simple Ingress Route with middleware crossprovider",
crossProviderNamespaces: []string{"default"},
allowCrossNamespace: true,
paths: []string{"services.yml", "with_middleware_cross_provider.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
@@ -6389,6 +6391,7 @@ func TestLoadIngressRoutes(t *testing.T) {
AllowCrossNamespace: test.allowCrossNamespace,
AllowExternalNameServices: true,
AllowEmptyServices: test.allowEmptyServices,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
@@ -8676,6 +8679,392 @@ func TestCrossNamespace(t *testing.T) {
}
}
func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
testCases := []struct {
desc string
allowList []string
namespace string
want bool
}{
{desc: "nil allowList allows any namespace", allowList: nil, namespace: "ns-a", want: true},
{desc: "empty allowList denies every namespace", allowList: []string{}, namespace: "ns-a", want: false},
{desc: "namespace in allowList is accepted", allowList: []string{"ns-a"}, namespace: "ns-a", want: true},
{desc: "namespace not in allowList is rejected", allowList: []string{"ns-b"}, namespace: "ns-a", want: false},
{desc: "namespace among multiple allowed entries is accepted", allowList: []string{"ns-a", "ns-b"}, namespace: "ns-b", want: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := isCrossProviderNamespaceAllowed(test.allowList, test.namespace)
assert.Equal(t, test.want, got)
})
}
}
// TestCrossProviderNamespaces_HTTPMiddleware verifies that the
// CrossProviderNamespaces option gates middleware references.
// Plain in-namespace middleware references are not affected.
func TestCrossProviderNamespaces_HTTPMiddleware(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantMiddlewares []string
wantRouterDropped bool
}{
{
desc: "nil: cross-provider middleware refs are accepted (backward compatible)",
crossProviderNamespaces: nil,
wantMiddlewares: []string{"default-stripprefix", "foo-addprefix", "basicauth@file", "redirect@file"},
},
{
desc: "empty list: cross-provider middleware refs are rejected, IngressRoute is dropped",
crossProviderNamespaces: []string{},
wantRouterDropped: true,
},
{
desc: "namespace allowed: cross-provider middleware refs are accepted",
crossProviderNamespaces: []string{"default"},
wantMiddlewares: []string{"default-stripprefix", "foo-addprefix", "basicauth@file", "redirect@file"},
},
{
desc: "namespace not allowed: cross-provider middleware refs are rejected, IngressRoute is dropped",
crossProviderNamespaces: []string{"other"},
wantRouterDropped: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_middleware_cross_provider.yml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
crdClient := traefikcrdfake.NewClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll(nil, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{
AllowCrossNamespace: true,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
router, ok := conf.HTTP.Routers["default-test2-route-23c7f4c450289ee29016"]
if test.wantRouterDropped {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, test.wantMiddlewares, router.Middlewares)
})
}
}
// TestCrossProviderNamespaces_HTTPServiceTransitivity verifies that the option for a TraefikService chain
// (here: IngressRoute -> Mirror / Weighted TraefikService -> @file service).
func TestCrossProviderNamespaces_HTTPServiceTransitivity(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantMirrorService bool
wantWeightedService bool
}{
{
desc: "nil: cross-provider service refs accepted (backward compatible)",
crossProviderNamespaces: nil,
wantMirrorService: true,
wantWeightedService: true,
},
{
desc: "empty list: both Mirror and Weighted TraefikServices are rejected",
crossProviderNamespaces: []string{},
wantMirrorService: false,
wantWeightedService: false,
},
{
desc: "only the Mirror's namespace is allowed: Weighted is still rejected",
crossProviderNamespaces: []string{"foo"},
wantMirrorService: true,
wantWeightedService: false,
},
{
desc: "only the Weighted's namespace is allowed: Mirror is still rejected",
crossProviderNamespaces: []string{"bar"},
wantMirrorService: false,
wantWeightedService: true,
},
{
desc: "both namespaces allowed: both TraefikServices are accepted",
crossProviderNamespaces: []string{"foo", "bar"},
wantMirrorService: true,
wantWeightedService: true,
},
{
desc: "originating IngressRoute namespace alone is not enough: TraefikService namespace must also be allowed",
crossProviderNamespaces: []string{"default"},
wantMirrorService: false,
wantWeightedService: false,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_service_cross_provider.yml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
crdClient := traefikcrdfake.NewClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll(nil, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{
AllowCrossNamespace: true,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
_, mirrorOK := conf.HTTP.Services["foo-mirror-cp"]
_, weightedOK := conf.HTTP.Services["bar-weighted-cp"]
assert.Equal(t, test.wantMirrorService, mirrorOK)
assert.Equal(t, test.wantWeightedService, weightedOK)
})
}
}
// TestCrossProviderNamespaces_HTTPTLSOption verifies that the
// CrossProviderNamespaces option gates @file references in IngressRoute tls.options.
func TestCrossProviderNamespaces_HTTPTLSOption(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantRouterDropped bool
}{
{
desc: "nil: cross-provider TLSOption ref is accepted (backward compatible)",
crossProviderNamespaces: nil,
},
{
desc: "empty list: cross-provider TLSOption ref is rejected, IngressRoute is dropped",
crossProviderNamespaces: []string{},
wantRouterDropped: true,
},
{
desc: "namespace allowed: cross-provider TLSOption ref is accepted",
crossProviderNamespaces: []string{"default"},
},
{
desc: "namespace not allowed: cross-provider TLSOption ref is rejected, IngressRoute is dropped",
crossProviderNamespaces: []string{"other"},
wantRouterDropped: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_tls_option_cross_provider.yml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
crdClient := traefikcrdfake.NewClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll(nil, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{
AllowCrossNamespace: true,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
router, ok := conf.HTTP.Routers["default-test-route-6b204d94623b3df4370c"]
if test.wantRouterDropped {
assert.False(t, ok)
return
}
require.True(t, ok)
require.NotNil(t, router.TLS)
assert.Equal(t, "foo@file", router.TLS.Options)
})
}
}
// TestCrossProviderNamespaces_TCPTLSOption verifies that the
// CrossProviderNamespaces option gates @file references in IngressRouteTCP tls.options.
func TestCrossProviderNamespaces_TCPTLSOption(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantRouterDropped bool
}{
{
desc: "nil: cross-provider TLSOption ref is accepted (backward compatible)",
crossProviderNamespaces: nil,
},
{
desc: "empty list: cross-provider TLSOption ref is rejected, IngressRouteTCP is dropped",
crossProviderNamespaces: []string{},
wantRouterDropped: true,
},
{
desc: "namespace allowed: cross-provider TLSOption ref is accepted",
crossProviderNamespaces: []string{"default"},
},
{
desc: "namespace not allowed: cross-provider TLSOption ref is rejected, IngressRouteTCP is dropped",
crossProviderNamespaces: []string{"other"},
wantRouterDropped: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, crdObjects := readResources(t, []string{"tcp/services.yml", "tcp/with_tls_options_cross_provider.yml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
crdClient := traefikcrdfake.NewClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll(nil, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{
AllowCrossNamespace: true,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
router, ok := conf.TCP.Routers["default-test.route-fdd3e9338e47a45efefc"]
if test.wantRouterDropped {
assert.False(t, ok)
return
}
require.True(t, ok)
require.NotNil(t, router.TLS)
assert.Equal(t, "foo@file", router.TLS.Options)
})
}
}
// TestCrossProviderNamespaces_HTTPServersTransport verifies that the
// CrossProviderNamespaces option gates @file references in service.serversTransport.
func TestCrossProviderNamespaces_HTTPServersTransport(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantServiceDropped bool
}{
{
desc: "nil: cross-provider ServersTransport ref is accepted (backward compatible)",
crossProviderNamespaces: nil,
},
{
desc: "empty list: cross-provider ServersTransport ref is rejected, service is dropped",
crossProviderNamespaces: []string{},
wantServiceDropped: true,
},
{
desc: "namespace allowed: cross-provider ServersTransport ref is accepted",
crossProviderNamespaces: []string{"default"},
},
{
desc: "namespace not allowed: cross-provider ServersTransport ref is rejected, service is dropped",
crossProviderNamespaces: []string{"other"},
wantServiceDropped: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_servers_transport_cross_provider.yml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
crdClient := traefikcrdfake.NewClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll(nil, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{
AllowCrossNamespace: true,
CrossProviderNamespaces: test.crossProviderNamespaces,
}
conf := p.loadConfigurationFromCRD(t.Context(), client)
service, ok := conf.HTTP.Services["default-test-route-6b204d94623b3df4370c"]
if test.wantServiceDropped {
assert.False(t, ok)
return
}
require.True(t, ok)
require.NotNil(t, service.LoadBalancer)
assert.Equal(t, "foo@file", service.LoadBalancer.ServersTransport)
})
}
}
func TestExternalNameService(t *testing.T) {
testCases := []struct {
desc string
@@ -36,16 +36,16 @@ spec:
group: ""
allowedRoutes:
kinds:
- kind: TCPRoute
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tcp-app-1
name: tls-app-1
namespace: default
spec:
parentRefs:
+16 -1
View File
@@ -161,7 +161,18 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
router.Service = errWrrName
case len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef):
router.Service = string(routeRule.BackendRefs[0].Name)
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
condition = metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load HTTPRoute BackendRef %s: internal service reference is not allowed: HTTPRoute namespace %q is not in crossProviderNamespaces", routeRule.BackendRefs[0].Name, route.Namespace),
}
} else {
router.Service = string(routeRule.BackendRefs[0].Name)
}
default:
var serviceCondition *metav1.Condition
@@ -312,6 +323,10 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
// Support for cross-provider references (e.g: api@internal).
// This provides the same behavior as for IngressRoutes.
if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, namespace) {
return "", nil, fmt.Errorf("TraefikService %q reference is not allowed: namespace %q is not in crossProviderNamespaces", string(backendRef.Name), namespace)
}
return string(backendRef.Name), nil, nil
}
+24 -11
View File
@@ -63,17 +63,17 @@ const (
// Provider holds configurations of the provider.
type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
// groupKindFilterFuncs is the list of allowed Group and Kinds for the Filter ExtensionRef objects.
groupKindFilterFuncs map[string]map[string]BuildFilterFunc
@@ -183,6 +183,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
if p.CrossProviderNamespaces != nil {
logger.Warn().Msgf("Cross-provider references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := p.client.WatchAll(p.Namespaces, ctxPool.Done())
@@ -1238,6 +1242,15 @@ func isInternalService(ref gatev1.BackendRef) bool {
return isTraefikService(ref) && strings.HasSuffix(string(ref.Name), "@internal")
}
// isCrossProviderNamespaceAllowed reports whether the given namespace is allowed to use cross-provider references.
func isCrossProviderNamespaceAllowed(allowList []string, namespace string) bool {
if allowList == nil {
return true
}
return slices.Contains(allowList, namespace)
}
// makeListenerKey joins protocol, hostname, and port of a listener into a string key.
func makeListenerKey(l gatev1.Listener) string {
var hostname gatev1.Hostname
@@ -2,9 +2,11 @@ package gateway
import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -5158,9 +5160,53 @@ func TestLoadTLSRoutes(t *testing.T) {
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
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("*")`,
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: "service@file",
Weight: ptr.To(1),
},
{
Name: "default-whoamitcp-9000",
Weight: ptr.To(1),
},
},
},
},
"default-whoamitcp-9000": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:9000",
},
{
Address: "10.10.0.10:9000",
},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
@@ -5169,7 +5215,16 @@ func TestLoadTLSRoutes(t *testing.T) {
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
{
@@ -8339,3 +8394,236 @@ func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Ob
return k8sObjects, gwObjects
}
func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
testCases := []struct {
desc string
allowList []string
namespace string
want bool
}{
{desc: "nil allowList allows any namespace", allowList: nil, namespace: "ns-a", want: true},
{desc: "empty allowList denies every namespace", allowList: []string{}, namespace: "ns-a", want: false},
{desc: "namespace in allowList is accepted", allowList: []string{"ns-a"}, namespace: "ns-a", want: true},
{desc: "namespace not in allowList is rejected", allowList: []string{"ns-b"}, namespace: "ns-a", want: false},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := isCrossProviderNamespaceAllowed(test.allowList, test.namespace)
assert.Equal(t, test.want, got)
})
}
}
// TestCrossProviderNamespaces_HTTPRoute verifies that the
// CrossProviderNamespaces option gates `@otherProvider` TraefikService
// backendRefs declared on a Gateway HTTPRoute. The check is anchored on the
// HTTPRoute's namespace; when the route is rejected, the whole router is
// dropped from the dynamic configuration.
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
testCases := []struct {
desc 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},
}
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"})
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 {
// just wait for the first event
<-eventCh
}
p := Provider{
EntryPoints: map[string]Entrypoint{"web": {Address: ":80"}},
CrossProviderNamespaces: test.crossProviderNamespaces,
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
router, ok := conf.HTTP.Routers["httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3"]
require.True(t, ok)
service, ok := conf.HTTP.Services[router.Service]
require.True(t, ok)
require.NotNil(t, service.Weighted)
require.Len(t, service.Weighted.Services, 2)
var hasError bool
for _, wrrService := range service.Weighted.Services {
// Whenever a service fails to be loaded, a placeholder service is added to the WRR to server a 500 status code.
if wrrService.Status != nil && *wrrService.Status == http.StatusInternalServerError {
hasError = true
break
}
}
assert.Equal(t, test.wantError, hasError)
})
}
}
// TestCrossProviderNamespaces_TCPRoute verifies that the option also gates
// cross-provider TraefikService backendRefs declared on a Gateway TCPRoute.
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
testCases := []struct {
desc 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},
}
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"})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubeClient, gwClient)
client.experimentalChannel = true
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
// just wait for the first event
<-eventCh
}
p := Provider{
EntryPoints: map[string]Entrypoint{"tcp": {Address: ":9000"}},
CrossProviderNamespaces: test.crossProviderNamespaces,
client: client,
ExperimentalChannel: true,
}
conf := p.loadConfigurationFromGateways(t.Context())
router, ok := conf.TCP.Routers["tcproute-default-tcp-app-1-gw-default-my-gateway-ep-tcp-0-e3b0c44298fc1c149afb"]
require.True(t, ok)
service, ok := conf.TCP.Services[router.Service]
require.True(t, ok)
require.NotNil(t, service.Weighted)
require.Len(t, service.Weighted.Services, 2)
var hasError bool
for _, wrrService := range service.Weighted.Services {
if strings.Contains(wrrService.Name, "@") {
continue
}
lbService, ok := conf.TCP.Services[wrrService.Name]
require.True(t, ok)
require.NotNil(t, lbService)
require.NotNil(t, lbService.LoadBalancer)
if len(lbService.LoadBalancer.Servers) == 0 {
hasError = true
}
}
assert.Equal(t, test.wantError, hasError)
})
}
}
// TestCrossProviderNamespaces_TLSRoute verifies that the option also gates
// cross-provider TraefikService backendRefs declared on a Gateway TLSRoute.
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
testCases := []struct {
desc 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},
}
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"})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubeClient, gwClient)
client.experimentalChannel = true
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
// just wait for the first event
<-eventCh
}
p := Provider{
EntryPoints: map[string]Entrypoint{"tls": {Address: ":9000"}},
CrossProviderNamespaces: test.crossProviderNamespaces,
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
fmt.Println(conf.TCP.Routers)
router, ok := conf.TCP.Routers["tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb"]
require.True(t, ok)
service, ok := conf.TCP.Services[router.Service]
require.True(t, ok)
require.NotNil(t, service.Weighted)
require.Len(t, service.Weighted.Services, 2)
var hasError bool
for _, wrrService := range service.Weighted.Services {
if strings.Contains(wrrService.Name, "@") {
continue
}
lbService, ok := conf.TCP.Services[wrrService.Name]
require.True(t, ok)
require.NotNil(t, lbService)
require.NotNil(t, lbService.LoadBalancer)
if len(lbService.LoadBalancer.Servers) == 0 {
hasError = true
}
}
assert.Equal(t, test.wantError, hasError)
})
}
}
+19 -2
View File
@@ -136,6 +136,19 @@ func (p *Provider) loadTCPRoute(listener gatewayListener, route *gatev1alpha2.TC
routerName := makeRouterName("", routeKey)
if len(rule.BackendRefs) == 1 && isInternalService(rule.BackendRefs[0]) {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
condition = 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: internal service reference is not allowed: TCPRoute namespace %q is not in crossProviderNamespaces", rule.BackendRefs[0].Name, route.Namespace),
}
continue
}
router.Service = string(rule.BackendRefs[0].Name)
conf.TCP.Routers[routerName] = &router
continue
@@ -224,7 +237,7 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
}
if group != groupCore || kind != kindService {
name, err := p.loadTCPBackendRef(backendRef)
name, err := p.loadTCPBackendRef(route.Namespace, backendRef)
if err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -296,10 +309,14 @@ func (p *Provider) loadTCPServers(namespace string, route *gatev1alpha2.TCPRoute
return lb, nil
}
func (p *Provider) loadTCPBackendRef(backendRef gatev1.BackendRef) (string, error) {
func (p *Provider) loadTCPBackendRef(routeNamespace string, backendRef gatev1.BackendRef) (string, error) {
// Support for cross-provider references (e.g: api@internal).
// This provides the same behavior as for IngressRoutes.
if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, routeNamespace) {
return "", fmt.Errorf("TraefikService %q reference is not allowed: route namespace %q is not in crossProviderNamespaces", string(backendRef.Name), routeNamespace)
}
return string(backendRef.Name), nil
}
+14 -1
View File
@@ -152,6 +152,19 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
routerName := makeRouterName("", routeKey)
if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0]) {
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
condition = 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: internal service reference is not allowed: TLSRoute namespace %q is not in crossProviderNamespaces", routeRule.BackendRefs[0].Name, route.Namespace),
}
continue
}
router.Service = string(routeRule.BackendRefs[0].Name)
conf.TCP.Routers[routerName] = &router
continue
@@ -240,7 +253,7 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
}
if group != groupCore || kind != kindService {
name, err := p.loadTCPBackendRef(backendRef)
name, err := p.loadTCPBackendRef(route.Namespace, backendRef)
if err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@@ -0,0 +1,51 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ""
namespace: testing
spec:
rules:
- http:
paths:
- path: /bar
backend:
service:
name: service1
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
annotations:
traefik.ingress.kubernetes.io/service.serverstransport: foobar@file
spec:
ports:
- port: 80
clusterIP: 10.0.0.1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: service1-abc
namespace: testing
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
- 10.21.0.1
conditions:
ready: true
@@ -0,0 +1,53 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ""
namespace: testing
annotations:
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.options: foobar@file
spec:
rules:
- http:
paths:
- path: /bar
backend:
service:
name: service1
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIP: 10.0.0.1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: service1-abc
namespace: testing
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
- 10.21.0.1
conditions:
ready: true
@@ -51,6 +51,7 @@ type Provider struct {
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"`
CrossProviderNamespaces []string `description:"List of namespaces from which Ingresses or Services are allowed to declare Middlewares, TLSOptions, or ServersTransport references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
// Deprecated: please use DisableClusterScopeResources.
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources)." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
DisableClusterScopeResources bool `description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true"`
@@ -92,6 +93,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
}
if p.CrossProviderNamespaces != nil {
logger.Warn().Msgf("Cross-provider Middleware, TLSOption and ServersTransport references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@@ -260,6 +265,19 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
// Middlewares and TLS options always contain cross-provider references.
if rtConfig != nil && rtConfig.Router != nil && p.CrossProviderNamespaces != nil && !slices.Contains(p.CrossProviderNamespaces, ingress.Namespace) {
if len(rtConfig.Router.Middlewares) > 0 {
logger.Error().Msgf("Skipping Ingress: cross-provider middleware reference is not allowed from namespace %q", ingress.Namespace)
continue
}
if rtConfig.Router.TLS != nil && rtConfig.Router.TLS.Options != "" {
logger.Error().Msgf("Skipping Ingress: cross-provider TLS option reference is not allowed from namespace %q", ingress.Namespace)
continue
}
}
err = getCertificates(ctxIngress, ingress, client, certConfigs)
if err != nil {
logger.Error().Err(err).Msg("Error configuring TLS")
@@ -582,6 +600,10 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
}
if svcConfig.Service.ServersTransport != "" {
if p.CrossProviderNamespaces != nil && !slices.Contains(p.CrossProviderNamespaces, namespace) {
return nil, fmt.Errorf("cross-provider serversTransport reference is not allowed from namespace %q", namespace)
}
svc.LoadBalancer.ServersTransport = svcConfig.Service.ServersTransport
}
@@ -2480,6 +2480,153 @@ func generateTestFilename(desc string) string {
return filepath.Join("fixtures", strings.ReplaceAll(desc, " ", "-")+".yml")
}
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces verifies that an Ingress,
// declaring a `traefik.ingress.kubernetes.io/router.middlewares` annotation,
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
path string
wantRouter string
}{
{
desc: "Ingress with middleware annotation is kept when option is unset (backward compatible)",
crossProviderNamespaces: nil,
path: "fixtures/Ingress-with-annotations.yml",
wantRouter: "testing-bar",
},
{
desc: "Ingress with middleware annotation is dropped when option is empty",
crossProviderNamespaces: []string{},
path: "fixtures/Ingress-with-annotations.yml",
},
{
desc: "Ingress with middleware annotation is kept when its namespace is allow-listed",
crossProviderNamespaces: []string{"testing"},
path: "fixtures/Ingress-with-annotations.yml",
wantRouter: "testing-bar",
},
{
desc: "Ingress with middleware annotation is dropped when its namespace is not allow-listed",
crossProviderNamespaces: []string{"other"},
path: "fixtures/Ingress-with-annotations.yml",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock(test.path))
if test.wantRouter == "" {
assert.Empty(t, conf.HTTP.Routers)
return
}
assert.Contains(t, conf.HTTP.Routers, test.wantRouter)
})
}
}
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_TLSOptions verifies that an Ingress,
// declaring a `traefik.ingress.kubernetes.io/router.tls.options` annotation,
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_TLSOptions(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantRouter string
}{
{
desc: "Ingress with TLS options annotation is kept when option is unset (backward compatible)",
crossProviderNamespaces: nil,
wantRouter: "testing-bar",
},
{
desc: "Ingress with TLS options annotation is dropped when option is empty",
crossProviderNamespaces: []string{},
},
{
desc: "Ingress with TLS options annotation is kept when its namespace is allow-listed",
crossProviderNamespaces: []string{"testing"},
wantRouter: "testing-bar",
},
{
desc: "Ingress with TLS options annotation is dropped when its namespace is not allow-listed",
crossProviderNamespaces: []string{"other"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock("fixtures/Ingress-with-tls-options-annotation.yml"))
if test.wantRouter == "" {
assert.Empty(t, conf.HTTP.Routers)
return
}
assert.Contains(t, conf.HTTP.Routers, test.wantRouter)
assert.NotNil(t, conf.HTTP.Routers[test.wantRouter].TLS)
assert.Equal(t, "foobar@file", conf.HTTP.Routers[test.wantRouter].TLS.Options)
})
}
}
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_ServersTransport verifies that a Service referencing a cross-provider ServersTransport,
// via the `traefik.ingress.kubernetes.io/service.serverstransport` annotation,
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_ServersTransport(t *testing.T) {
testCases := []struct {
desc string
crossProviderNamespaces []string
wantService string
}{
{
desc: "Service with serversTransport annotation is kept when option is unset (backward compatible)",
crossProviderNamespaces: nil,
wantService: "testing-service1-80",
},
{
desc: "Service with serversTransport annotation is dropped when option is empty",
crossProviderNamespaces: []string{},
},
{
desc: "Service with serversTransport annotation is kept when its namespace is allow-listed",
crossProviderNamespaces: []string{"testing"},
wantService: "testing-service1-80",
},
{
desc: "Service with serversTransport annotation is dropped when its namespace is not allow-listed",
crossProviderNamespaces: []string{"other"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock("fixtures/Ingress-with-servers-transport-annotation.yml"))
if test.wantService == "" {
assert.Empty(t, conf.HTTP.Services)
assert.Empty(t, conf.HTTP.Routers)
return
}
assert.Contains(t, conf.HTTP.Services, test.wantService)
assert.Equal(t, "foobar@file", conf.HTTP.Services[test.wantService].LoadBalancer.ServersTransport)
})
}
}
func TestGetCertificates(t *testing.T) {
testIngressWithoutHostname := buildIngress(
iNamespace("testing"),