From 8447bfc71e971ed150d610d67ca261d64253695c Mon Sep 17 00:00:00 2001 From: Baptiste Mayelle Date: Tue, 9 Jun 2026 16:24:05 +0200 Subject: [PATCH] Reject cross-provider references with backendRefs.namespace --- .../httproute/invalid_cross_provider.yml | 55 +++++++++++++++ .../tcproute/invalid_cross_provider.yml | 51 ++++++++++++++ .../tlsroute/invalid_cross_provider.yml | 67 +++++++++++++++++++ pkg/provider/kubernetes/gateway/httproute.go | 11 +++ .../kubernetes/gateway/kubernetes_test.go | 36 +++++----- pkg/provider/kubernetes/gateway/tcproute.go | 11 +++ pkg/provider/kubernetes/gateway/tlsroute.go | 11 +++ 7 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 pkg/provider/kubernetes/gateway/fixtures/httproute/invalid_cross_provider.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/tcproute/invalid_cross_provider.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/tlsroute/invalid_cross_provider.yml diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/invalid_cross_provider.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/invalid_cross_provider.yml new file mode 100644 index 000000000..55bb251d3 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/invalid_cross_provider.yml @@ -0,0 +1,55 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "foo.com" + rules: + - matches: + - path: + type: Exact + value: /bar + backendRefs: + - weight: 1 + group: traefik.io + kind: TraefikService + name: service@file + namespace: bar + port: 80 + + - name: whoami + port: 80 + weight: 1 + group: "" + kind: Service diff --git a/pkg/provider/kubernetes/gateway/fixtures/tcproute/invalid_cross_provider.yml b/pkg/provider/kubernetes/gateway/fixtures/tcproute/invalid_cross_provider.yml new file mode 100644 index 000000000..b04ab7f1e --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/tcproute/invalid_cross_provider.yml @@ -0,0 +1,51 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tcp + protocol: TCP + port: 9000 + allowedRoutes: + kinds: + - kind: TCPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TCPRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tcp-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - weight: 1 + group: traefik.io + kind: TraefikService + name: service@file + namespace: bar + port: 9000 + - name: whoamitcp + port: 9000 + weight: 1 + group: "" + kind: Service diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/invalid_cross_provider.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/invalid_cross_provider.yml new file mode 100644 index 000000000..5e79dca73 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/invalid_cross_provider.yml @@ -0,0 +1,67 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: default + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t + +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls + protocol: TLS + port: 9000 + tls: + certificateRefs: + - kind: Secret + name: supersecret + group: "" + allowedRoutes: + kinds: + - kind: TLSRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TLSRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tls-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - weight: 1 + group: traefik.io + kind: TraefikService + name: service@file + namespace: bar + port: 9000 + - name: whoamitcp + port: 9000 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 088c3dc8e..af6c0f614 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -237,6 +237,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co namespace := route.Namespace if backendRef.Namespace != nil && *backendRef.Namespace != "" { namespace = string(*backendRef.Namespace) + + if strings.Contains(string(backendRef.Name), "@") { + return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name), + } + } } serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http") diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 3e6d08a88..3fdad7bff 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -8366,20 +8366,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) { func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) { testCases := []struct { desc string + fixture string crossProviderNamespaces []string wantError bool }{ - {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, - {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, - {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, - {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false}, + {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true}, + {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false}, + {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() - k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"}) + k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture}) kubeClient := kubefake.NewClientset(k8sObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...) @@ -8429,20 +8431,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) { func TestCrossProviderNamespaces_TCPRoute(t *testing.T) { testCases := []struct { desc string + fixture string crossProviderNamespaces []string wantError bool }{ - {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, - {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, - {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, - {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false}, + {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true}, + {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false}, + {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() - k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"}) + k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture}) kubeClient := kubefake.NewClientset(k8sObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...) @@ -8501,20 +8505,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) { func TestCrossProviderNamespaces_TLSRoute(t *testing.T) { testCases := []struct { desc string + fixture string crossProviderNamespaces []string wantError bool }{ - {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, - {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, - {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, - {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false}, + {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true}, + {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false}, + {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, + {desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true}, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() - k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"}) + k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture}) kubeClient := kubefake.NewClientset(k8sObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...) diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go index 3a9632a7d..2ea0a962b 100644 --- a/pkg/provider/kubernetes/gateway/tcproute.go +++ b/pkg/provider/kubernetes/gateway/tcproute.go @@ -221,6 +221,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev namespace := route.Namespace if backendRef.Namespace != nil && *backendRef.Namespace != "" { namespace = string(*backendRef.Namespace) + + if strings.Contains(string(backendRef.Name), "@") { + return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name), + } + } } serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name)) diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go index 56bfae939..fae27acec 100644 --- a/pkg/provider/kubernetes/gateway/tlsroute.go +++ b/pkg/provider/kubernetes/gateway/tlsroute.go @@ -224,6 +224,17 @@ func (p *Provider) loadTLSService(route *gatev1alpha2.TLSRoute, backendRef gatev namespace := route.Namespace if backendRef.Namespace != nil && *backendRef.Namespace != "" { namespace = string(*backendRef.Namespace) + + if strings.Contains(string(backendRef.Name), "@") { + return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name), + } + } } serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))