From 4d9031bdb2efafebc746429102ec8ccf617c7eec Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 18 May 2026 15:06:09 +0200 Subject: [PATCH 01/32] Add error on basic auth build if users is empty --- docs/content/migration/v2.md | 10 +++++++++- pkg/middlewares/auth/basic_auth.go | 4 ++++ pkg/middlewares/auth/basic_auth_test.go | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index c2a154e1e6..69baa2c9a9 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -826,7 +826,7 @@ environment variable to override the API version used by Traefik. ## v2.11.46 -### Kubernetes providers: `crossProviderNamespaces` +### Kubernetes Providers: `crossProviderNamespaces` In `v2.11.46`, a new `crossProviderNamespaces` option is available on the Kubernetes CRD, Ingress, and Gateway providers. @@ -848,3 +848,11 @@ The behavior is as follows: Please check out the [Kubernetes CRD](../providers/kubernetes-crd.md#crossprovidernamespaces), [Kubernetes Ingress](../providers/kubernetes-ingress.md#crossprovidernamespaces), and [Kubernetes Gateway](../providers/kubernetes-gateway.md#crossprovidernamespaces) provider documentation for more details. + +## v2.11.47 + +### BasicAuth Middleware + +From version `v2.11.47` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +Previously, the middleware would be built successfully but always return a 401 status code for any request. +Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. diff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go index 198c8ca84e..dcdb1f45c8 100644 --- a/pkg/middlewares/auth/basic_auth.go +++ b/pkg/middlewares/auth/basic_auth.go @@ -41,6 +41,10 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu return nil, err } + if len(users) == 0 { + return nil, fmt.Errorf("no users found in %s", authConfig.UsersFile) + } + // To prevent timing attacks, we need to compute a hash even if the user is not found. // We assume it to be safe only when the users hashes are all from the same algorithm, // so we can pick the first one as a random hash to compute. diff --git a/pkg/middlewares/auth/basic_auth_test.go b/pkg/middlewares/auth/basic_auth_test.go index baabfba195..b7dd770133 100644 --- a/pkg/middlewares/auth/basic_auth_test.go +++ b/pkg/middlewares/auth/basic_auth_test.go @@ -14,6 +14,15 @@ import ( "github.com/traefik/traefik/v2/pkg/testhelpers" ) +func TestNewBasicEmpty(t *testing.T) { + auth := dynamic.BasicAuth{ + Users: []string{}, + } + + _, err := NewBasic(t.Context(), nil, auth, "authName") + require.Error(t, err) +} + func TestNewBasicNotFoundSecretIsSet(t *testing.T) { auth := dynamic.BasicAuth{ Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"}, From f9d9b72380856c2ea579171053e418ea6db2ea9a Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 27 May 2026 16:32:10 +0200 Subject: [PATCH 02/32] Avoid ingress path matcher injection and backport 11d251415 --- .../rawdata-ingress-label-selector.json | 2 +- integration/testdata/rawdata-ingress.json | 8 +- .../testdata/rawdata-ingressclass.json | 2 +- ...nvalid-pathmatcher-annotation_endpoint.yml | 11 ++ ...invalid-pathmatcher-annotation_ingress.yml | 18 +++ ...invalid-pathmatcher-annotation_service.yml | 10 ++ pkg/provider/kubernetes/ingress/kubernetes.go | 27 +++- .../kubernetes/ingress/kubernetes_test.go | 150 ++++++++++-------- 8 files changed, 151 insertions(+), 77 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_service.yml diff --git a/integration/testdata/rawdata-ingress-label-selector.json b/integration/testdata/rawdata-ingress-label-selector.json index 101e829251..6863c50af2 100644 --- a/integration/testdata/rawdata-ingress-label-selector.json +++ b/integration/testdata/rawdata-ingress-label-selector.json @@ -33,7 +33,7 @@ "web" ], "service": "default-whoami-http", - "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", + "rule": "Host(\"whoami.test\") \u0026\u0026 PathPrefix(\"/whoami\")", "status": "enabled", "using": [ "web" diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 564a15e20c..f6edb43844 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -33,7 +33,7 @@ "web" ], "service": "default-whoami-http", - "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", + "rule": "Host(\"whoami.test.https\") \u0026\u0026 PathPrefix(\"/whoami\")", "status": "enabled", "using": [ "web" @@ -44,7 +44,7 @@ "web" ], "service": "default-whoami-http", - "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", + "rule": "Host(\"whoami.test\") \u0026\u0026 PathPrefix(\"/whoami\")", "status": "enabled", "using": [ "web" @@ -55,7 +55,7 @@ "web" ], "service": "default-whoami-80", - "rule": "Host(`whoami.test.drop`) \u0026\u0026 PathPrefix(`/drop`)", + "rule": "Host(\"whoami.test.drop\") \u0026\u0026 PathPrefix(\"/drop\")", "status": "enabled", "using": [ "web" @@ -66,7 +66,7 @@ "web" ], "service": "default-whoami-80", - "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "rule": "Host(\"whoami.test.keep\") \u0026\u0026 PathPrefix(\"/keep\")", "status": "enabled", "using": [ "web" diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json index 952bc95b2d..448a9af2cc 100644 --- a/integration/testdata/rawdata-ingressclass.json +++ b/integration/testdata/rawdata-ingressclass.json @@ -33,7 +33,7 @@ "web" ], "service": "default-whoami-80", - "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "rule": "Host(\"whoami.test.keep\") \u0026\u0026 PathPrefix(\"/keep\")", "status": "enabled", "using": [ "web" diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_endpoint.yml new file mode 100644 index 0000000000..6ed60d79ce --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_ingress.yml new file mode 100644 index 0000000000..2aac8eaf69 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_ingress.yml @@ -0,0 +1,18 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: 'Host("injection") || PathPrefix' +spec: + rules: + - http: + paths: + - path: /bar + pathType: ImplementationSpecific + backend: + service: + name: service1 + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_service.yml new file mode 100644 index 0000000000..0ec7e2269c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 42c7aae1d1..bf62999d9d 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -283,7 +283,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } rt := &dynamic.Router{ - Rule: "PathPrefix(`/`)", + Rule: `PathPrefix("/")`, Priority: math.MinInt32, Service: "default-backend", } @@ -349,7 +349,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString) conf.HTTP.Services[serviceName] = service - rt := loadRouter(rule, pa, rtConfig, serviceName) + rt, err := loadRouter(rule, pa, rtConfig, serviceName) + if err != nil { + log.FromContext(ctxIngress). + WithField("serviceName", pa.Backend.Service.Name). + WithField("path", pa.Path). + Errorf("Skipping path: %s", err) + continue + } p.applyRouterTransform(ctxIngress, rt, ingress) @@ -447,10 +454,10 @@ func (p *Provider) shouldProcessIngress(ingress *netv1.Ingress, ingressClasses [ func buildHostRule(host string) string { if strings.HasPrefix(host, "*.") { - return "HostRegexp(`" + strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1) + "`)" + return fmt.Sprintf("HostRegexp(%q)", strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1)) } - return "Host(`" + host + "`)" + return fmt.Sprintf("Host(%q)", host) } func getCertificates(ctx context.Context, ingress *netv1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { @@ -693,7 +700,7 @@ func makeRouterKeyWithHash(key, rule string) (string, error) { return dupKey, nil } -func loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router { +func loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) (*dynamic.Router, error) { var rules []string if len(rule.Host) > 0 { rules = []string{buildHostRule(rule.Host)} @@ -704,13 +711,19 @@ func loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *Rout if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == netv1.PathTypeImplementationSpecific { if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { + switch rtConfig.Router.PathMatcher { + case "Path", "PathPrefix": + default: + return nil, fmt.Errorf("invalid router path matcher %q: must be one of Path, PathPrefix", rtConfig.Router.PathMatcher) + } + matcher = rtConfig.Router.PathMatcher } } else if *pa.PathType == netv1.PathTypeExact { matcher = "Path" } - rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path)) + rules = append(rules, fmt.Sprintf("%s(%q)", matcher, pa.Path)) } rt := &dynamic.Router{ @@ -728,7 +741,7 @@ func loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *Rout } } - return rt + return rt, nil } func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index c02b308b41..f7cf748d6f 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -60,7 +60,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -90,7 +90,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, EntryPoints: []string{"ep1", "ep2"}, Service: "testing-service1-80", Middlewares: []string{"md1", "md2"}, @@ -145,11 +145,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, "testing-foo": { - Rule: "PathPrefix(`/foo`)", + Rule: `PathPrefix("/foo")`, Service: "testing-service1-80", }, }, @@ -178,12 +178,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { HTTP: &dynamic.HTTPConfiguration{ Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ - "testing-bar-bar-3be6cfd7daba66cf2fdd": { - Rule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar`) && PathPrefix(`/bar`)", + "testing-bar-bar-19f852c6ac4fff6a1896": { + Rule: `HostRegexp("{subdomain:[a-zA-Z0-9-]+}.bar") && PathPrefix("/bar")`, Service: "testing-service1-80", }, - "testing-bar-bar-636bf36c00fedaab3d44": { - Rule: "Host(`bar`) && PathPrefix(`/bar`)", + "testing-bar-bar-605945111a3c9f84dc65": { + Rule: `Host("bar") && PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -212,12 +212,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { HTTP: &dynamic.HTTPConfiguration{ Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ - "testing-foo-bar-d0b30949e54d6a7515ca": { - Rule: "PathPrefix(`/foo/bar`)", + "testing-foo-bar-207cc2245cb31ba18e29": { + Rule: `PathPrefix("/foo-bar")`, Service: "testing-service1-80", }, - "testing-foo-bar-dcd54bae39a6d7557f48": { - Rule: "PathPrefix(`/foo-bar`)", + "testing-foo-bar-930f0e8b221e60bc7ab7": { + Rule: `PathPrefix("/foo/bar")`, Service: "testing-service1-80", }, }, @@ -247,11 +247,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, "testing-foo": { - Rule: "PathPrefix(`/foo`)", + Rule: `PathPrefix("/foo")`, Service: "testing-service1-80", }, }, @@ -281,7 +281,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -311,7 +311,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-example-com": { - Rule: "Host(`example.com`)", + Rule: `Host("example.com")`, Service: "testing-example-com-80", }, }, @@ -338,11 +338,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, "testing-traefik-tchouk-foo": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/foo`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/foo")`, Service: "testing-service1-80", }, }, @@ -372,11 +372,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, "testing-traefik-courgette-carotte": { - Rule: "Host(`traefik.courgette`) && PathPrefix(`/carotte`)", + Rule: `Host("traefik.courgette") && PathPrefix("/carotte")`, Service: "testing-service1-80", }, }, @@ -406,11 +406,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, "testing-traefik-courgette-carotte": { - Rule: "Host(`traefik.courgette`) && PathPrefix(`/carotte`)", + Rule: `Host("traefik.courgette") && PathPrefix("/carotte")`, Service: "testing-service2-8082", }, }, @@ -454,7 +454,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -511,7 +511,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", + Rule: `PathPrefix("/")`, Service: "default-backend", Priority: math.MinInt32, }, @@ -542,7 +542,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -572,7 +572,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-tchouk", }, }, @@ -602,7 +602,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-tchouk", }, }, @@ -632,11 +632,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-tchouk", }, "testing-traefik-tchouk-foo": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/foo`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/foo")`, Service: "testing-service1-carotte", }, }, @@ -679,7 +679,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-tchouk", }, }, @@ -709,11 +709,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-tchouk", }, "toto-toto-traefik-tchouk-bar": { - Rule: "Host(`toto.traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("toto.traefik.tchouk") && PathPrefix("/bar")`, Service: "toto-service1-tchouk", }, }, @@ -778,7 +778,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-port-port": { - Rule: "Host(`traefik.port`) && PathPrefix(`/port`)", + Rule: `Host("traefik.port") && PathPrefix("/port")`, Service: "testing-service1-8080", }, }, @@ -805,7 +805,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-example-com": { - Rule: "Host(`example.com`)", + Rule: `Host("example.com")`, Service: "testing-example-com-80", TLS: &dynamic.RouterTLSConfig{}, }, @@ -843,7 +843,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-443", }, }, @@ -873,7 +873,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-8443", }, }, @@ -904,7 +904,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-8443", }, }, @@ -934,7 +934,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", + Rule: `PathPrefix("/")`, Service: "default-backend", Priority: math.MinInt32, }, @@ -965,7 +965,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1039,7 +1039,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-foobar-com-bar": { - Rule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.foobar.com`) && PathPrefix(`/bar`)", + Rule: `HostRegexp("{subdomain:[a-zA-Z0-9-]+}.foobar.com") && PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1069,7 +1069,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1097,11 +1097,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-foo": { - Rule: "PathPrefix(`/foo`)", + Rule: `PathPrefix("/foo")`, Service: "testing-service1-80", }, "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1129,7 +1129,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1157,7 +1157,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1185,7 +1185,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1213,7 +1213,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1241,7 +1241,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1281,7 +1281,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1310,7 +1310,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-foo": { - Rule: "PathPrefix(`/foo`)", + Rule: `PathPrefix("/foo")`, Service: "testing-service1-80", }, }, @@ -1338,7 +1338,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1366,7 +1366,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1394,7 +1394,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1422,7 +1422,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1450,7 +1450,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "Path(`/bar`)", + Rule: `Path("/bar")`, Service: "testing-service1-80", }, }, @@ -1478,7 +1478,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1506,7 +1506,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1534,7 +1534,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-80", }, }, @@ -1562,7 +1562,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service1-foobar", }, }, @@ -1602,7 +1602,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", + Rule: `PathPrefix("/")`, Priority: math.MinInt32, Service: "default-backend", }, @@ -1622,6 +1622,28 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "Ingress with invalid pathmatcher annotation", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{}, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: pointer(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases { @@ -1692,7 +1714,7 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-8080", }, }, @@ -1719,7 +1741,7 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-example-com-bar": { - Rule: "PathPrefix(`/bar`)", + Rule: `PathPrefix("/bar")`, Service: "testing-service-bar-8080", }, }, @@ -1747,7 +1769,7 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-example-com-foo": { - Rule: "PathPrefix(`/foo`)", + Rule: `PathPrefix("/foo")`, Service: "testing-service-foo-8080", }, }, @@ -1825,7 +1847,7 @@ func TestLoadConfigurationFromIngressesWithNativeLB(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Rule: `Host("traefik.tchouk") && PathPrefix("/bar")`, Service: "testing-service1-8080", }, }, From 2c03850b907773f996627ab7c70f75f16d78a315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lemay?= Date: Thu, 28 May 2026 03:38:12 -0400 Subject: [PATCH 03/32] Capitalize NGINX in kubernetesIngressNGINX --- docs/content/migrate/nginx-to-traefik.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/migrate/nginx-to-traefik.md b/docs/content/migrate/nginx-to-traefik.md index 2c5e1c283f..ae4eb4cf85 100644 --- a/docs/content/migrate/nginx-to-traefik.md +++ b/docs/content/migrate/nginx-to-traefik.md @@ -170,7 +170,7 @@ helm repo update ```bash helm upgrade --install traefik traefik/traefik \ --namespace traefik --create-namespace \ - --set providers.kubernetesIngressNginx.enabled=true + --set providers.kubernetesIngressNGINX.enabled=true ``` Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/VALUES.md) for more configuration: @@ -178,7 +178,7 @@ Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/mast ```yaml tab="traefik-values.yaml" ... providers: - kubernetesIngressNginx: + kubernetesIngressNGINX: enabled: true ... ``` From 5026ca97d0266e2ad260947cfb22c3f5453fa673 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Thu, 28 May 2026 10:30:07 +0200 Subject: [PATCH 04/32] Move snicheck to ctx instead of simulated routing --- .../fixtures/https/https_domain_fronting.toml | 47 ++- integration/https_test.go | 61 ++-- integration/simple_test.go | 2 +- integration/try/try.go | 6 +- pkg/config/dynamic/http_config.go | 7 +- pkg/middlewares/snicheck/snicheck.go | 101 ++---- pkg/middlewares/snicheck/snicheck_test.go | 59 ---- pkg/muxer/tcp/mux.go | 6 +- pkg/muxer/tcp/mux_test.go | 2 +- pkg/server/aggregator.go | 96 +++++- pkg/server/router/router.go | 7 + pkg/server/router/tcp/manager.go | 107 ++----- pkg/server/router/tcp/manager_test.go | 300 +----------------- pkg/server/router/tcp/router.go | 51 ++- pkg/server/router/tcp/router_test.go | 21 +- pkg/server/server_entrypoint_tcp.go | 10 + pkg/server/server_entrypoint_tcp_http3.go | 43 ++- .../server_entrypoint_tcp_http3_test.go | 4 +- pkg/tcp/tls.go | 29 +- 19 files changed, 362 insertions(+), 597 deletions(-) delete mode 100644 pkg/middlewares/snicheck/snicheck_test.go diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml index 80011ee8ed..7d34884f84 100644 --- a/integration/fixtures/https/https_domain_fronting.toml +++ b/integration/fixtures/https/https_domain_fronting.toml @@ -7,6 +7,10 @@ [entryPoints.websecure] address = ":4443" + [entryPoints.websecure.http3] + +[experimental] + http3 = true [api] insecure = true @@ -32,6 +36,35 @@ [http.routers.router3.tls] options = "mytls" +[http.routers.router4] + rule = "Host(`site4.www.snitest.com`)" + service = "service4" + [http.routers.router4.tls] + +[http.routers.router4path] + rule = "Host(`site4.www.snitest.com`) && PathPrefix(`/foo`)" + service = "service4" + [http.routers.router4path.tls] + options = "mytls" + +[http.routers.router5] + rule = "Host(`site5.www.snitest.com`)" + service = "service5" + [http.routers.router5.tls] + options = "mytls" + +[http.routers.router5path] + rule = "Host(`site5.www.snitest.com`) && PathPrefix(`/bar`)" + service = "service5" + [http.routers.router5path.tls] + options = "mytls" + +[http.routers.router6] + rule = "Host(`site6.www.snitest.com.`)" + service = "service6" + [http.routers.router6.tls] + options = "mytls" + [http.services.service1] [[http.services.service1.loadBalancer.servers]] url = "http://127.0.0.1:9010" @@ -44,10 +77,22 @@ [[http.services.service3.loadBalancer.servers]] url = "http://127.0.0.1:9030" +[http.services.service4] + [[http.services.service4.loadBalancer.servers]] + url = "http://127.0.0.1:9040" + +[http.services.service5] + [[http.services.service5.loadBalancer.servers]] + url = "http://127.0.0.1:9050" + +[http.services.service6] + [[http.services.service6.loadBalancer.servers]] + url = "http://127.0.0.1:9060" + [[tls.certificates]] certFile = "fixtures/https/wildcard.www.snitest.com.cert" keyFile = "fixtures/https/wildcard.www.snitest.com.key" [tls.options] [tls.options.mytls] - maxVersion = "VersionTLS12" + maxVersion = "VersionTLS13" diff --git a/integration/https_test.go b/integration/https_test.go index a0ab2c1bc3..8bdf5c1c45 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/BurntSushi/toml" + "github.com/quic-go/quic-go/http3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -254,7 +255,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() { assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion") // with unknown tls option - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS options instead", tr4.TLSClientConfig.ServerName))) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead")) require.NoError(s.T(), err) } @@ -995,19 +996,20 @@ func (s *HTTPSSuite) TestWithDomainFronting() { defer backend2.Close() backend3 := startTestServer("9030", http.StatusOK, "server3") defer backend3.Close() + backend5 := startTestServer("9050", http.StatusOK, "server5") + defer backend5.Close() file := s.adaptFile("fixtures/https/https_domain_fronting.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) // wait for Traefik - err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)")) + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)")) require.NoError(s.T(), err) testCases := []struct { desc string hostHeader string serverName string - expectedError bool expectedContent string expectedStatusCode int }{ @@ -1025,14 +1027,6 @@ func (s *HTTPSSuite) TestWithDomainFronting() { expectedContent: "server3", expectedStatusCode: http.StatusOK, }, - { - desc: "Spaces after the host header", - hostHeader: "site3.www.snitest.com ", - serverName: "site3.www.snitest.com", - expectedError: true, - expectedContent: "server3", - expectedStatusCode: http.StatusOK, - }, { desc: "Spaces after the servername", hostHeader: "site3.www.snitest.com", @@ -1040,14 +1034,6 @@ func (s *HTTPSSuite) TestWithDomainFronting() { expectedContent: "server3", expectedStatusCode: http.StatusOK, }, - { - desc: "Spaces after the servername and host header", - hostHeader: "site3.www.snitest.com ", - serverName: "site3.www.snitest.com ", - expectedError: true, - expectedContent: "server3", - expectedStatusCode: http.StatusOK, - }, { desc: "Domain Fronting with same tlsOptions should follow header", hostHeader: "site1.www.snitest.com", @@ -1083,6 +1069,34 @@ func (s *HTTPSSuite) TestWithDomainFronting() { expectedContent: "server1", expectedStatusCode: http.StatusOK, }, + { + desc: "Domain Fronting with ambiguous TLS options should produce a 421", + hostHeader: "site4.www.snitest.com", + serverName: "site3.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Domain Fronting with same non-default TLS options should not produce a 421", + hostHeader: "site5.www.snitest.com", + serverName: "site3.www.snitest.com", + expectedContent: "server5", + expectedStatusCode: http.StatusOK, + }, + { + desc: "FQDN host header with empty SNI to non-default TLS options route should produce a 421", + hostHeader: "site3.www.snitest.com.", + serverName: "", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Non-FQDN host header with empty SNI matching FQDN route rule should produce a 421", + hostHeader: "site6.www.snitest.com", + serverName: "", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, } for _, test := range testCases { @@ -1091,11 +1105,10 @@ func (s *HTTPSSuite) TestWithDomainFronting() { req.Host = test.hostHeader err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) - if test.expectedError { - assert.Error(s.T(), err) - } else { - require.NoError(s.T(), err) - } + assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err) + + err = try.RequestWithTransport(req, 500*time.Millisecond, &http3.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) + assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err) } } diff --git a/integration/simple_test.go b/integration/simple_test.go index e8938c8943..40620f8f9d 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -667,7 +667,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() { s.traefikCmd(withConfigFile(file)) // All errors - err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS options instead"]`)) + err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`)) require.NoError(s.T(), err) // router3 has an error because it uses an unknown entrypoint diff --git a/integration/try/try.go b/integration/try/try.go index 20bc8bc1ac..73a3decf71 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -76,7 +76,7 @@ func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCon // the condition on the response. // ResponseCondition may be nil, in which case only the request against the URL must // succeed. -func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error { +func RequestWithTransport(req *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) error { resp, err := doTryRequest(req, timeout, transport, conditions...) if resp != nil && resp.Body != nil { @@ -140,12 +140,12 @@ func doTryRequest(request *http.Request, timeout time.Duration, transport http.R func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) { var resp *http.Response return resp, action(timeout, func() error { - var err error - client := http.DefaultClient + var client http.Client if transport != nil { client.Transport = transport } + var err error resp, err = client.Do(request) if err != nil { return err diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index d397d23069..7d68c18dce 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -103,9 +103,10 @@ func (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} { // RouterTLSConfig holds the TLS configuration for a router. type RouterTLSConfig struct { - Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"` - CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"` - Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"` + Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"` + ResolvedOptions string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false"` + CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"` + Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/middlewares/snicheck/snicheck.go b/pkg/middlewares/snicheck/snicheck.go index e18b605cbf..d2f7015472 100644 --- a/pkg/middlewares/snicheck/snicheck.go +++ b/pkg/middlewares/snicheck/snicheck.go @@ -1,24 +1,26 @@ package snicheck import ( - "net" "net/http" - "strings" "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator" - traefiktls "github.com/traefik/traefik/v2/pkg/tls" + "github.com/traefik/traefik/v2/pkg/tcp" ) // SNICheck is an HTTP handler that checks whether the TLS configuration for the server name is the same as for the host header. type SNICheck struct { - next http.Handler - tlsOptionsForHost map[string]string + next http.Handler + routerName string + tlsOptionsName string } // New creates a new SNICheck. -func New(tlsOptionsForHost map[string]string, next http.Handler) *SNICheck { - return &SNICheck{next: next, tlsOptionsForHost: tlsOptionsForHost} +func New(routerName, tlsOptionsName string, next http.Handler) *SNICheck { + return &SNICheck{ + next: next, + routerName: routerName, + tlsOptionsName: tlsOptionsName, + } } func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -27,81 +29,16 @@ func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - host := getHost(req) - serverName := strings.TrimSpace(req.TLS.ServerName) - - // Domain Fronting - if !strings.EqualFold(host, serverName) { - tlsOptionHeader := findTLSOptionName(s.tlsOptionsForHost, host, true) - tlsOptionSNI := findTLSOptionName(s.tlsOptionsForHost, serverName, false) - - if tlsOptionHeader != tlsOptionSNI { - log.WithoutContext(). - WithField("host", host). - WithField("req.Host", req.Host). - WithField("req.TLS.ServerName", req.TLS.ServerName). - Debugf("TLS options difference: SNI:%s, Header:%s", tlsOptionSNI, tlsOptionHeader) - http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) - return - } + tlsOptionsNameUsed := tcp.GetTLSOptionsName(req.Context()) + if s.tlsOptionsName != tlsOptionsNameUsed { + log.WithoutContext(). + WithField("routerName", s.routerName). + WithField("req.Host", req.Host). + WithField("req.TLS.ServerName", req.TLS.ServerName). + Debugf("TLS options difference: SNI:%s, Header:%s", tlsOptionsNameUsed, s.tlsOptionsName) + http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) + return } s.next.ServeHTTP(rw, req) } - -func getHost(req *http.Request) string { - h := requestdecorator.GetCNAMEFlatten(req.Context()) - if h != "" { - return h - } - - h = requestdecorator.GetCanonizedHost(req.Context()) - if h != "" { - return h - } - - host, _, err := net.SplitHostPort(req.Host) - if err != nil { - host = req.Host - } - - return strings.TrimSpace(host) -} - -func findTLSOptionName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { - name := findTLSOptName(tlsOptionsForHost, host, fqdn) - if name != "" { - return name - } - - name = findTLSOptName(tlsOptionsForHost, strings.ToLower(host), fqdn) - if name != "" { - return name - } - - return traefiktls.DefaultTLSConfigName -} - -func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { - if tlsOptions, ok := tlsOptionsForHost[host]; ok { - return tlsOptions - } - - if !fqdn { - return "" - } - - if last := len(host) - 1; last >= 0 && host[last] == '.' { - if tlsOptions, ok := tlsOptionsForHost[host[:last]]; ok { - return tlsOptions - } - - return "" - } - - if tlsOptions, ok := tlsOptionsForHost[host+"."]; ok { - return tlsOptions - } - - return "" -} diff --git a/pkg/middlewares/snicheck/snicheck_test.go b/pkg/middlewares/snicheck/snicheck_test.go deleted file mode 100644 index d7411e555e..0000000000 --- a/pkg/middlewares/snicheck/snicheck_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package snicheck - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSNICheck_ServeHTTP(t *testing.T) { - testCases := []struct { - desc string - tlsOptionsForHost map[string]string - host string - expected int - }{ - { - desc: "no TLS options", - expected: http.StatusOK, - }, - { - desc: "with TLS options", - tlsOptionsForHost: map[string]string{ - "example.com": "foo", - }, - expected: http.StatusOK, - }, - { - desc: "server name and host doesn't have the same TLS configuration", - tlsOptionsForHost: map[string]string{ - "example.com": "foo", - }, - host: "example.com", - expected: http.StatusMisdirectedRequest, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) - - sniCheck := New(test.tlsOptionsForHost, next) - - req := httptest.NewRequest(http.MethodGet, "https://localhost", nil) - if test.host != "" { - req.Host = test.host - } - - recorder := httptest.NewRecorder() - - sniCheck.ServeHTTP(recorder, req) - - assert.Equal(t, test.expected, recorder.Code) - }) - } -} diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index 8302ab3408..2a4fa057cb 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -61,10 +61,10 @@ type ConnData struct { } // NewConnData builds a connData struct from the given parameters. -func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (ConnData, error) { - remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String()) +func NewConnData(serverName string, remoteAddr net.Addr, alpnProtos []string) (ConnData, error) { + remoteIP, _, err := net.SplitHostPort(remoteAddr.String()) if err != nil { - return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err) + return ConnData{}, fmt.Errorf("parsing remote address %q: %w", remoteAddr.String(), err) } // as per https://datatracker.ietf.org/doc/html/rfc6066: diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go index e82432a75b..7ac0d70b5c 100644 --- a/pkg/muxer/tcp/mux_test.go +++ b/pkg/muxer/tcp/mux_test.go @@ -532,7 +532,7 @@ func Test_addTCPRoute(t *testing.T) { remoteAddr: fakeAddr{addr: addr}, } - connData, err := NewConnData(test.serverName, conn, test.protos) + connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos) require.NoError(t, err) matchingHandler, _ := router.Match(connData) diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 28d07dda9a..2dcabdae2b 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -1,13 +1,16 @@ package server import ( + "context" + "fmt" "slices" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" + httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http" "github.com/traefik/traefik/v2/pkg/server/provider" - "github.com/traefik/traefik/v2/pkg/tls" + traefiktls "github.com/traefik/traefik/v2/pkg/tls" ) func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration { @@ -31,8 +34,8 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint Services: make(map[string]*dynamic.UDPService), }, TLS: &dynamic.TLSConfiguration{ - Stores: make(map[string]tls.Store), - Options: make(map[string]tls.Options), + Stores: make(map[string]traefiktls.Store), + Options: make(map[string]traefiktls.Options), }, } @@ -101,7 +104,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint } for key, store := range configuration.TLS.Stores { - if key != tls.DefaultTLSStoreName { + if key != traefiktls.DefaultTLSStoreName { key = provider.MakeQualifiedName(pvd, key) } else { defaultTLSStoreProviders = append(defaultTLSStoreProviders, pvd) @@ -123,19 +126,96 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint if len(defaultTLSStoreProviders) > 1 { log.WithoutContext().Errorf("Default TLS Store defined in multiple providers: %v", defaultTLSStoreProviders) - delete(conf.TLS.Stores, tls.DefaultTLSStoreName) + delete(conf.TLS.Stores, traefiktls.DefaultTLSStoreName) } if len(defaultTLSOptionProviders) == 0 { - conf.TLS.Options[tls.DefaultTLSConfigName] = tls.DefaultTLSOptions + conf.TLS.Options[traefiktls.DefaultTLSConfigName] = traefiktls.DefaultTLSOptions } else if len(defaultTLSOptionProviders) > 1 { log.WithoutContext().Errorf("Default TLS Options defined in multiple providers %v", defaultTLSOptionProviders) // We do not set an empty tls.TLS{} as above so that we actually get a "cascading failure" later on, // i.e. routers depending on this missing TLS option will fail to initialize as well. - delete(conf.TLS.Options, tls.DefaultTLSConfigName) + delete(conf.TLS.Options, traefiktls.DefaultTLSConfigName) } - return conf + return resolveHTTPTLSOptions(conf) +} + +func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration { + if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 { + return cfg + } + + rts := make(map[string]*dynamic.Router) + + // Keyed by domain, then by options reference. + // The actual source of truth for what TLS options will actually be used for the connection. + // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS + // options that occur for a given host name, so that later on we can set relevant + // errors and logging for all the routers concerned (i.e. wrongly configured). + tlsOptionsForHostSNI := map[string]map[string][]string{} + + for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers { + rts[routerHTTPName] = routerHTTPConfig.DeepCopy() + + if routerHTTPConfig.TLS == nil { + continue + } + + ctxRouter := log.With(provider.AddInContext(context.Background(), routerHTTPName), log.Str(log.RouterName, routerHTTPName)) + logger := log.FromContext(ctxRouter) + + tlsOptionsName := traefiktls.DefaultTLSConfigName + if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { + tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) + } + + domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule) + if err != nil { + routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err) + logger.Error(routerErr) + continue + } + + if len(domains) == 0 { + rts[routerHTTPName].TLS.ResolvedOptions = "default" + logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule) + } + + for _, domain := range domains { + // domain is already in lower case thanks to the domain parsing + if tlsOptionsForHostSNI[domain] == nil { + tlsOptionsForHostSNI[domain] = make(map[string][]string) + } + tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName) + } + } + + for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { + if len(tlsConfigs) == 1 { + for optionsName, v := range tlsConfigs { + log.WithoutContext().Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName) + for _, s := range v { + rts[s].TLS.ResolvedOptions = optionsName + } + } + continue + } + + // multiple tlsConfigs + routers := make([]string, 0, len(tlsConfigs)) + for _, v := range tlsConfigs { + for _, s := range v { + rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName + routers = append(routers, s) + } + } + + log.WithoutContext().Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers) + } + + cfg.HTTP.Routers = rts + return cfg } func applyModel(cfg dynamic.Configuration) dynamic.Configuration { diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 5fc2aa2b23..b6192eaee4 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -16,6 +16,7 @@ import ( "github.com/traefik/traefik/v2/pkg/middlewares/denyrouterrecursion" metricsMiddle "github.com/traefik/traefik/v2/pkg/middlewares/metrics" "github.com/traefik/traefik/v2/pkg/middlewares/recovery" + "github.com/traefik/traefik/v2/pkg/middlewares/snicheck" "github.com/traefik/traefik/v2/pkg/middlewares/tracing" httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http" "github.com/traefik/traefik/v2/pkg/server/middleware" @@ -229,6 +230,12 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn }) } + if router.TLS != nil { + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + return snicheck.New(routerName, router.TLS.ResolvedOptions, next), nil + }) + } + return chain.Extend(*mHandler).Append(tHandler).Then(sHandler) } diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index de4d94d2ec..0a17dfb272 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -2,7 +2,6 @@ package tcp import ( "context" - "crypto/tls" "errors" "fmt" "math" @@ -11,7 +10,6 @@ import ( "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/middlewares/snicheck" httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http" tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp" "github.com/traefik/traefik/v2/pkg/server/provider" @@ -91,11 +89,6 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls return make(map[string]map[string]*runtime.RouterInfo) } -type nameAndConfig struct { - routerName string // just so we have it as additional information when logging - TLSConfig *tls.Config -} - func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) { // Build a new Router. router, err := NewRouter() @@ -113,18 +106,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err) } - // Keyed by domain. The source of truth for doing SNI checking (domain fronting). - // As soon as there's (at least) two different tlsOptions found for the same domain, - // we set the value to the default TLS conf. - tlsOptionsForHost := map[string]string{} - - // Keyed by domain, then by options reference. - // The actual source of truth for what TLS options will actually be used for the connection. - // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS - // options that occur for a given host name, so that later on we can set relevant - // errors and logging for all the routers concerned (i.e. wrongly configured). - tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} - for routerHTTPName, routerHTTPConfig := range configsHTTP { if routerHTTPConfig.TLS == nil { continue @@ -133,11 +114,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName)) logger := log.FromContext(ctxRouter) - tlsOptionsName := traefiktls.DefaultTLSConfigName - if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { - tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) - } - domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule) if err != nil { routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err) @@ -152,7 +128,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string // This is only about choosing the TLS configuration. // The actual routing will be done further on by the HTTPS handler. // See examples below. - router.AddHTTPTLSConfig("*", defaultTLSConf) + router.AddHTTPTLSConfig("*", defaultTLSConf, traefiktls.DefaultTLSConfigName) // The server name (from a Host(SNI) rule) is the only parameter (available in HTTP routing rules) on which we can map a TLS config, // because it is the only one accessible before decryption (we obtain it during the ClientHello). @@ -180,79 +156,43 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule) } + // Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator + // we also have to handle it here to be able to mark the router in error. + tlsOptionsName := traefiktls.DefaultTLSConfigName + if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { + tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) + } + + if routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName { + routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false) + } + // Even though the error is seemingly ignored (aside from logging it), // we actually rely later on the fact that a tls config is nil (which happens when an error is returned) to take special steps // when assigning a handler to a route. - tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName) + tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, routerHTTPConfig.TLS.ResolvedOptions) if tlsConfErr != nil { // Note: we do not call AddError here because we already did so when buildRouterHandler errored for the same reason. logger.Error(tlsConfErr) } for _, domain := range domains { - // domain is already in lower case thanks to the domain parsing - if tlsOptionsForHostSNI[domain] == nil { - tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) - } - tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{ - routerName: routerHTTPName, - TLSConfig: tlsConf, - } - - if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName { - // Different tlsOptions on the same domain, so fallback to default - tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName - } else { - tlsOptionsForHost[domain] = tlsOptionsName - } - } - } - - sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS) - - // Keep in mind that defaultTLSConf might be nil here. - router.SetHTTPSHandler(sniCheck, defaultTLSConf) - - logger := log.FromContext(ctx) - for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { - if len(tlsConfigs) == 1 { - var optionsName string - var config *tls.Config - for k, v := range tlsConfigs { - optionsName = k - config = v.TLSConfig - break - } - - if config == nil { + if tlsConf == nil { // we use nil config as a signal to insert a handler // that enforces that TLS connection attempts to the corresponding (broken) router should fail. - logger.Debugf("Adding special closing route for %s because broken TLS options %s", hostSNI, optionsName) - router.AddHTTPTLSConfig(hostSNI, nil) + logger.Debugf("Adding special closing route for %s because of a broken TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions) + router.AddHTTPTLSConfig(domain, nil, "") continue } - logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName) - router.AddHTTPTLSConfig(hostSNI, config) - continue + logger.Debugf("Adding route for %s with TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions) + router.AddHTTPTLSConfig(domain, tlsConf, routerHTTPConfig.TLS.ResolvedOptions) } - - // multiple tlsConfigs - - routers := make([]string, 0, len(tlsConfigs)) - for _, v := range tlsConfigs { - configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false) - routers = append(routers, v.routerName) - } - - logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers) - if defaultTLSConf == nil { - logger.Debugf("Adding special closing route for %s because broken default TLS options", hostSNI) - } - - router.AddHTTPTLSConfig(hostSNI, defaultTLSConf) } + // Keep in mind that defaultTLSConf might be nil here. + router.SetHTTPSHandler(handlerHTTPS, defaultTLSConf) + m.addTCPHandlers(ctx, configs, router) return router, nil @@ -385,8 +325,9 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim } handler = &tcp.TLSHandler{ - Next: handler, - Config: tlsConf, + Next: handler, + Config: tlsConf, + TLSOptionsName: tlsOptionsName, } logger.Debugf("Adding TLS route for %q", routerConfig.Rule) diff --git a/pkg/server/router/tcp/manager_test.go b/pkg/server/router/tcp/manager_test.go index d005f1b0e2..a24f58424f 100644 --- a/pkg/server/router/tcp/manager_test.go +++ b/pkg/server/router/tcp/manager_test.go @@ -1,14 +1,10 @@ package tcp import ( - "crypto/tls" "math" - "net/http" - "net/http/httptest" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp" @@ -129,7 +125,8 @@ func TestRuntimeConfiguration(t *testing.T) { Service: "foo-service", Rule: "Host(`bar.foo`)", TLS: &dynamic.RouterTLSConfig{ - Options: "foo", + Options: "foo", + ResolvedOptions: "default", }, }, }, @@ -139,7 +136,8 @@ func TestRuntimeConfiguration(t *testing.T) { Service: "foo-service", Rule: "Host(`bar.foo`) && PathPrefix(`/path`)", TLS: &dynamic.RouterTLSConfig{ - Options: "bar", + Options: "bar", + ResolvedOptions: "default", }, }, }, @@ -396,293 +394,3 @@ func TestRuntimeConfiguration(t *testing.T) { }) } } - -func TestDomainFronting(t *testing.T) { - tlsOptionsBase := map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS10", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - "host1@crd": { - MinVersion: "VersionTLS12", - }, - } - - entryPoints := []string{"web"} - - tests := []struct { - desc string - routers map[string]*runtime.RouterInfo - tlsOptions map[string]traefiktls.Options - host string - ServerName string - expectedStatus int - }{ - { - desc: "Request is misdirected when TLS options are different", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{}, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is OK when TLS options are the same", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Default TLS options is used when options are ambiguous for the same host", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`) && PathPrefix(`/foo`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "default", - }, - }, - }, - "router-3@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Default TLS options should not be used when options are the same for the same host", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`) && PathPrefix(`/bar`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-3@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Request is misdirected when TLS options have the same name but from different providers", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@crd": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is OK when TLS options reference from a different provider is the same", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@crd", - }, - }, - }, - "router-2@crd": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@crd", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Request is misdirected when server name is empty and the host name is an FQDN, but router's rule is not", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@file", - }, - }, - }, - }, - tlsOptions: map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS13", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - }, - host: "host1.local.", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is misdirected when server name is empty and the host name is not FQDN, but router's rule is", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local.`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@file", - }, - }, - }, - }, - tlsOptions: map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS13", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - }, - host: "host1.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - } - - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - conf := &runtime.Configuration{ - Routers: test.routers, - } - - serviceManager := tcp.NewManager(conf) - - tlsManager := traefiktls.NewManager() - tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{}) - - httpsHandler := map[string]http.Handler{ - "web": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}), - } - - middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares) - - routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager) - - routers := routerManager.BuildHandlers(t.Context(), entryPoints) - - router, ok := routers["web"] - require.True(t, ok) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Host = test.host - req.TLS = &tls.ConnectionState{ - ServerName: test.ServerName, - } - - rw := httptest.NewRecorder() - - router.GetHTTPSHandler().ServeHTTP(rw, req) - - assert.Equal(t, test.expectedStatus, rw.Code) - }) - } -} diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 23f624c0c1..c40f64eae5 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -17,11 +17,17 @@ import ( "github.com/traefik/traefik/v2/pkg/log" tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp" "github.com/traefik/traefik/v2/pkg/tcp" + traefiktls "github.com/traefik/traefik/v2/pkg/tls" ) // errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello. var errClientHelloRead = errors.New("client hello successfully read") +type tlsConfigWithOptionsName struct { + cfg *tls.Config + optionsName string +} + // Router is a TCP router. type Router struct { acmeTLSPassthrough bool @@ -48,7 +54,7 @@ type Router struct { httpsTLSConfig *tls.Config // default TLS config // hostHTTPTLSConfig contains TLS configs keyed by SNI. // A nil config is the hint to set up a brokenTLSRouter. - hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI + hostHTTPTLSConfig map[string]tlsConfigWithOptionsName // TLS configs keyed by SNI } // NewRouter returns a new TCP router. @@ -75,14 +81,20 @@ func NewRouter() (*Router, error) { }, nil } -// GetTLSGetClientInfo is called after a ClientHello is received from a client. -func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) { - return func(info *tls.ClientHelloInfo) (*tls.Config, error) { - if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok { - return tlsConfig, nil +// HTTP3TLSConfigMatcherFunc returns a matcher func for HTTP/3 which returns a tls.Config with its corresponding +// TLSOptionName matching the given HostSNI in the connection data, or the default TLS config if there is no match. +func (r *Router) HTTP3TLSConfigMatcherFunc() func(connData tcpmuxer.ConnData) (*tls.Config, string, error) { + return func(connData tcpmuxer.ConnData) (*tls.Config, string, error) { + h, _ := r.muxerHTTPS.Match(connData) + if h == nil { + return r.httpsTLSConfig, traefiktls.DefaultTLSConfigName, nil } - return r.httpsTLSConfig, nil + if tlsHandler, ok := h.(*tcp.TLSHandler); ok { + return tlsHandler.Config, tlsHandler.TLSOptionsName, nil + } + + return nil, "", errors.New("matching handler is not a TLSHandler") } } @@ -94,7 +106,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { // we would block forever on clientHelloInfo, // which is why we want to detect and handle that case first and foremost. if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() { - connData, err := tcpmuxer.NewConnData("", conn, nil) + connData, err := tcpmuxer.NewConnData("", conn.RemoteAddr(), nil) if err != nil { log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err) conn.Close() @@ -136,7 +148,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { log.WithoutContext().Errorf("Error while setting deadline: %v", err) } - connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) + connData, err := tcpmuxer.NewConnData(hello.serverName, conn.RemoteAddr(), hello.protos) if err != nil { log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err) conn.Close() @@ -212,12 +224,15 @@ func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error { } // AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig. -func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) { +func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config, optionsName string) { if r.hostHTTPTLSConfig == nil { - r.hostHTTPTLSConfig = map[string]*tls.Config{} + r.hostHTTPTLSConfig = map[string]tlsConfigWithOptionsName{} } - r.hostHTTPTLSConfig[sniHost] = config + r.hostHTTPTLSConfig[sniHost] = tlsConfigWithOptionsName{ + cfg: config, + optionsName: optionsName, + } } // GetConn creates a connection proxy with a peeked string. @@ -262,12 +277,13 @@ func (t *brokenTLSRouter) ServeTCP(conn tcp.WriteCloser) { func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { for sniHost, tlsConf := range r.hostHTTPTLSConfig { var tcpHandler tcp.Handler - if tlsConf == nil { + if tlsConf.cfg == nil { tcpHandler = &brokenTLSRouter{} } else { tcpHandler = &tcp.TLSHandler{ - Next: handler, - Config: tlsConf, + Next: handler, + Config: tlsConf.cfg, + TLSOptionsName: tlsConf.optionsName, } } @@ -285,8 +301,9 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { } r.httpsForwarder = &tcp.TLSHandler{ - Next: handler, - Config: r.httpsTLSConfig, + Next: handler, + Config: r.httpsTLSConfig, + TLSOptionsName: "default", } } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index e12a564610..79ca37c438 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -2,6 +2,7 @@ package tcp import ( "bytes" + "context" "crypto/tls" "errors" "fmt" @@ -20,7 +21,7 @@ import ( "github.com/traefik/traefik/v2/pkg/config/runtime" tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp" "github.com/traefik/traefik/v2/pkg/server/service/tcp" - tcp2 "github.com/traefik/traefik/v2/pkg/tcp" + traefiktcp "github.com/traefik/traefik/v2/pkg/tcp" traefiktls "github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls/generate" ) @@ -52,7 +53,7 @@ func (h *httpForwarder) Close() error { } // ServeTCP uses the connection to serve it later in "Accept". -func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) { +func (h *httpForwarder) ServeTCP(conn traefiktcp.WriteCloser) { h.connChan <- conn } @@ -621,6 +622,16 @@ func Test_Routing(t *testing.T) { _, err = fmt.Fprint(w, "HTTPS") require.NoError(t, err) }), + + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + if tlsConn, ok := c.(*tls.Conn); ok { + if tlsConnWithOptionsName, ok := tlsConn.NetConn().(traefiktcp.TLSConn); ok { + return traefiktcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName) + } + } + + return ctx + }, } stoppedHTTPS := make(chan struct{}) @@ -812,7 +823,8 @@ func routerHTTPSPathPrefix(conf *runtime.Configuration) { Service: "http", Rule: "PathPrefix(`/`)", TLS: &dynamic.RouterTLSConfig{ - Options: "tls10", + Options: "tls10", + ResolvedOptions: "tls10", }, }, } @@ -826,7 +838,8 @@ func routerHTTPS(conf *runtime.Configuration) { Service: "http", Rule: "Host(`foo.bar`)", TLS: &dynamic.RouterTLSConfig{ - Options: "tls12", + Options: "tls12", + ResolvedOptions: "tls12", }, }, } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 5814411b16..39dd3b42fe 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "errors" "expvar" "fmt" @@ -610,6 +611,15 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati HTTP2: &http.HTTP2Config{ MaxConcurrentStreams: int(configuration.HTTP2.MaxConcurrentStreams), }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + if tlsConn, ok := c.(*tls.Conn); ok { + if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok { + return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName) + } + } + + return ctx + }, } if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context { diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go index d8821267c7..b971d97892 100644 --- a/pkg/server/server_entrypoint_tcp_http3.go +++ b/pkg/server/server_entrypoint_tcp_http3.go @@ -13,7 +13,9 @@ import ( "github.com/quic-go/quic-go/http3" "github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/log" + tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp" tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp" + "github.com/traefik/traefik/v2/pkg/tcp" ) type http3server struct { @@ -22,7 +24,7 @@ type http3server struct { http3conn net.PacketConn lock sync.RWMutex - getter func(info *tls.ClientHelloInfo) (*tls.Config, error) + getter func(data tcpmuxer.ConnData) (*tls.Config, string, error) } func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, httpsServer *httpServer) (*http3server, error) { @@ -41,8 +43,8 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https h3 := &http3server{ http3conn: conn, - getter: func(info *tls.ClientHelloInfo) (*tls.Config, error) { - return nil, errors.New("no tls config") + getter: func(data tcpmuxer.ConnData) (*tls.Config, string, error) { + return nil, "", errors.New("no TLS config") }, } @@ -50,10 +52,18 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https Addr: configuration.GetAddress(), Port: configuration.HTTP3.AdvertisedPort, Handler: httpsServer.Server.(*http.Server).Handler, - TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient}, + TLSConfig: &tls.Config{GetConfigForClient: h3.getTLSConfigForClient}, QUICConfig: &quic.Config{ Allow0RTT: false, }, + ConnContext: func(ctx context.Context, c *quic.Conn) context.Context { + tlsOptionsName, err := h3.getTLSOptionsName(c) + if err != nil { + log.WithoutContext().Errorf("Error getting TLS options name for client: %v", err) + return ctx + } + return tcp.AddTLSOptionsNameInContext(ctx, tlsOptionsName) + }, } previousHandler := httpsServer.Server.(*http.Server).Handler @@ -77,7 +87,7 @@ func (e *http3server) Switch(rt *tcprouter.Router) { e.lock.Lock() defer e.lock.Unlock() - e.getter = rt.GetTLSGetClientInfo() + e.getter = rt.HTTP3TLSConfigMatcherFunc() } func (e *http3server) Shutdown(_ context.Context) error { @@ -85,9 +95,28 @@ func (e *http3server) Shutdown(_ context.Context) error { return e.Server.Close() } -func (e *http3server) getGetConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) { +func (e *http3server) getTLSConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) { e.lock.RLock() defer e.lock.RUnlock() - return e.getter(info) + connData, err := tcpmuxer.NewConnData(info.ServerName, info.Conn.RemoteAddr(), info.SupportedProtos) + if err != nil { + return nil, fmt.Errorf("creating ConnData from client hello: %w", err) + } + + conf, _, err := e.getter(connData) + return conf, err +} + +func (e *http3server) getTLSOptionsName(c *quic.Conn) (string, error) { + e.lock.RLock() + defer e.lock.RUnlock() + + connData, err := tcpmuxer.NewConnData(c.ConnectionState().TLS.ServerName, c.RemoteAddr(), []string{c.ConnectionState().TLS.NegotiatedProtocol}) + if err != nil { + return "", fmt.Errorf("creating ConnData from quic Conn: %w", err) + } + + _, name, err := e.getter(connData) + return name, err } diff --git a/pkg/server/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go index bcaa1c028d..9adc1d43d7 100644 --- a/pkg/server/server_entrypoint_tcp_http3_test.go +++ b/pkg/server/server_entrypoint_tcp_http3_test.go @@ -102,7 +102,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) { router.AddHTTPTLSConfig("*", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, - }) + }, traefiktls.DefaultTLSConfigName) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) @@ -164,7 +164,7 @@ func TestHTTP30RTT(t *testing.T) { router.AddHTTPTLSConfig("example.com", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, - }) + }, traefiktls.DefaultTLSConfigName) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) diff --git a/pkg/tcp/tls.go b/pkg/tcp/tls.go index 207aebb150..33914e5e53 100644 --- a/pkg/tcp/tls.go +++ b/pkg/tcp/tls.go @@ -1,16 +1,39 @@ package tcp import ( + "context" "crypto/tls" ) +// TLSConn is a TLS connection that also carries the name of the TLS config used. +type TLSConn struct { + WriteCloser + + TLSOptionsName string +} + // TLSHandler handles TLS connections. type TLSHandler struct { - Next Handler - Config *tls.Config + Next Handler + Config *tls.Config + TLSOptionsName string } // ServeTCP terminates the TLS connection. func (t *TLSHandler) ServeTCP(conn WriteCloser) { - t.Next.ServeTCP(tls.Server(conn, t.Config)) + t.Next.ServeTCP(tls.Server(TLSConn{WriteCloser: conn, TLSOptionsName: t.TLSOptionsName}, t.Config)) +} + +type tlsOptionsNameKey struct{} + +func AddTLSOptionsNameInContext(ctx context.Context, name string) context.Context { + return context.WithValue(ctx, tlsOptionsNameKey{}, name) +} + +func GetTLSOptionsName(ctx context.Context) string { + if name, ok := ctx.Value(tlsOptionsNameKey{}).(string); ok { + return name + } + + return "" } From 892bcc288b1c99f7c18a977c9bcab617d5cfa37e Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 28 May 2026 15:56:25 +0200 Subject: [PATCH 05/32] Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation --- docs/content/migration/v2.md | 17 +++++++++++++++++ pkg/middlewares/stripprefix/strip_prefix.go | 12 +++++++++++- .../stripprefix/strip_prefix_test.go | 19 +++++++------------ .../stripprefixregex/strip_prefix_regex.go | 12 +++++++++++- .../strip_prefix_regex_test.go | 12 ++---------- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 69baa2c9a9..8c09fd7d5f 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -856,3 +856,20 @@ and [Kubernetes Gateway](../providers/kubernetes-gateway.md#crossprovidernamespa From version `v2.11.47` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. Previously, the middleware would be built successfully but always return a 401 status code for any request. Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. + +### StripPrefix and StripPrefixRegex Middleware + +From version `v2.11.47` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +when stripping the configured prefix produces a path that differs from its normalised form +(i.e. a path containing `.` or `..` segments that would be collapsed by normalisation). + +This prevents the stripped path from being interpreted as a different resource by the upstream service. + +Examples with a configured prefix of `/api`: + +| Request path | Path after strip | Normalised path | Result | +|--------------|------------------|-----------------|--------------| +| `/api/foo` | `/foo` | `/foo` | `200` (sent) | +| `/api/` | `/` | `/` | `200` (sent) | +| `/api./foo` | `/./foo` | `/foo` | `400` | +| `/api../foo` | `/../foo` | `/foo` | `400` | diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go index 6abd4391e4..29a6afddca 100644 --- a/pkg/middlewares/stripprefix/strip_prefix.go +++ b/pkg/middlewares/stripprefix/strip_prefix.go @@ -48,6 +48,8 @@ func (s *stripPrefix) GetTracingInformation() (string, ext.SpanKindEnum) { } func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), s.name, typeName)) + for _, prefix := range s.prefixes { if strings.HasPrefix(req.URL.Path, prefix) { req.URL.Path = s.getPathStripped(req.URL.Path, prefix) @@ -58,10 +60,18 @@ func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Here we are sanitizing the URL when the path is not empty, // as the JoinPath method is adding a leading slash if the path is empty // to be aligned with ensureLeadingSlash behavior. - if req.URL.Path != "" { + path := req.URL.Path + if path != "" { req.URL = req.URL.JoinPath() } + // Stop here if the normalization of the path produces a different path. + if path != req.URL.Path { + logger.Debugf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + req.Header.Add(ForwardedPrefixHeader, prefix) req.RequestURI = req.URL.RequestURI() break diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go index 1fa0330de1..b551154af7 100644 --- a/pkg/middlewares/stripprefix/strip_prefix_test.go +++ b/pkg/middlewares/stripprefix/strip_prefix_test.go @@ -191,10 +191,7 @@ func TestStripPrefix(t *testing.T) { Prefixes: []string{"/api"}, }, path: "/api./foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "multiple dots in the path not stripped by the prefix", @@ -202,10 +199,7 @@ func TestStripPrefix(t *testing.T) { Prefixes: []string{"/api"}, }, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "multiple dots in the path not stripped by the prefix with forceSlash", @@ -214,10 +208,7 @@ func TestStripPrefix(t *testing.T) { ForceSlash: true, }, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, } @@ -244,6 +235,10 @@ func TestStripPrefix(t *testing.T) { handler.ServeHTTP(resp, req) assert.Equal(t, test.expectedStatusCode, resp.Code, "Unexpected status code.") + if test.expectedStatusCode != http.StatusOK { + return + } + assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.") assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.") assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ForwardedPrefixHeader) diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go index b07966fd51..825e91bd77 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go @@ -50,6 +50,8 @@ func (s *stripPrefixRegex) GetTracingInformation() (string, ext.SpanKindEnum) { } func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), s.name, typeName)) + for _, exp := range s.expressions { parts := exp.FindStringSubmatch(req.URL.Path) if len(parts) > 0 && len(parts[0]) > 0 { @@ -68,10 +70,18 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) // Here we are sanitizing the URL when the path is not empty, // as the JoinPath method is adding a leading slash if the path is empty // to be aligned with ensureLeadingSlash behavior. - if req.URL.Path != "" { + path := req.URL.Path + if path != "" { req.URL = req.URL.JoinPath() } + // Stop here if the normalization of the path produces a different path. + if path != req.URL.Path { + logger.Debugf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + req.RequestURI = req.URL.RequestURI() break } diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go index c9c6b59347..3803421960 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go @@ -201,21 +201,13 @@ func TestStripPrefixRegex(t *testing.T) { desc: "/api./foo", config: dynamic.StripPrefixRegex{Regex: []string{"/api"}}, path: "/api./foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedRequestURI: "/foo", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "/api../foo", config: dynamic.StripPrefixRegex{Regex: []string{"/api"}}, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedRequestURI: "/foo", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, } From 83b36871c3ce628f745630cb8288e08dffb33486 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Thu, 28 May 2026 17:10:15 +0200 Subject: [PATCH 06/32] Add ingressClassName to Kubernetes CRD provider migration guide --- docs/content/migrate/v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 36a959b3e2..e8d8894705 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -80,7 +80,7 @@ kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/downloa ### Kubernetes CRD Provider -To use the new options of the `retry` middleware with the Kubernetes CRD provider, you need to update your CRDs. +To use the new options of the `retry` middleware or the new `ingressClassName` field with the Kubernetes CRD provider, you need to update your CRDs. **Apply Updated CRDs:** From fc83948b1eba226bef903d06063fa2b3df3c514f Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Fri, 29 May 2026 09:14:05 +0200 Subject: [PATCH 07/32] Bump golang.org/x/net to v0.55.0 --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index cc56298a02..b4324a7ee5 100644 --- a/go.mod +++ b/go.mod @@ -75,8 +75,8 @@ require ( go.elastic.co/apm/module/apmot/v2 v2.4.8 go.elastic.co/apm/v2 v2.4.8 golang.org/x/mod v0.35.0 - golang.org/x/net v0.53.0 - golang.org/x/text v0.36.0 + golang.org/x/net v0.55.0 + golang.org/x/text v0.37.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.44.0 google.golang.org/grpc v1.80.0 @@ -393,12 +393,12 @@ require ( go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.42.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.276.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect diff --git a/go.sum b/go.sum index b4d6490011..fbfb2385b4 100644 --- a/go.sum +++ b/go.sum @@ -2363,8 +2363,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2520,8 +2520,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2709,8 +2709,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2733,8 +2733,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2756,8 +2756,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 9c94e3b493ade96959a6bea9dd47ba6d6571cb4e Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Fri, 29 May 2026 09:48:09 +0200 Subject: [PATCH 08/32] Bump github.com/moby/spdystream to v0.5.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1c78f68451..b5f3a989a1 100644 --- a/go.mod +++ b/go.mod @@ -297,7 +297,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect - github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect diff --git a/go.sum b/go.sum index 757a98b626..c59154a514 100644 --- a/go.sum +++ b/go.sum @@ -1633,8 +1633,8 @@ github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjI github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= From 8c05c1b1a709ed306c9dec68d36c783d52b2ff62 Mon Sep 17 00:00:00 2001 From: Yuxiao Zeng Date: Fri, 29 May 2026 21:14:05 +0900 Subject: [PATCH 09/32] Improve file provider behavior regarding dangling symlinks --- pkg/provider/file/file.go | 24 +++++++++++++++++++----- pkg/provider/file/file_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 04395878e5..557429a3a6 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -71,9 +71,16 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. // ignore sub-dir continue } + if !isFileSupported(entry.Name()) { + // ignore unsupported file extension + continue + } watchItems = append(watchItems, path.Join(p.Directory, entry.Name())) } case len(p.Filename) > 0: + if !isFileSupported(p.Filename) { + return fmt.Errorf("unsupported file extension for file %s", p.Filename) + } watchItems = append(watchItems, filepath.Dir(p.Filename), p.Filename) default: return errors.New("error using file configuration provider, neither filename nor directory is defined") @@ -168,7 +175,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan log.Debug().Msgf("add watcher on: %s", item) err = watcher.Add(item) if err != nil { - return fmt.Errorf("error adding file watcher: %w", err) + return fmt.Errorf("error adding file watcher for %s: %w", item, err) } } @@ -420,10 +427,8 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st continue } - switch strings.ToLower(filepath.Ext(item.Name())) { - case ".toml", ".yaml", ".yml": - // noop - default: + if !isFileSupported(item.Name()) { + logger.Debug().Msg("Skipping file, unsupported extension") continue } @@ -627,3 +632,12 @@ func readFile(filename string) (string, error) { } return "", fmt.Errorf("invalid filename: %s", filename) } + +func isFileSupported(filename string) bool { + switch strings.ToLower(filepath.Ext(filename)) { + case ".toml", ".yaml", ".yml": + return true + default: + return false + } +} diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go index 3aebe1908e..e89e504e3a 100644 --- a/pkg/provider/file/file_test.go +++ b/pkg/provider/file/file_test.go @@ -197,6 +197,38 @@ func TestProvideWithWatch(t *testing.T) { } } +func TestProvideWatchWithNonConfigDanglingSymlink(t *testing.T) { + tempDir := t.TempDir() + + err := copyFile("./fixtures/yaml/simple_file_01.yml", filepath.Join(tempDir, "simple_file_01.yml")) + require.NoError(t, err) + + err = os.Symlink(filepath.Join(tempDir, "non_existent_file.txt"), filepath.Join(tempDir, "dangling_symlink.txt")) + require.NoError(t, err) + + provider := &Provider{ + Directory: tempDir, + Watch: true, + } + configChan := make(chan dynamic.Message) + go func() { + err := provider.Provide(configChan, safe.NewPool(t.Context())) + assert.NoError(t, err) + }() + + timeout := time.After(time.Second) + select { + case conf := <-configChan: + require.NotNil(t, conf.Configuration.HTTP) + numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services) + numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers) + assert.Equal(t, 6, numServices) + assert.Equal(t, 3, numRouters) + case <-timeout: + t.Errorf("timeout while waiting for config") + } +} + func getTestCases() []ProvideTestCase { return []ProvideTestCase{ { From af3158784156fe416d80523e1646b97395df8986 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Sat, 30 May 2026 10:30:05 +0200 Subject: [PATCH 10/32] Bump github.com/bytedance/sonic to v1.15.1 --- go.mod | 3 +-- go.sum | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index b5f3a989a1..a44d65a073 100644 --- a/go.mod +++ b/go.mod @@ -178,7 +178,7 @@ require ( github.com/blendle/zapdriver v1.3.1 // indirect github.com/bodgit/tsig v1.2.2 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/bytedance/sonic v1.12.0 // indirect + github.com/bytedance/sonic v1.15.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect @@ -273,7 +273,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect diff --git a/go.sum b/go.sum index c59154a514..9fb2e57500 100644 --- a/go.sum +++ b/go.sum @@ -839,10 +839,12 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= -github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw= +github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= @@ -871,10 +873,8 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -1471,8 +1471,8 @@ github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHU github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= From 9a276c3aeb970b4f8898835bbd476a2eadfcf78f Mon Sep 17 00:00:00 2001 From: filip2mac Date: Mon, 1 Jun 2026 09:56:05 +0200 Subject: [PATCH 11/32] Add nginx.ingress.kubernetes.io/enable-global-auth to the list of supported annotations --- .../reference/routing-configuration/kubernetes/ingress-nginx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index c26265b784..64a714e998 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -280,6 +280,7 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/auth-snippet` | Supported directives: `proxy_method`, `more_set_headers`, `proxy_set_header`, `more_set_input_headers`, `set`, `if`, `return code [text]`. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. | | `nginx.ingress.kubernetes.io/auth-method` | This annotation uses the `proxy_method` directive in Nginx. Thus, it can't be defined on an ingress that already have an `auth-snippet` annotation with the `proxy_method` directive. | | `nginx.ingress.kubernetes.io/auth-response-headers` | | +| `nginx.ingress.kubernetes.io/enable-global-auth` | | ### SSL/TLS @@ -456,7 +457,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o | `nginx.ingress.kubernetes.io/auth-keepalive-requests` | | | `nginx.ingress.kubernetes.io/auth-keepalive-timeout` | | | `nginx.ingress.kubernetes.io/auth-proxy-set-headers` | | -| `nginx.ingress.kubernetes.io/enable-global-auth` | | | `nginx.ingress.kubernetes.io/disable-proxy-intercept-errors` | | | `nginx.ingress.kubernetes.io/limit-rate-after` | | | `nginx.ingress.kubernetes.io/limit-rate` | | From 3697409701c2a1e1fcb39565e94c3571403c6395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20P=C3=BCnk=C3=B6sd?= Date: Mon, 1 Jun 2026 16:18:05 +0200 Subject: [PATCH 12/32] Fix race condition in tests accessing global variables in parallel --- pkg/middlewares/observability/semconv_test.go | 2 -- pkg/proxy/httputil/observability_test.go | 2 -- 2 files changed, 4 deletions(-) diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go index 960d4b98af..1494e3aeb1 100644 --- a/pkg/middlewares/observability/semconv_test.go +++ b/pkg/middlewares/observability/semconv_test.go @@ -52,8 +52,6 @@ func TestSemConvServerMetrics(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - t.Parallel() - var cfg otypes.OTLP (&cfg).SetDefaults() cfg.AddRoutersLabels = true diff --git a/pkg/proxy/httputil/observability_test.go b/pkg/proxy/httputil/observability_test.go index c0ef0e7a3b..117b589bbe 100644 --- a/pkg/proxy/httputil/observability_test.go +++ b/pkg/proxy/httputil/observability_test.go @@ -57,8 +57,6 @@ func TestObservabilityRoundTripper_metrics(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - t.Parallel() - var cfg otypes.OTLP (&cfg).SetDefaults() cfg.AddRoutersLabels = true From a669522ecaf5b7cd5c1d6f5f82aeda0da044ff34 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:40:06 +0200 Subject: [PATCH 13/32] Clear Ssl-Client-* headers when no client certificate is present --- .../auth_tls_pass_certificate_to_upstream.go | 4 ++++ ...h_tls_pass_certificate_to_upstream_test.go | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go index b7f1fecbaa..b98e738d32 100644 --- a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go +++ b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go @@ -65,6 +65,10 @@ func (p *authTLSPassCertificateToUpstream) ServeHTTP(rw http.ResponseWriter, req if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 { logger.Debug().Msg("Tried to extract a certificate on a request without mutual TLS") req.Header.Set(sslClientVerify, "NONE") + // Prevent client-supplied values from reaching the upstream on the no-mTLS path. + req.Header.Del(sslClientCert) + req.Header.Del(sslClientSubjectDN) + req.Header.Del(sslClientIssuerDN) p.next.ServeHTTP(rw, req) return } diff --git a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go index 08f216b0a2..aa96f6d291 100644 --- a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go +++ b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go @@ -360,6 +360,26 @@ func TestAuthTLSPassCertificateToUpstream(t *testing.T) { } } +func TestAuthTLSNoMTLSClearsCertHeaders(t *testing.T) { + config := dynamic.AuthTLSPassCertificateToUpstream{ + ClientAuthType: tls.VerifyClientCertIfGiven, + } + handler, err := NewAuthTLSPassCertificateToUpstream(t.Context(), next, config, "test") + require.NoError(t, err) + + req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil) + req.Header.Set(sslClientCert, "client-cert") + req.Header.Set(sslClientSubjectDN, "CN=client") + req.Header.Set(sslClientIssuerDN, "CN=client-CA") + + handler.ServeHTTP(httptest.NewRecorder(), req) + + assert.Equal(t, "NONE", req.Header.Get(sslClientVerify)) + assert.Empty(t, req.Header.Get(sslClientCert)) + assert.Empty(t, req.Header.Get(sslClientSubjectDN)) + assert.Empty(t, req.Header.Get(sslClientIssuerDN)) +} + func buildTLSWith(certContents []string) *cryptoTLS.ConnectionState { var peerCertificates []*x509.Certificate From f25d48e039f13ad9390b0f8e1c9f5d150371858b Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 3 Jun 2026 09:14:05 +0200 Subject: [PATCH 14/32] Bump golang.org/x/crypto to v0.52.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b4324a7ee5..cf0276f98c 100644 --- a/go.mod +++ b/go.mod @@ -393,7 +393,7 @@ require ( go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect diff --git a/go.sum b/go.sum index fbfb2385b4..5ca5859d6a 100644 --- a/go.sum +++ b/go.sum @@ -2363,8 +2363,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= From 855561306fee33fa5fd59d2bbbb7961cf34aa6ef Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 3 Jun 2026 11:02:05 +0200 Subject: [PATCH 15/32] Prepare release v2.11.47 --- CHANGELOG.md | 11 +++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ff9a128e..60182a3ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) + +**Bug fixes:** +- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) +- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) +- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) +- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) +- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) +- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) + ## [v2.11.46](https://github.com/traefik/traefik/tree/v2.11.46) (2026-05-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.45...v2.11.46) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 89120441da..29e2645689 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.46 +# example new bugfix v2.11.47 CurrentRef = "v2.11" -PreviousRef = "v2.11.45" +PreviousRef = "v2.11.46" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.46" +FutureCurrentRefName = "v2.11.47" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From dcbe752df54e178b4575786476e8843c3e2cd052 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 3 Jun 2026 11:06:05 +0200 Subject: [PATCH 16/32] Change default values and expose configuration for Kubernetes client QPS and Burst Co-authored-by: Anatole Lucet --- docs/content/migrate/v3.md | 14 ++++++++ .../configuration-options.md | 2 ++ .../kubernetes/kubernetes-gateway.md | 34 ++++++++++--------- pkg/provider/kubernetes/gateway/client.go | 17 ++++++---- pkg/provider/kubernetes/gateway/kubernetes.go | 13 +++++-- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index eb89268256..cb6b08cbb9 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,6 +9,20 @@ This guide provides detailed migration steps for upgrading between different Tra --- +## v3.6.19 + +### Kubernetes Gateway API Provider + +Starting with `v3.6.19`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). + +The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification. +This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built. + +These values are configurable through the [`kubernetesGateway.qps`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-qps) +and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-burst) provider options. + +--- + ## v3.6.18 ### BasicAuth Middleware diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 50732d098a..807a110d86 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -359,6 +359,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetescrd.throttleduration | Ingress refresh throttle duration | 0 | | 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.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 | | 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). | | @@ -366,6 +367,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetesgateway.labelselector | Kubernetes label selector to select specific GatewayClasses. | | | providers.kubernetesgateway.namespaces | Kubernetes namespaces. | | | providers.kubernetesgateway.nativelbbydefault | Defines whether to use Native Kubernetes load-balancing by default. | false | +| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | | providers.kubernetesgateway.statusaddress.hostname | Hostname used for Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.ip | IP used to set Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.service | Published Kubernetes Service to copy status addresses from. | | 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 95c8c32292..b0fdbe029c 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md @@ -67,22 +67,24 @@ providers: -| 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.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No | -| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute` and `TLSRoute`) | false | No | -| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No | -| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | -| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | -| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | -| `providers.kubernetesGateway.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.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | -| `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | -| `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 | +| 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.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No | +| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute` and `TLSRoute`) | false | No | +| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No | +| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | +| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | +| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesGateway.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.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | +| `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | +| `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 | +| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No | +| providers.kubernetesgateway.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No | diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index fd383d89e4..37c9a85dad 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -49,7 +49,10 @@ type clientWrapper struct { experimentalChannel bool } -func createClientFromConfig(c *rest.Config) (*clientWrapper, error) { +func createClientFromConfig(c *rest.Config, qps, burst int) (*clientWrapper, error) { + c.QPS = float32(qps) + c.Burst = burst + csGateway, err := gateclientset.NewForConfig(c) if err != nil { return nil, err @@ -75,7 +78,7 @@ func newClientImpl(csKube kclientset.Interface, csGateway gateclientset.Interfac // newInClusterClient returns a new Provider client that is expected to run // inside the cluster. -func newInClusterClient(endpoint string) (*clientWrapper, error) { +func newInClusterClient(endpoint string, qps, burst int) (*clientWrapper, error) { config, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("failed to create in-cluster configuration: %w", err) @@ -85,20 +88,20 @@ func newInClusterClient(endpoint string) (*clientWrapper, error) { config.Host = endpoint } - return createClientFromConfig(config) + return createClientFromConfig(config, qps, burst) } -func newExternalClusterClientFromFile(file string) (*clientWrapper, error) { +func newExternalClusterClientFromFile(file string, qps, burst int) (*clientWrapper, error) { configFromFlags, err := clientcmd.BuildConfigFromFlags("", file) if err != nil { return nil, err } - return createClientFromConfig(configFromFlags) + return createClientFromConfig(configFromFlags, qps, burst) } // newExternalClusterClient returns a new Provider client that may run outside of the cluster. // The endpoint parameter must not be empty. -func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent) (*clientWrapper, error) { +func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent, qps, burst int) (*clientWrapper, error) { if endpoint == "" { return nil, errors.New("endpoint missing for external cluster client") } @@ -122,7 +125,7 @@ func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrCon config.TLSClientConfig = rest.TLSClientConfig{CAData: caData} } - return createClientFromConfig(config) + return createClientFromConfig(config, qps, burst) } // WatchAll starts namespace-specific controllers for all relevant kinds. diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 2883c42c13..ddc4903884 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -64,6 +64,8 @@ const ( 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"` + QPS int `description:"Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting." json:"qps,omitempty" toml:"qps,omitempty" yaml:"qps,omitempty" export:"true"` + Burst int `description:"Defines the maximum burst of requests to the Kubernetes API server." json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty" export:"true"` 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"` @@ -85,6 +87,11 @@ type Provider struct { client *clientWrapper } +func (p *Provider) SetDefaults() { + p.QPS = 50 // the default value for the QPS is 10x the default Kubernetes client QPS value. + p.Burst = 100 // the default value for the Burst is 10x the default Kubernetes client Burst value. +} + // Entrypoint defines the available entry points. type Entrypoint struct { Address string @@ -274,13 +281,13 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { switch { case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": logger.Info().Str("endpoint", p.Endpoint).Msg("Creating in-cluster Provider client") - client, err = newInClusterClient(p.Endpoint) + client, err = newInClusterClient(p.Endpoint, p.QPS, p.Burst) case os.Getenv("KUBECONFIG") != "": logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) - client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) + client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG"), p.QPS, p.Burst) default: logger.Info().Str("endpoint", p.Endpoint).Msg("Creating cluster-external Provider client") - client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) + client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token, p.QPS, p.Burst) } if err != nil { From 32da599169a0bde6ef0aab66d7989998d02d5636 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 3 Jun 2026 11:50:05 +0200 Subject: [PATCH 17/32] Prepare release v3.6.18 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ docs/content/migrate/v3.md | 8 ++------ script/gcg/traefik-bugfix.toml | 6 +++--- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aabe98e1ce..c60be5711c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) + +**Bug fixes:** +- **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) +- **[k8s/gatewayapi]** Escape exact gRPC method matches ([#13201](https://github.com/traefik/traefik/pull/13201) @nickmnt) +- **[logs, middleware]** Allow query parameters to be dropped from RequestPath in access log ([#13091](https://github.com/traefik/traefik/pull/13091) @calinelson) +- **[k8s/gatewayapi]** Bump github.com/moby/spdystream to v0.5.1 ([#13252](https://github.com/traefik/traefik/pull/13252) @kevinpollet) +- **[file]** Improve file provider behavior regarding dangling symlinks ([#12449](https://github.com/traefik/traefik/pull/12449) @fh-yuxiao-zeng) +- **[server]** Bump github.com/bytedance/sonic to v1.15.1 ([#13254](https://github.com/traefik/traefik/pull/13254) @kevinpollet) +- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) +- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) +- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) +- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) +- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) +- **[k8s/gatewayapi]** Change default values and expose configuration for Kubernetes client QPS and Burst ([#13277](https://github.com/traefik/traefik/pull/13277) @kevinpollet) +- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) + +**Documentation:** +- **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik) +- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) +- **[k8s/crd]** Fix typo in accesslogs field name ([#13177](https://github.com/traefik/traefik/pull/13177) @PlayMTL) +- **[k8s/ingress-nginx]** Surface the Ingress status race condition during NGINX coexistence ([#13205](https://github.com/traefik/traefik/pull/13205) @emilevauge) +- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) +- Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs) +- Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge) + ## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) [All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index cb6b08cbb9..77b5b4b153 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,11 +9,11 @@ This guide provides detailed migration steps for upgrading between different Tra --- -## v3.6.19 +## v3.6.18 ### Kubernetes Gateway API Provider -Starting with `v3.6.19`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). +Starting with `v3.6.18`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification. This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built. @@ -21,10 +21,6 @@ This change helps avoid performance issues related to Kubernetes API rate limiti These values are configurable through the [`kubernetesGateway.qps`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-qps) and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-burst) provider options. ---- - -## v3.6.18 - ### BasicAuth Middleware From version `v3.6.18` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index b469458dc6..dc5c6d8474 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.6.17 +# example new bugfix v3.6.18 CurrentRef = "v3.6" -PreviousRef = "v3.6.16" +PreviousRef = "v3.6.17" BaseBranch = "v3.6" -FutureCurrentRefName = "v3.6.17" +FutureCurrentRefName = "v3.6.18" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From e38281d8adf6a091bd3d8e043e683864e6052c83 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 3 Jun 2026 15:34:05 +0200 Subject: [PATCH 18/32] Prepare release v3.7.2 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8ee3aaa3..5e06d7f0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +## [v3.7.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2) + +**Bug fixes:** +- **[webui, tcp]** Fix TCP router service resolution in dashboard flow diagram ([#13155](https://github.com/traefik/traefik/pull/13155) @aliamerj) +- **[k8s/ingress-nginx]** Trim quotes from proxy_set_header header name ([#13203](https://github.com/traefik/traefik/pull/13203) @crisbal) +- **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) +- **[k8s/gatewayapi]** Escape exact gRPC method matches ([#13201](https://github.com/traefik/traefik/pull/13201) @nickmnt) +- **[logs, middleware]** Allow query parameters to be dropped from RequestPath in access log ([#13091](https://github.com/traefik/traefik/pull/13091) @calinelson) +- **[k8s/ingress-nginx]** Clear Ssl-Client-* headers when no client certificate is present ([#13260](https://github.com/traefik/traefik/pull/13260) @gndz07) +- **[k8s/gatewayapi]** Bump github.com/moby/spdystream to v0.5.1 ([#13252](https://github.com/traefik/traefik/pull/13252) @kevinpollet) +- **[file]** Improve file provider behavior regarding dangling symlinks ([#12449](https://github.com/traefik/traefik/pull/12449) @fh-yuxiao-zeng) +- **[server]** Bump github.com/bytedance/sonic to v1.15.1 ([#13254](https://github.com/traefik/traefik/pull/13254) @kevinpollet) +- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) +- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) +- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) +- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) +- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) +- **[k8s/gatewayapi]** Change default values and expose configuration for Kubernetes client QPS and Burst ([#13277](https://github.com/traefik/traefik/pull/13277) @kevinpollet) +- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) + +**Documentation:** +- **[k8s]** Document new chart behavior on Gateway API ([#13167](https://github.com/traefik/traefik/pull/13167) @mloiseleur) +- **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik) +- **[k8s/crd]** Fix typo in accesslogs field name ([#13177](https://github.com/traefik/traefik/pull/13177) @PlayMTL) +- **[k8s/ingress-nginx]** Surface the Ingress status race condition during NGINX coexistence ([#13205](https://github.com/traefik/traefik/pull/13205) @emilevauge) +- Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs) +- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) +- Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge) +- Add ingressClassName to Kubernetes CRD provider migration guide ([#13248](https://github.com/traefik/traefik/pull/13248) @kevinpollet) +- **[k8s/ingress-nginx]** Add nginx.ingress.kubernetes.io/enable-global-auth to the list of supported annotations ([#13219](https://github.com/traefik/traefik/pull/13219) @filip2mac) +- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) + ## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) [All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 483676a2c4..bbb80c2a3a 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.7.1 +# example new bugfix v3.7.2 CurrentRef = "v3.7" -PreviousRef = "v3.7.0" +PreviousRef = "v3.7.1" BaseBranch = "v3.7" -FutureCurrentRefName = "v3.7.1" +FutureCurrentRefName = "v3.7.2" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 85290af77db9c1876df104a47af2e3ccecc006e3 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 3 Jun 2026 15:50:05 +0200 Subject: [PATCH 19/32] Bump github.com/fsnotify/fsnotify to v1.10.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8a3f42136f..e49c510be1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/docker/cli v29.4.0+incompatible github.com/docker/go-connections v0.6.0 github.com/fatih/structs v1.1.0 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 github.com/go-acme/lego/v4 v4.35.2 github.com/go-kit/kit v0.13.0 github.com/go-kit/log v0.2.1 diff --git a/go.sum b/go.sum index d68ebd9815..4b600004ff 100644 --- a/go.sum +++ b/go.sum @@ -1009,8 +1009,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= From a664812e9c30f8c9705ce16f0d2f2d96e017bccd Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 10:16:05 +0200 Subject: [PATCH 20/32] Compute resolved tlsOptions after applying models Co-authored-by: Gina A. <70909035+gndz07@users.noreply.github.com> --- .../fixtures/https/https_entrypoint_tls.toml | 53 ++++++++++++++++ integration/https_test.go | 63 ++++++++++++++++++- pkg/server/aggregator.go | 2 +- pkg/server/configurationwatcher.go | 1 + pkg/server/configurationwatcher_test.go | 49 +++++++++++++++ 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 integration/fixtures/https/https_entrypoint_tls.toml diff --git a/integration/fixtures/https/https_entrypoint_tls.toml b/integration/fixtures/https/https_entrypoint_tls.toml new file mode 100644 index 0000000000..26a85c8128 --- /dev/null +++ b/integration/fixtures/https/https_entrypoint_tls.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.websecure] + address = ":4443" + [entryPoints.websecure.http.tls] + + [entryPoints.websecure-options] + address = ":4444" + [entryPoints.websecure-options.http.tls] + options = "foo" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + entryPoints = ["websecure"] + service = "service1" + rule = "Host(`snitest.com`)" + + [http.routers.router2] + entryPoints = ["websecure-options"] + service = "service1" + rule = "Host(`snitest.org`)" + +[http.services] + [http.services.service1] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[[tls.certificates]] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + +[[tls.certificates]] + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" + +[tls.options] + [tls.options.foo] + maxVersion = "VersionTLS12" diff --git a/integration/https_test.go b/integration/https_test.go index 8bdf5c1c45..2a79da18b3 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -114,8 +114,69 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute() { require.NoError(s.T(), err) } -// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options. +// TestWithEntryPointTLSConfig verifies that a router relying on the entry point +// TLS configuration (without an explicit router TLS section) is served over HTTPS, +// including when the entry point references user-defined TLS options. +// Regression test for https://github.com/traefik/traefik/issues/13289. +func (s *HTTPSSuite) TestWithEntryPointTLSConfig() { + file := s.adaptFile("fixtures/https/https_entrypoint_tls.toml", struct{}{}) + s.traefikCmd(withConfigFile(file)) + // wait for Traefik + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.com`)")) + require.NoError(s.T(), err) + + backend := startTestServer("9010", http.StatusNoContent, "") + defer backend.Close() + + err = try.GetRequest(backend.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + require.NoError(s.T(), err) + req.Host = tr.TLSClientConfig.ServerName + req.Header.Set("Host", tr.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 30*time.Second, tr, try.HasCn(tr.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + // The websecure-options entry point references the user-defined "foo" TLS options (maxVersion VersionTLS12). + // A request with no router-level TLS must still have these options resolved and applied. + trOptions := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4444/", nil) + require.NoError(s.T(), err) + req.Host = trOptions.TLSClientConfig.ServerName + req.Header.Set("Host", trOptions.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 30*time.Second, trOptions, try.HasCn(trOptions.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + // A TLS 1.3-only client must fail the handshake, proving the "foo" options + // (resolved from the entry point) are effectively enforced. + _, err = tls.Dial("tcp", "127.0.0.1:4444", &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + MinVersion: tls.VersionTLS13, + }) + assert.Error(s.T(), err) +} + +// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options. func (s *HTTPSSuite) TestWithTLSOptions() { file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 2dcabdae2b..1d8f5ea337 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -138,7 +138,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint delete(conf.TLS.Options, traefiktls.DefaultTLSConfigName) } - return resolveHTTPTLSOptions(conf) + return conf } func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration { diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 4d6798694e..5c1e8241ec 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -167,6 +167,7 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) { conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints) conf = applyModel(conf) + conf = resolveHTTPTLSOptions(conf) for _, listener := range c.configurationListeners { listener(conf) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 3e6c32fd76..08fd497e0d 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -893,3 +893,52 @@ func TestPublishConfigUpdatedByConfigWatcherListener(t *testing.T) { assert.Equal(t, 1, publishedConfigCount) } + +// TestEntryPointTLSResolvedOptions is a regression test for +// https://github.com/traefik/traefik/issues/13289: a router whose TLS +// configuration comes from the entry point (and not from an explicit router TLS +// section) must still have its TLS options resolved in the published configuration. +func TestEntryPointTLSResolvedOptions(t *testing.T) { + routinesPool := safe.NewPool(t.Context()) + t.Cleanup(routinesPool.Stop) + + pvd := &mockProvider{ + messages: []dynamic.Message{{ + ProviderName: "internal", + Configuration: &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "foo": { + EntryPoints: []string{"websecure"}, + Rule: "Host(`foo.example.com`)", + Service: "service", + }, + }, + Models: map[string]*dynamic.Model{ + "websecure": { + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + }}, + } + + watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "") + + run := make(chan struct{}) + watcher.AddListener(func(conf dynamic.Configuration) { + router := conf.HTTP.Routers["foo@internal"] + if router == nil || router.TLS == nil { + return + } + + assert.Equal(t, "default", router.TLS.ResolvedOptions) + close(run) + }) + + watcher.Start() + t.Cleanup(watcher.Stop) + + <-run +} From 2c436f3c239932f9eb8afab57fc2668d86880e35 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 10:36:22 +0200 Subject: [PATCH 21/32] Prepare release v2.11.48 --- CHANGELOG.md | 10 ++++++++-- docs/content/migration/v2.md | 6 +++--- script/gcg/traefik-bugfix.toml | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60182a3ed3..335030a399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) +## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48) **Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) - **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) - **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) - **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) @@ -9,6 +10,11 @@ - **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) - **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) +## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) + +Release canceled. + ## [v2.11.46](https://github.com/traefik/traefik/tree/v2.11.46) (2026-05-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.45...v2.11.46) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 8c09fd7d5f..3c74454bce 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -849,17 +849,17 @@ The behavior is as follows: Please check out the [Kubernetes CRD](../providers/kubernetes-crd.md#crossprovidernamespaces), [Kubernetes Ingress](../providers/kubernetes-ingress.md#crossprovidernamespaces), and [Kubernetes Gateway](../providers/kubernetes-gateway.md#crossprovidernamespaces) provider documentation for more details. -## v2.11.47 +## v2.11.48 ### BasicAuth Middleware -From version `v2.11.47` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v2.11.48` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. Previously, the middleware would be built successfully but always return a 401 status code for any request. Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. ### StripPrefix and StripPrefixRegex Middleware -From version `v2.11.47` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v2.11.48` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) when stripping the configured prefix produces a path that differs from its normalised form (i.e. a path containing `.` or `..` segments that would be collapsed by normalisation). diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 29e2645689..6f161f1556 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.47 +# example new bugfix v2.11.48 CurrentRef = "v2.11" -PreviousRef = "v2.11.46" +PreviousRef = "v2.11.47" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.47" +FutureCurrentRefName = "v2.11.48" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 281ce42477540b3777afd80c8d85dc17611388b4 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 11:28:16 +0200 Subject: [PATCH 22/32] Prepare release v3.6.19 --- CHANGELOG.md | 36 ++++++++++++++++++++-------------- docs/content/migrate/v3.md | 8 ++++---- script/gcg/traefik-bugfix.toml | 6 +++--- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b98ed2a8..c56bc2c3b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,8 @@ -## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04) -[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48) +## [v3.6.19](https://github.com/traefik/traefik/tree/v3.6.19) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.19) **Bug fixes:** - **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) -- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) -- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) -- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) -- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) -- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) -- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) - -## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) - -**Bug fixes:** - **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) - **[k8s/gatewayapi]** Escape exact gRPC method matches ([#13201](https://github.com/traefik/traefik/pull/13201) @nickmnt) - **[logs, middleware]** Allow query parameters to be dropped from RequestPath in access log ([#13091](https://github.com/traefik/traefik/pull/13091) @calinelson) @@ -30,12 +19,29 @@ **Documentation:** - **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik) -- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) - **[k8s/crd]** Fix typo in accesslogs field name ([#13177](https://github.com/traefik/traefik/pull/13177) @PlayMTL) - **[k8s/ingress-nginx]** Surface the Ingress status race condition during NGINX coexistence ([#13205](https://github.com/traefik/traefik/pull/13205) @emilevauge) -- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) - Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs) +- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) - Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge) +- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) + +## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48) + +**Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) +- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) +- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) +- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) +- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) +- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) +- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) + +## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) + +Release canceled. ## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) [All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 77b5b4b153..c0b6d591ed 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,11 +9,11 @@ This guide provides detailed migration steps for upgrading between different Tra --- -## v3.6.18 +## v3.6.19 ### Kubernetes Gateway API Provider -Starting with `v3.6.18`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). +Starting with `v3.6.19`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification. This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built. @@ -23,13 +23,13 @@ and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kub ### BasicAuth Middleware -From version `v3.6.18` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v3.6.19` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. Previously, the middleware would be built successfully but always return a 401 status code for any request. Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. ### StripPrefix and StripPrefixRegex Middleware -From version `v3.6.18` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v3.6.19` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) when stripping the configured prefix produces a path that differs from its normalised form (i.e. a path containing `.` or `..` segments that would be collapsed by normalisation). diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index dc5c6d8474..be68e36919 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.6.18 +# example new bugfix v3.6.19 CurrentRef = "v3.6" -PreviousRef = "v3.6.17" +PreviousRef = "v3.6.18" BaseBranch = "v3.6" -FutureCurrentRefName = "v3.6.18" +FutureCurrentRefName = "v3.6.19" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 5ea71f1c3af049067a0b6d5afd97bad1cf1a0071 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 15:14:05 +0200 Subject: [PATCH 23/32] Prepare release v3.7.3 --- CHANGELOG.md | 10 ++++++++-- docs/content/migrate/v3.md | 8 ++++---- script/gcg/traefik-bugfix.toml | 6 +++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ab09b6a4..4b9bf937c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## [v3.7.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2) +## [v3.7.3](https://github.com/traefik/traefik/tree/v3.7.3) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.3) **Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) - **[webui, tcp]** Fix TCP router service resolution in dashboard flow diagram ([#13155](https://github.com/traefik/traefik/pull/13155) @aliamerj) - **[k8s/ingress-nginx]** Trim quotes from proxy_set_header header name ([#13203](https://github.com/traefik/traefik/pull/13203) @crisbal) - **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) @@ -70,6 +71,11 @@ - **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) - **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) - **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) +- +## [v3.7.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2) + +Release canceled. ## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) [All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index daa4fdfd4c..fd65db12df 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,11 +9,11 @@ This guide provides detailed migration steps for upgrading between different Tra --- -## v3.7.2 +## v3.7.3 ### Kubernetes Gateway API Provider -Starting with `v3.7.2`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). +Starting with `v3.7.3`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification. This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built. @@ -23,13 +23,13 @@ and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kub ### BasicAuth Middleware -From version `v3.7.2` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v3.7.3` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. Previously, the middleware would be built successfully but always return a 401 status code for any request. Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. ### StripPrefix and StripPrefixRegex Middleware -From version `v3.7.2` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v3.7.3` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) when stripping the configured prefix produces a path that differs from its normalised form (i.e. a path containing `.` or `..` segments that would be collapsed by normalisation). diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index bbb80c2a3a..faae6153a2 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.7.2 +# example new bugfix v3.7.3 CurrentRef = "v3.7" -PreviousRef = "v3.7.1" +PreviousRef = "v3.7.2" BaseBranch = "v3.7" -FutureCurrentRefName = "v3.7.2" +FutureCurrentRefName = "v3.7.3" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From f32f05f811f58d4b1fae32ba620f6fd43388aa13 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 16:12:05 +0200 Subject: [PATCH 24/32] Bump github.com/quic-go/quic-go to v0.59.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cf0276f98c..358198fe63 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo. github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 - github.com/quic-go/quic-go v0.57.1 + github.com/quic-go/quic-go v0.59.1 github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac // No tag on the repo. github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 5ca5859d6a..4085371ad7 100644 --- a/go.sum +++ b/go.sum @@ -1946,8 +1946,8 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++ github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac h1:wBGhHdXKICZmvAPWS8gQoMyOWDH7QAi9bU4Z1nDWnFU= github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac/go.mod h1:67sLWL17mVlO1HFROaTBmU71NB4R8UNCesFHhg0f6LQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= From d653ee3d73a16467b9836483c685a21e17f290bb Mon Sep 17 00:00:00 2001 From: bzyy1024 <972702550@qq.com> Date: Thu, 4 Jun 2026 22:22:06 +0800 Subject: [PATCH 25/32] Fix redis write timeout option configuration --- pkg/middlewares/ratelimiter/redis_limiter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/middlewares/ratelimiter/redis_limiter.go b/pkg/middlewares/ratelimiter/redis_limiter.go index 9c49c629a2..29a2dc1221 100644 --- a/pkg/middlewares/ratelimiter/redis_limiter.go +++ b/pkg/middlewares/ratelimiter/redis_limiter.go @@ -49,7 +49,7 @@ func newRedisLimiter(ctx context.Context, rate rate.Limit, burst int64, maxDelay } if config.Redis.WriteTimeout != nil { - if *config.Redis.ReadTimeout > 0 { + if *config.Redis.WriteTimeout > 0 { options.WriteTimeout = time.Duration(*config.Redis.WriteTimeout) } else { options.WriteTimeout = -1 From 5d123f52e1205550dbbddf05db9529578e0891e3 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:20:05 +0200 Subject: [PATCH 26/32] Bump react-router and jsdom --- webui/package.json | 8 +- webui/yarn.lock | 488 ++++++++++++++++++++++++--------------------- 2 files changed, 264 insertions(+), 232 deletions(-) diff --git a/webui/package.json b/webui/package.json index 946fc98b0f..11a7616e6e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -64,7 +64,7 @@ "framer-motion": "^11.18.2", "globals": "^16.0.0", "jest-extended": "^4.0.2", - "jsdom": "^24.0.0", + "jsdom": "29.1.1", "lodash": "4.18.1", "msw": "^2.1.7", "query-string": "^6.9.0", @@ -75,7 +75,7 @@ "react-helmet-async": "^2.0.4", "react-icons": "^5.0.1", "react-infinite-scroll-hook": "^4.1.1", - "react-router-dom": "6.30.2", + "react-router-dom": "6.30.4", "swr": "^2.2.4", "typescript": "^5.2.2", "typescript-eslint": "^8.38.0", @@ -98,6 +98,8 @@ "packageManager": "yarn@4.9.1", "resolutions": { "form-data": "4.0.4", - "js-yaml": "4.1.1" + "js-yaml": "4.1.1", + "@csstools/css-syntax-patches-for-csstree": "1.1.4", + "undici": "7.27.0" } } diff --git a/webui/yarn.lock b/webui/yarn.lock index 6de5380e6f..8b5bded15c 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -22,16 +22,43 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^3.1.1": - version: 3.1.1 - resolution: "@asamuzakjp/css-color@npm:3.1.1" +"@asamuzakjp/css-color@npm:^5.1.11": + version: 5.1.11 + resolution: "@asamuzakjp/css-color@npm:5.1.11" dependencies: - "@csstools/css-calc": "npm:^2.1.2" - "@csstools/css-color-parser": "npm:^3.0.8" - "@csstools/css-parser-algorithms": "npm:^3.0.4" - "@csstools/css-tokenizer": "npm:^3.0.3" - lru-cache: "npm:^10.4.3" - checksum: 10c0/4abb010fd29de8acae8571eba738468c22cb45a1f77647df3c59a80f1c83d83d728cae3ebbf99e5c73f2517761abaaffbe5e4176fc46b5f9bf60f1478463b51e + "@asamuzakjp/generational-cache": "npm:^1.0.1" + "@csstools/css-calc": "npm:^3.2.0" + "@csstools/css-color-parser": "npm:^4.1.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + checksum: 10c0/32720bdff8daea6a8847aba6cdfae55baa3b4a2690b51d21db7f0382bbd183f3d9f2d5126df50afd889062635684b2819e47113629ee2e80c99389e75f48d060 + languageName: node + linkType: hard + +"@asamuzakjp/dom-selector@npm:^7.1.1": + version: 7.1.1 + resolution: "@asamuzakjp/dom-selector@npm:7.1.1" + dependencies: + "@asamuzakjp/generational-cache": "npm:^1.0.1" + "@asamuzakjp/nwsapi": "npm:^2.3.9" + bidi-js: "npm:^1.0.3" + css-tree: "npm:^3.2.1" + is-potential-custom-element-name: "npm:^1.0.1" + checksum: 10c0/8cec1c618781c94de5836a215bbe5aafb4d8b835b18c51faf8547f4574afa39f92def3951e40123860062467613dd825f1e1600ff32e8045cc099a91796dcfb8 + languageName: node + linkType: hard + +"@asamuzakjp/generational-cache@npm:^1.0.1": + version: 1.0.1 + resolution: "@asamuzakjp/generational-cache@npm:1.0.1" + checksum: 10c0/1de62de43764e13fca3b9a31b7ea9b1bf0780fe053d266e40378a19ff8c66b543e011e6a0df02d410cd59bf981126706f176cdbb938985165202c4a079fe1057 + languageName: node + linkType: hard + +"@asamuzakjp/nwsapi@npm:^2.3.9": + version: 2.3.9 + resolution: "@asamuzakjp/nwsapi@npm:2.3.9" + checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d languageName: node linkType: hard @@ -301,6 +328,17 @@ __metadata: languageName: node linkType: hard +"@bramus/specificity@npm:^2.4.2": + version: 2.4.2 + resolution: "@bramus/specificity@npm:2.4.2" + dependencies: + css-tree: "npm:^3.0.0" + bin: + specificity: bin/cli.js + checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72 + languageName: node + linkType: hard + "@bundled-es-modules/cookie@npm:^2.0.1": version: 2.0.1 resolution: "@bundled-es-modules/cookie@npm:2.0.1" @@ -329,49 +367,61 @@ __metadata: languageName: node linkType: hard -"@csstools/color-helpers@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/color-helpers@npm:5.0.2" - checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b +"@csstools/color-helpers@npm:^6.0.2": + version: 6.0.2 + resolution: "@csstools/color-helpers@npm:6.0.2" + checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789 languageName: node linkType: hard -"@csstools/css-calc@npm:^2.1.2": - version: 2.1.2 - resolution: "@csstools/css-calc@npm:2.1.2" +"@csstools/css-calc@npm:^3.2.0, @csstools/css-calc@npm:^3.2.1": + version: 3.2.1 + resolution: "@csstools/css-calc@npm:3.2.1" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.4 - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/0191c8d1cd4dffa0d3b6bfd1e78a721934b1d7a6c972966e4fdaa72208c6789e8ff443ee81764a32f1e6107825695b5524ef2b4dc1681b5b29230f2a1277e5df languageName: node linkType: hard -"@csstools/css-color-parser@npm:^3.0.8": - version: 3.0.8 - resolution: "@csstools/css-color-parser@npm:3.0.8" +"@csstools/css-color-parser@npm:^4.1.0": + version: 4.1.1 + resolution: "@csstools/css-color-parser@npm:4.1.1" dependencies: - "@csstools/color-helpers": "npm:^5.0.2" - "@csstools/css-calc": "npm:^2.1.2" + "@csstools/color-helpers": "npm:^6.0.2" + "@csstools/css-calc": "npm:^3.2.1" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.4 - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/427bd32f1a8917342a70a6fd97b93bb492aae7c8790e7782b5d6edc8c08064bb8aef0a86099f286db00288f9afea85eb92c46350e9057f5fea058e03a2a09203 languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/css-parser-algorithms@npm:3.0.4" +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" peerDependencies: - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^3.0.3": - version: 3.0.3 - resolution: "@csstools/css-tokenizer@npm:3.0.3" - checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5 +"@csstools/css-syntax-patches-for-csstree@npm:1.1.4": + version: 1.1.4 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.4" + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + checksum: 10c0/3872a7befb553c53249c87e964ac00f55d059f4574d2cc023e03e1dafc86a5ad19f6a6d05fa2c14fb192e6a4538a73158104cc2e32e0688f27fd841b9ba76568 + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f languageName: node linkType: hard @@ -850,6 +900,18 @@ __metadata: languageName: node linkType: hard +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0": + version: 1.15.1 + resolution: "@exodus/bytes@npm:1.15.1" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.6.0": version: 1.6.9 resolution: "@floating-ui/core@npm:1.6.9" @@ -2417,10 +2479,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.23.1": - version: 1.23.1 - resolution: "@remix-run/router@npm:1.23.1" - checksum: 10c0/94ac9632c0070199b8275cd6dffe78eb4c02e8926328937c65561c5c30d7ddf842743df3c8f7df302f00a593dd204846d93667fbbbe3c3641437d7b8f333ed90 +"@remix-run/router@npm:1.23.3": + version: 1.23.3 + resolution: "@remix-run/router@npm:1.23.3" + checksum: 10c0/73622465dd9e9a2c5a7ced7d1151ea6e9195a8a979c99b4f70a67093eeff7f339daf03a7c6304709a9c27deb2081a8fff6737663aacb33529c1a12a0a0827a17 languageName: node linkType: hard @@ -3841,13 +3903,6 @@ __metadata: languageName: node linkType: hard -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d - languageName: node - linkType: hard - "available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" @@ -3878,6 +3933,15 @@ __metadata: languageName: node linkType: hard +"bidi-js@npm:^1.0.3": + version: 1.0.3 + resolution: "bidi-js@npm:1.0.3" + dependencies: + require-from-string: "npm:^2.0.2" + checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -4103,15 +4167,6 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 - languageName: node - linkType: hard - "commander@npm:^13.1.0": version: 13.1.0 resolution: "commander@npm:13.1.0" @@ -4151,6 +4206,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + "css-what@npm:^6.1.0": version: 6.2.2 resolution: "css-what@npm:6.2.2" @@ -4172,16 +4237,6 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^4.0.1": - version: 4.3.0 - resolution: "cssstyle@npm:4.3.0" - dependencies: - "@asamuzakjp/css-color": "npm:^3.1.1" - rrweb-cssom: "npm:^0.8.0" - checksum: 10c0/770ccb288a99257fd0d5b129e03878f848e922d3b017358acb02e8dd530e8f0c7c6f74e6ae5367d715e2da36a490a734b4177fc1b78f3f08eca25f204a56a692 - languageName: node - linkType: hard - "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -4203,13 +4258,13 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^5.0.0": - version: 5.0.0 - resolution: "data-urls@npm:5.0.0" +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" dependencies: - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.0.0" - checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f languageName: node linkType: hard @@ -4298,10 +4353,10 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.3": - version: 10.5.0 - resolution: "decimal.js@npm:10.5.0" - checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3 +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa languageName: node linkType: hard @@ -4393,13 +4448,6 @@ __metadata: languageName: node linkType: hard -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 - languageName: node - linkType: hard - "dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" @@ -4499,10 +4547,10 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.5.0": - version: 4.5.0 - resolution: "entities@npm:4.5.0" - checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 +"entities@npm:^8.0.0": + version: 8.0.0 + resolution: "entities@npm:8.0.0" + checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d languageName: node linkType: hard @@ -5430,19 +5478,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:4.0.4": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695 - languageName: node - linkType: hard - "framer-motion@npm:^11.18.2": version: 11.18.2 resolution: "framer-motion@npm:11.18.2" @@ -5761,12 +5796,12 @@ __metadata: languageName: node linkType: hard -"html-encoding-sniffer@npm:^4.0.0": - version: 4.0.0 - resolution: "html-encoding-sniffer@npm:4.0.0" +"html-encoding-sniffer@npm:^6.0.0": + version: 6.0.0 + resolution: "html-encoding-sniffer@npm:6.0.0" dependencies: - whatwg-encoding: "npm:^3.1.1" - checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140 + "@exodus/bytes": "npm:^1.6.0" + checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025 languageName: node linkType: hard @@ -5784,7 +5819,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": +"http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -5794,7 +5829,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": +"https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -5820,7 +5855,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -6355,37 +6390,37 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:^24.0.0": - version: 24.1.3 - resolution: "jsdom@npm:24.1.3" +"jsdom@npm:29.1.1": + version: 29.1.1 + resolution: "jsdom@npm:29.1.1" dependencies: - cssstyle: "npm:^4.0.1" - data-urls: "npm:^5.0.0" - decimal.js: "npm:^10.4.3" - form-data: "npm:^4.0.0" - html-encoding-sniffer: "npm:^4.0.0" - http-proxy-agent: "npm:^7.0.2" - https-proxy-agent: "npm:^7.0.5" + "@asamuzakjp/css-color": "npm:^5.1.11" + "@asamuzakjp/dom-selector": "npm:^7.1.1" + "@bramus/specificity": "npm:^2.4.2" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3" + "@exodus/bytes": "npm:^1.15.0" + css-tree: "npm:^3.2.1" + data-urls: "npm:^7.0.0" + decimal.js: "npm:^10.6.0" + html-encoding-sniffer: "npm:^6.0.0" is-potential-custom-element-name: "npm:^1.0.1" - nwsapi: "npm:^2.2.12" - parse5: "npm:^7.1.2" - rrweb-cssom: "npm:^0.7.1" + lru-cache: "npm:^11.3.5" + parse5: "npm:^8.0.1" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^4.1.4" + tough-cookie: "npm:^6.0.1" + undici: "npm:^7.25.0" w3c-xmlserializer: "npm:^5.0.0" - webidl-conversions: "npm:^7.0.0" - whatwg-encoding: "npm:^3.1.1" - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.0.0" - ws: "npm:^8.18.0" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.1" xml-name-validator: "npm:^5.0.0" peerDependencies: - canvas: ^2.11.2 + canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true - checksum: 10c0/e48b342afacd7418a23dac204a62deea729c50f4d072a7c04c09fd32355fdb4335f8779fa79fd0277a2dbeb2d356250a950955719d00047324b251233b11277f + checksum: 10c0/20e2174b09d9d06393cb48e1392b7a1cb7191d6656a6f7b3b8fbf9853b4ab0ef60b4a42c2c55f71b55ca5da50ffa75bcdc6986210963182e7993c6f9cd4f499b languageName: node linkType: hard @@ -6595,6 +6630,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.3.5": + version: 11.5.1 + resolution: "lru-cache@npm:11.5.1" + checksum: 10c0/7b341cea79a8efe9c6a6f20c8757a77eca5b25d7ff983ccf4e11e547b81f6787824baa1c84705251dff84ab4ffac85717ac354b9d02e465f86a9f8b166409979 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -6677,6 +6719,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "media-query-parser@npm:^2.0.2": version: 2.0.2 resolution: "media-query-parser@npm:2.0.2" @@ -6710,22 +6759,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa - languageName: node - linkType: hard - -"mime-types@npm:^2.1.12": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 - languageName: node - linkType: hard - "mimic-fn@npm:^4.0.0": version: 4.0.0 resolution: "mimic-fn@npm:4.0.0" @@ -7015,13 +7048,6 @@ __metadata: languageName: node linkType: hard -"nwsapi@npm:^2.2.12": - version: 2.2.20 - resolution: "nwsapi@npm:2.2.20" - checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297 - languageName: node - linkType: hard - "object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -7205,12 +7231,12 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.1.2": - version: 7.2.1 - resolution: "parse5@npm:7.2.1" +"parse5@npm:^8.0.1": + version: 8.0.1 + resolution: "parse5@npm:8.0.1" dependencies: - entities: "npm:^4.5.0" - checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80 + entities: "npm:^8.0.0" + checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f languageName: node linkType: hard @@ -7604,27 +7630,27 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.30.2": - version: 6.30.2 - resolution: "react-router-dom@npm:6.30.2" +"react-router-dom@npm:6.30.4": + version: 6.30.4 + resolution: "react-router-dom@npm:6.30.4" dependencies: - "@remix-run/router": "npm:1.23.1" - react-router: "npm:6.30.2" + "@remix-run/router": "npm:1.23.3" + react-router: "npm:6.30.4" peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 10c0/d0c6edf4e2aa7639b4a4f64a7747f03d8861bdf4857e8981b1cff1451b7cb91fcdcd7e315a6e3df910271b2f5071825d2aec218d5f7890f2269fc074f198e42a + checksum: 10c0/1b25ab26a288da852f7b58eec5c14b3f37919e6773a557cd846d3a05d7a7d890c8d49bda93c0a7f497f240df1df8b0ad50635f990aafb55f2fc545ad8269a822 languageName: node linkType: hard -"react-router@npm:6.30.2": - version: 6.30.2 - resolution: "react-router@npm:6.30.2" +"react-router@npm:6.30.4": + version: 6.30.4 + resolution: "react-router@npm:6.30.4" dependencies: - "@remix-run/router": "npm:1.23.1" + "@remix-run/router": "npm:1.23.3" peerDependencies: react: ">=16.8" - checksum: 10c0/cff5ea92d248d2230adc46d4e2ed3fbeddfaf1ae2e63411da8b7ea6ddc207d71dbc522c05c492e671e553e2153934f4ab180ac02bd36205b274e097f2cfe6fc4 + checksum: 10c0/fb6de7d1002bcab9ea12c4072d93792eca494f1e4f30cfec334ccb6523756d23ce3e093c7c1241399296cebed94789a3fb89f96ee76004e0e746458a8f6bab33 languageName: node linkType: hard @@ -7707,6 +7733,13 @@ __metadata: languageName: node linkType: hard +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -7967,20 +8000,6 @@ __metadata: languageName: node linkType: hard -"rrweb-cssom@npm:^0.7.1": - version: 0.7.1 - resolution: "rrweb-cssom@npm:0.7.1" - checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e - languageName: node - linkType: hard - -"rrweb-cssom@npm:^0.8.0": - version: 0.8.0 - resolution: "rrweb-cssom@npm:0.8.0" - checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -8628,6 +8647,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.4.2": + version: 7.4.2 + resolution: "tldts-core@npm:7.4.2" + checksum: 10c0/e8e02a43f6823ea1beab8f5a9da370b9a6cbf1a942d4f7d828700e65f03a119348f8d19faa95b599f3ca76fcb5fe5c4611d5b9c274f5a58c7487d342f6083a06 + languageName: node + linkType: hard + +"tldts@npm:^7.0.5": + version: 7.4.2 + resolution: "tldts@npm:7.4.2" + dependencies: + tldts-core: "npm:^7.4.2" + bin: + tldts: bin/cli.js + checksum: 10c0/68f7ec58c9ea41f1583a6384f0fdf1b33d2d8ee55e7d403ec5cf9de4a7226a25c585b5ce1a73de8ddc9abbbe7b783bb3554d1c811fd47fceb0d5511e06d0d766 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -8649,12 +8686,21 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^5.1.0": - version: 5.1.0 - resolution: "tr46@npm:5.1.0" +"tough-cookie@npm:^6.0.1": + version: 6.0.1 + resolution: "tough-cookie@npm:6.0.1" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d + languageName: node + linkType: hard + +"tr46@npm:^6.0.0": + version: 6.0.0 + resolution: "tr46@npm:6.0.0" dependencies: punycode: "npm:^2.3.1" - checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8 + checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e languageName: node linkType: hard @@ -8691,7 +8737,7 @@ __metadata: globals: "npm:^16.0.0" husky: "npm:^9.0.0" jest-extended: "npm:^4.0.2" - jsdom: "npm:^24.0.0" + jsdom: "npm:29.1.1" lint-staged: "npm:^15.0.0" lodash: "npm:4.18.1" msw: "npm:^2.1.7" @@ -8704,7 +8750,7 @@ __metadata: react-helmet-async: "npm:^2.0.4" react-icons: "npm:^5.0.1" react-infinite-scroll-hook: "npm:^4.1.1" - react-router-dom: "npm:6.30.2" + react-router-dom: "npm:6.30.4" swr: "npm:^2.2.4" typescript: "npm:^5.2.2" typescript-eslint: "npm:^8.38.0" @@ -8888,6 +8934,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:7.27.0": + version: 7.27.0 + resolution: "undici@npm:7.27.0" + checksum: 10c0/6fd15a81b0ca177b2667738b830ed175363e5e2164e992251d03aaee6c6517098b930085bd68b8fe5911920371076526657de035e07dc72377b9c5c731b90f0b + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -9266,36 +9319,28 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^7.0.0": - version: 7.0.0 - resolution: "webidl-conversions@npm:7.0.0" - checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46 languageName: node linkType: hard -"whatwg-encoding@npm:^3.1.1": - version: 3.1.1 - resolution: "whatwg-encoding@npm:3.1.1" +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4 + languageName: node + linkType: hard + +"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1": + version: 16.0.1 + resolution: "whatwg-url@npm:16.0.1" dependencies: - iconv-lite: "npm:0.6.3" - checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e - languageName: node - linkType: hard - -"whatwg-mimetype@npm:^4.0.0": - version: 4.0.0 - resolution: "whatwg-mimetype@npm:4.0.0" - checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df - languageName: node - linkType: hard - -"whatwg-url@npm:^14.0.0": - version: 14.2.0 - resolution: "whatwg-url@npm:14.2.0" - dependencies: - tr46: "npm:^5.1.0" - webidl-conversions: "npm:^7.0.0" - checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272 + "@exodus/bytes": "npm:^1.11.0" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2 languageName: node linkType: hard @@ -9445,21 +9490,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0": - version: 8.18.1 - resolution: "ws@npm:8.18.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa - languageName: node - linkType: hard - "xml-name-validator@npm:^5.0.0": version: 5.0.0 resolution: "xml-name-validator@npm:5.0.0" From 5404f6fb25c74ee31910f70f97f0ee0bd20f1b61 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:22:05 +0200 Subject: [PATCH 27/32] Bump axios to v1.17.0 --- webui/package.json | 2 +- webui/yarn.lock | 35 +++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/webui/package.json b/webui/package.json index a1e0b5ae0a..3fae976adc 100644 --- a/webui/package.json +++ b/webui/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@quasar/extras": "^1.16.12", - "axios": "1.15.1", + "axios": "1.17.0", "chart.js": "^4.4.1", "core-js": "^3.35.1", "dot-prop": "^8.0.2", diff --git a/webui/yarn.lock b/webui/yarn.lock index 0c6ee58cb5..9883515301 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -1939,6 +1939,13 @@ acorn@^8.11.0, acorn@^8.12.1, acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2163,13 +2170,14 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.15.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.1.tgz#075420b785da8adbdf545785b69f90c926b28542" - integrity sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg== +axios@1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.17.0.tgz#ae5a1164a4f719942cd73c67e6a3f62d3ccb8f2b" + integrity sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw== dependencies: - follow-redirects "^1.15.11" + follow-redirects "^1.16.0" form-data "^4.0.5" + https-proxy-agent "^5.0.1" proxy-from-env "^2.1.0" b4a@^1.6.4: @@ -2744,6 +2752,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@4: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3580,7 +3595,7 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -follow-redirects@^1.15.11: +follow-redirects@^1.16.0: version "1.16.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== @@ -3920,6 +3935,14 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + human-signals@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" From 6ddda321848c5267f774c32d57af41a054ab52b5 Mon Sep 17 00:00:00 2001 From: Anatole Lucet Date: Fri, 5 Jun 2026 12:16:08 +0200 Subject: [PATCH 28/32] Fix BackendTLSPolicy status update Co-authored-by: Kevin Pollet --- pkg/provider/kubernetes/gateway/client.go | 13 ++++++++++--- pkg/provider/kubernetes/gateway/httproute.go | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index 37c9a85dad..7090516c9c 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -720,20 +720,27 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy ancestorStatuses := make([]gatev1.PolicyAncestorStatus, len(status.Ancestors)) copy(ancestorStatuses, status.Ancestors) - // keep statuses added by other gateway controllers, - // and statuses for Traefik gateway controller but not for the same Gateway as the one in parameter (AncestorRef). for _, ancestorStatus := range currentPolicy.Status.Ancestors { + // Keep statuses added by other gateway controllers. if ancestorStatus.ControllerName != controllerName { ancestorStatuses = append(ancestorStatuses, ancestorStatus) continue } + + // Keep statuses added by Traefik for other ancestors. + // A BackendTLSPolicy can target services attached to different listeners. + if !slices.ContainsFunc(status.Ancestors, func(s gatev1.PolicyAncestorStatus) bool { + return reflect.DeepEqual(s.AncestorRef, ancestorStatus.AncestorRef) + }) { + ancestorStatuses = append(ancestorStatuses, ancestorStatus) + } } if len(ancestorStatuses) > 16 { return fmt.Errorf("failed to update BackendTLSPolicy %s/%s status: PolicyAncestor statuses count exceeds 16", policy.Namespace, policy.Name) } - // do not update status when nothing has changed. + // Do not update status when nothing has changed. if policyAncestorStatusesEqual(currentPolicy.Status.Ancestors, ancestorStatuses) { return nil } diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index e9c9875e86..088c3dc8e8 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -451,6 +451,12 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route var serversTransport *dynamic.ServersTransport for _, policy := range backendTLSPolicies { for _, targetRef := range policy.Spec.TargetRefs { + // Skip targetRefs that doesn't match the backendRef, + // since a BackendTLSPolicy can select multiple services. + if targetRef.Name != backendRef.Name { + continue + } + // Skip the targetRef if the sectionName doesn't match the backendRef port. if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) { continue } From b6bb80f8ff7e564f47acebade9f78e01f689264d Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Fri, 5 Jun 2026 14:36:05 +0200 Subject: [PATCH 29/32] Fix snicheck with keepalive --- .golangci.yml | 4 +- pkg/server/conncontext.go | 29 +++++++++++ pkg/server/conncontext_test.go | 26 ++++++++++ pkg/server/router/tcp/manager.go | 22 ++++---- pkg/server/server_entrypoint_tcp.go | 80 ++++++++++++++--------------- 5 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 pkg/server/conncontext.go create mode 100644 pkg/server/conncontext_test.go diff --git a/.golangci.yml b/.golangci.yml index 02749c267d..feefe53c34 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -327,7 +327,9 @@ linters: text: 'SA1008: keys in http.Header are canonicalized, "x-user" is not canonical; fix the constant or use http.CanonicalHeaderKey' - path: pkg/middlewares/auth/digest_auth_test.go text: 'SA1008: keys in http.Header are canonicalized, "x-user" is not canonical; fix the constant or use http.CanonicalHeaderKey' - + - path: pkg/server/conncontext.go + linters: + - fatcontext paths: - pkg/provider/kubernetes/crd/generated/ diff --git a/pkg/server/conncontext.go b/pkg/server/conncontext.go new file mode 100644 index 0000000000..a93290c7a2 --- /dev/null +++ b/pkg/server/conncontext.go @@ -0,0 +1,29 @@ +package server + +import ( + "context" + "net" +) + +type connContextFunc func(context.Context, net.Conn) context.Context + +type multipleConnContext struct { + fns []connContextFunc +} + +func (m *multipleConnContext) AddConnContextFunc(fn connContextFunc) { + m.fns = append(m.fns, fn) +} + +func (m *multipleConnContext) Build() connContextFunc { + if len(m.fns) == 0 { + return nil + } + + return func(ctx context.Context, c net.Conn) context.Context { + for _, contextFunc := range m.fns { + ctx = contextFunc(ctx, c) + } + return ctx + } +} diff --git a/pkg/server/conncontext_test.go b/pkg/server/conncontext_test.go new file mode 100644 index 0000000000..6160ea358e --- /dev/null +++ b/pkg/server/conncontext_test.go @@ -0,0 +1,26 @@ +package server + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +type keyTest string + +func TestConnContext(t *testing.T) { + var connContext multipleConnContext + connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context { + return context.WithValue(ctx, keyTest("test"), "test") + }) + connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context { + return context.WithValue(ctx, keyTest("test2"), "test2") + }) + + ctx := connContext.Build()(context.Background(), nil) + + require.Equal(t, "test", ctx.Value(keyTest("test"))) + require.Equal(t, "test2", ctx.Value(keyTest("test2"))) +} diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index 0a17dfb272..de9e9e2762 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -114,6 +114,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName)) logger := log.FromContext(ctxRouter) + // Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator + // we also have to handle it here to be able to mark the router in error. + tlsOptionsName := traefiktls.DefaultTLSConfigName + if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { + tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) + } + domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule) if err != nil { routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err) @@ -153,17 +160,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string // # When a request for "/foo" comes, even though it won't be routed by httpRouter2, // # if its SNI is set to foo.com, myTLSOptions will be used for the TLS connection. // # Otherwise, it will fallback to the default TLS config. - logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule) + if tlsOptionsName != traefiktls.DefaultTLSConfigName { + logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule) + routerHTTPConfig.AddError(fmt.Errorf("no domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName), false) + } } - // Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator - // we also have to handle it here to be able to mark the router in error. - tlsOptionsName := traefiktls.DefaultTLSConfigName - if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { - tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) - } - - if routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName { + if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName { + logger.Warn("Found different TLS options for routers on the same host, so using the default TLS options instead.") routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false) } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 39dd3b42fe..252ec0514a 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -601,6 +601,44 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler = denyFragment(handler) + var connContext multipleConnContext + connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context { + // This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM. + ctx = service.AddTransportOnContext(ctx) + + if tlsConn, ok := c.(*tls.Conn); ok { + if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok { + return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName) + } + } + + return ctx + }) + + if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { + connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context { + cState := &connState{Start: time.Now()} + if debugConnection { + clientConnectionStatesMu.Lock() + clientConnectionStates[getConnKey(c)] = cState + clientConnectionStatesMu.Unlock() + } + + return context.WithValue(ctx, connStateKey, cState) + }) + } + + var connState func(c net.Conn, state http.ConnState) + if debugConnection { + connState = func(c net.Conn, state http.ConnState) { + clientConnectionStatesMu.Lock() + if clientConnectionStates[getConnKey(c)] != nil { + clientConnectionStates[getConnKey(c)].State = state.String() + } + clientConnectionStatesMu.Unlock() + } + } + serverHTTP := &http.Server{ Protocols: &protocols, Handler: handler, @@ -611,46 +649,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati HTTP2: &http.HTTP2Config{ MaxConcurrentStreams: int(configuration.HTTP2.MaxConcurrentStreams), }, - ConnContext: func(ctx context.Context, c net.Conn) context.Context { - if tlsConn, ok := c.(*tls.Conn); ok { - if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok { - return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName) - } - } - - return ctx - }, - } - if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { - serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context { - cState := &connState{Start: time.Now()} - if debugConnection { - clientConnectionStatesMu.Lock() - clientConnectionStates[getConnKey(c)] = cState - clientConnectionStatesMu.Unlock() - } - return context.WithValue(ctx, connStateKey, cState) - } - - if debugConnection { - serverHTTP.ConnState = func(c net.Conn, state http.ConnState) { - clientConnectionStatesMu.Lock() - if clientConnectionStates[getConnKey(c)] != nil { - clientConnectionStates[getConnKey(c)].State = state.String() - } - clientConnectionStatesMu.Unlock() - } - } - } - - prevConnContext := serverHTTP.ConnContext - serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context { - // This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM. - ctx = service.AddTransportOnContext(ctx) - if prevConnContext != nil { - return prevConnContext(ctx, c) - } - return ctx + ConnContext: connContext.Build(), + ConnState: connState, } listener := newHTTPForwarder(ln) From ba8830fdef9ea6e9f0497b06128b36cf5d4a2760 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 5 Jun 2026 14:48:04 +0200 Subject: [PATCH 30/32] Prepare release v2.11.49 --- CHANGELOG.md | 8 ++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 335030a399..b3b6e5402b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [v2.11.49](https://github.com/traefik/traefik/tree/v2.11.49) (2026-06-05) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.48...v2.11.49) + +**Bug fixes:** +- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte) +- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07) +- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens) + ## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04) [All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 6f161f1556..4bc6c9e476 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.48 +# example new bugfix v2.11.49 CurrentRef = "v2.11" -PreviousRef = "v2.11.47" +PreviousRef = "v2.11.48" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.48" +FutureCurrentRefName = "v2.11.49" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 1137f1f8076b3bd7966464769e9a65a554e7e894 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 5 Jun 2026 15:20:05 +0200 Subject: [PATCH 31/32] Prepare release v3.6.20 --- CHANGELOG.md | 11 +++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4058426560..369b2989d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [v3.6.20](https://github.com/traefik/traefik/tree/v3.6.20) (2026-06-05) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.19...v3.6.20) + +**Bug fixes:** +- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024) +- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07) +- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet) +- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte) +- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07) +- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens) + ## [v2.11.49](https://github.com/traefik/traefik/tree/v2.11.49) (2026-06-05) [All Commits](https://github.com/traefik/traefik/compare/v2.11.48...v2.11.49) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index be68e36919..c5bf100f8c 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.6.19 +# example new bugfix v3.6.20 CurrentRef = "v3.6" -PreviousRef = "v3.6.18" +PreviousRef = "v3.6.19" BaseBranch = "v3.6" -FutureCurrentRefName = "v3.6.19" +FutureCurrentRefName = "v3.6.20" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 74b6408475ffbba8a8c02a0abd0ac7e608d72087 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 5 Jun 2026 15:56:04 +0200 Subject: [PATCH 32/32] Prepare release v3.7.4 --- CHANGELOG.md | 11 +++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8224d33dfa..83301e2565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4) + +**Bug fixes:** +- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024) +- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07) +- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet) +- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte) +- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07) +- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens) + ## [v3.6.20](https://github.com/traefik/traefik/tree/v3.6.20) (2026-06-05) [All Commits](https://github.com/traefik/traefik/compare/v3.6.19...v3.6.20) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index faae6153a2..80f7df1e50 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.7.3 +# example new bugfix v3.7.4 CurrentRef = "v3.7" -PreviousRef = "v3.7.2" +PreviousRef = "v3.7.3" BaseBranch = "v3.7" -FutureCurrentRefName = "v3.7.3" +FutureCurrentRefName = "v3.7.4" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000