diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md
index d707123f2..36a959b3e 100644
--- a/docs/content/migrate/v3.md
+++ b/docs/content/migrate/v3.md
@@ -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
diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md
index 4f8f7f52c..626b89dbb 100644
--- a/docs/content/reference/install-configuration/configuration-options.md
+++ b/docs/content/reference/install-configuration/configuration-options.md
@@ -353,6 +353,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| providers.kubernetescrd.allowemptyservices | Allow the creation of services without endpoints. | false |
| providers.kubernetescrd.allowexternalnameservices | Allow ExternalName services. | false |
| providers.kubernetescrd.certauthfilepath | Kubernetes certificate authority file path (not needed for in-cluster client). | |
+| providers.kubernetescrd.crossprovidernamespaces | List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references. | |
| providers.kubernetescrd.disableclusterscoperesources | Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). | false |
| providers.kubernetescrd.endpoint | Kubernetes server endpoint (required for external cluster client). | |
| providers.kubernetescrd.ingressclass | 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
| providers.kubernetescrd.token | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
| providers.kubernetesgateway | Enables Kubernetes Gateway API provider. | false |
| providers.kubernetesgateway.certauthfilepath | Kubernetes certificate authority file path (not needed for in-cluster client). | |
+| providers.kubernetesgateway.crossprovidernamespaces | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | |
| providers.kubernetesgateway.endpoint | Kubernetes server endpoint (required for external cluster client). | |
| providers.kubernetesgateway.experimentalchannel | Toggles Experimental Channel resources support (TCPRoute, TLSRoute...). | false |
| providers.kubernetesgateway.labelselector | Kubernetes label selector to select specific GatewayClasses. | |
@@ -379,6 +381,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| providers.kubernetesingress.allowemptyservices | Allow creation of services without endpoints. | false |
| providers.kubernetesingress.allowexternalnameservices | Allow ExternalName services. | false |
| providers.kubernetesingress.certauthfilepath | Kubernetes certificate authority file path (not needed for in-cluster client). | |
+| providers.kubernetesingress.crossprovidernamespaces | List of namespaces from which Ingresses or Services are allowed to declare Middlewares, TLSOptions, or ServersTransport references. | |
| providers.kubernetesingress.disableclusterscoperesources | Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services). | false |
| providers.kubernetesingress.disableingressclasslookup | Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources). | false |
| providers.kubernetesingress.endpoint | Kubernetes server endpoint (required for external cluster client). | |
diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md
index 837801d5b..d873360dc 100644
--- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md
+++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md
@@ -65,6 +65,7 @@ providers:
| `providers.kubernetesCRD.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
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 |
| `providers.kubernetesCRD.allowCrossNamespace` | Allows the `IngressRoutes` to reference resources in namespaces other than theirs. | false | No |
| `providers.kubernetesCRD.allowExternalNameServices` | Allows the `IngressRoutes` to reference ExternalName services. | false | No |
+| `providers.kubernetesCRD.crossProviderNamespaces` | List of namespaces from which `IngressRoute`, `IngressRouteTCP`, `IngressRouteUDP`, and `TraefikService` are allowed to declare cross-provider references (e.g. `myservice@file`).
When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| `providers.kubernetesCRD.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `IngressRoute` by default.
It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB). | false | No |
| `providers.kubernetesCRD.disableClusterScopeResources` | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).
By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.
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).
This will also prevent from using the `NodePortLB` options on services. | false | No |
diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
index 1a127277a..b29193c46 100644
--- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
+++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
@@ -82,6 +82,7 @@ providers:
| `providers.kubernetesGateway.`
`statusAddress.ip` | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| `providers.kubernetesGateway.`
`statusAddress.service.namespace` | The namespace of the Kubernetes service to copy status addresses from.
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 |
| `providers.kubernetesGateway.`
`statusAddress.service.name` | The name of the Kubernetes service to copy status addresses from.
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 |
+| `providers.kubernetesGateway.crossProviderNamespaces` | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.
When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
index ecc3e4ec2..7a680e1a8 100644
--- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
+++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
@@ -45,25 +45,26 @@ which in turn creates the resulting routers, services, handlers, etc.
-| Field | Description | Default | Required |
-| :------------------------------------------------------------------ | :------------- | :------ | :------- |
-| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
-| `providers.kubernetesIngress.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
-| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesIngress.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No |
-| `providers.kubernetesIngress.labelselector` | Allow filtering on `Ingress` objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
-| `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
-| `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.hostname` | Hostname used for Kubernetes Ingress endpoints. | "" | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.ip` | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.
More information [here](#ingressendpointpublishedservice). | "" | No |
-| `providers.kubernetesIngress.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
-| `providers.kubernetesIngress.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
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 |
-| `providers.kubernetesIngress.allowExternalNameServices` | Allows the `Ingress` to reference ExternalName services. | false | No |
-| `providers.kubernetesIngress.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.
It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
+| Field | Description | Default | Required |
+|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
+| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
+| `providers.kubernetesIngress.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
+| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesIngress.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No |
+| `providers.kubernetesIngress.labelselector` | Allow filtering on `Ingress` objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
+| `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
+| `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.hostname` | Hostname used for Kubernetes Ingress endpoints. | "" | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.ip` | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.
More information [here](#ingressendpointpublishedservice). | "" | No |
+| `providers.kubernetesIngress.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
+| `providers.kubernetesIngress.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
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 |
+| `providers.kubernetesIngress.allowExternalNameServices` | Allows the `Ingress` to reference ExternalName services. | false | No |
+| `providers.kubernetesIngress.crossProviderNamespaces` | 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.
When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
+| `providers.kubernetesIngress.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.
It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
| `providers.kubernetesIngress.disableClusterScopeResources` | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).
By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.
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).
This will also prevent from using the `NodePortLB` options on services. | false | No |
-| `providers.kubernetesIngress.strictPrefixMatching` | 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 |
+| `providers.kubernetesIngress.strictPrefixMatching` | 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 |
diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_middleware_crossprovider.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_middleware_cross_provider.yml
similarity index 100%
rename from pkg/provider/kubernetes/crd/fixtures/tcp/with_middleware_crossprovider.yml
rename to pkg/provider/kubernetes/crd/fixtures/tcp/with_middleware_cross_provider.yml
diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_cross_provider.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_cross_provider.yml
new file mode 100644
index 000000000..c792f643f
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_cross_provider.yml
@@ -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
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_middleware_crossprovider.yml b/pkg/provider/kubernetes/crd/fixtures/with_middleware_cross_provider.yml
similarity index 100%
rename from pkg/provider/kubernetes/crd/fixtures/with_middleware_crossprovider.yml
rename to pkg/provider/kubernetes/crd/fixtures/with_middleware_cross_provider.yml
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport_cross_provider.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport_cross_provider.yml
new file mode 100644
index 000000000..08448e05b
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport_cross_provider.yml
@@ -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
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_service_cross_provider.yml b/pkg/provider/kubernetes/crd/fixtures/with_service_cross_provider.yml
new file mode 100644
index 000000000..1db35d219
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_service_cross_provider.yml
@@ -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
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_option_cross_provider.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_option_cross_provider.yml
new file mode 100644
index 000000000..3fba6f758
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_option_cross_provider.yml
@@ -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
diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index ea459188c..e650e13eb 100644
--- a/pkg/provider/kubernetes/crd/kubernetes.go
+++ b/pkg/provider/kubernetes/crd/kubernetes.go
@@ -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
+}
diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go
index b728ce1e3..8d1625b3e 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_http.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_http.go
@@ -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
diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go
index d8ac3fa39..d62e13edb 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go
@@ -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)
diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go
index c6d306c4b..403f33fb0 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_test.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_test.go
@@ -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
diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/simple_cross_provider.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/simple_cross_provider.yml
index 636673c68..a07865563 100644
--- a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/simple_cross_provider.yml
+++ b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/simple_cross_provider.yml
@@ -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:
diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go
index 00f58facf..a8be580f6 100644
--- a/pkg/provider/kubernetes/gateway/httproute.go
+++ b/pkg/provider/kubernetes/gateway/httproute.go
@@ -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
}
diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go
index 7910dc453..629178731 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes.go
@@ -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
diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go
index c89660142..ccf623eb0 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes_test.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go
@@ -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)
+ })
+ }
+}
diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go
index e75b2ebdb..3a9632a7d 100644
--- a/pkg/provider/kubernetes/gateway/tcproute.go
+++ b/pkg/provider/kubernetes/gateway/tcproute.go
@@ -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
}
diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go
index d1fa8169b..9b660d449 100644
--- a/pkg/provider/kubernetes/gateway/tlsroute.go
+++ b/pkg/provider/kubernetes/gateway/tlsroute.go
@@ -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),
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-servers-transport-annotation.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-servers-transport-annotation.yml
new file mode 100644
index 000000000..61bbe2b8d
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-servers-transport-annotation.yml
@@ -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
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-tls-options-annotation.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-tls-options-annotation.yml
new file mode 100644
index 000000000..f1ca207cc
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-tls-options-annotation.yml
@@ -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
+
diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go
index 7d670ce22..055295681 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes.go
@@ -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
}
diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index 7be35de78..44b8a3a7d 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go
@@ -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"),