From 7494b5c9ff9e4304654f04d54962ccc500ee0da2 Mon Sep 17 00:00:00 2001 From: LBF38 Date: Mon, 23 Feb 2026 10:04:10 +0100 Subject: [PATCH 1/7] Fix case sensitivity on x-forwarded headers for Connection --- docs/content/routing/entrypoints.md | 11 ++- .../forwardedheaders/forwarded_header.go | 16 ++- .../forwardedheaders/forwarded_header_test.go | 99 ++++++++++++++++++- 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index d6126df56..abaaaa3dd 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -127,6 +127,9 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. trustedIPs: - "127.0.0.1" - "192.168.0.1" + connection: + - X-Foo + - foobar http: encodedCharacters: allowEncodedSlash: false @@ -161,6 +164,7 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. [entryPoints.name.forwardedHeaders] insecure = true trustedIPs = ["127.0.0.1", "192.168.0.1"] + connection = ["X-Foo", "foobar"] [entryPoints.name.http.encodedCharacters] allowEncodedSlash = false allowEncodedBackSlash = false @@ -185,6 +189,7 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. --entryPoints.name.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1 --entryPoints.name.forwardedHeaders.insecure=true --entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1 + --entryPoints.name.forwardedHeaders.connection=X-Foo,foobar --entryPoints.name.http.encodedCharacters.allowEncodedSlash=false --entryPoints.name.http.encodedCharacters.allowEncodedBackSlash=false --entryPoints.name.http.encodedCharacters.allowEncodedNullCharacter=false @@ -452,6 +457,7 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward The removal happens as soon as the request is handled by Traefik, thus the removed headers are not available when the request passes through the middleware chain. The `connection` option lists the Connection headers allowed to passthrough the middleware chain before their removal. + The headers defined by this option are not case-sensitive. The middleware will automatically canonicalize them. ```yaml tab="File (YAML)" ## Static configuration @@ -460,6 +466,7 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward address: ":80" forwardedHeaders: connection: + - X-Foo - foobar ``` @@ -470,13 +477,13 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward address = ":80" [entryPoints.web.forwardedHeaders] - connection = ["foobar"] + connection = ["X-Foo", "foobar"] ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.forwardedHeaders.connection=foobar + --entryPoints.web.forwardedHeaders.connection=X-Foo,foobar ``` ### Transport diff --git a/pkg/middlewares/forwardedheaders/forwarded_header.go b/pkg/middlewares/forwardedheaders/forwarded_header.go index 775bcb5f8..73bc948f8 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header.go @@ -71,10 +71,15 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin hostname = "localhost" } + canonicalConnectionHeaders := make([]string, len(connectionHeaders)) + for i, header := range connectionHeaders { + canonicalConnectionHeaders[i] = http.CanonicalHeaderKey(header) + } + return &XForwarded{ insecure: insecure, trustedIPs: trustedIPs, - connectionHeaders: connectionHeaders, + connectionHeaders: canonicalConnectionHeaders, ipChecker: ipChecker, next: next, hostname: hostname, @@ -209,22 +214,23 @@ func (x *XForwarded) removeConnectionHeaders(req *http.Request) { for _, f := range req.Header[connection] { for sf := range strings.SplitSeq(f, ",") { if sf = textproto.TrimString(sf); sf != "" { + key := http.CanonicalHeaderKey(sf) // Connection header cannot dictate to remove X- headers managed by Traefik, // as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1, // A proxy or gateway MUST ... and then remove the Connection header field itself // (or replace it with the intermediary's own connection options for the forwarded message). - if slices.Contains(xHeaders, sf) { + if slices.Contains(xHeaders, key) { continue } // Keep headers allowed through the middleware chain. - if slices.Contains(x.connectionHeaders, sf) { - connectionHopByHopHeaders = append(connectionHopByHopHeaders, sf) + if slices.Contains(x.connectionHeaders, key) { + connectionHopByHopHeaders = append(connectionHopByHopHeaders, key) continue } // Apply Connection header option. - req.Header.Del(sf) + delete(req.Header, key) } } } diff --git a/pkg/middlewares/forwardedheaders/forwarded_header_test.go b/pkg/middlewares/forwardedheaders/forwarded_header_test.go index 8289e8c69..505349781 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header_test.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -470,6 +471,100 @@ func TestServeHTTP(t *testing.T) { connection: "", }, }, + { + desc: "Trusted (insecure) and Connection: Testing case sensitivity on connection Headers param", + insecure: true, + connectionHeaders: []string{ + strings.ToLower(xForwardedProto), + strings.ToLower(xForwardedFor), + strings.ToLower(xForwardedURI), + strings.ToLower(xForwardedMethod), + strings.ToLower(xForwardedHost), + strings.ToLower(xForwardedPort), + strings.ToLower(xForwardedTLSClientCert), + strings.ToLower(xForwardedTLSClientCertInfo), + strings.ToLower(xForwardedPrefix), + strings.ToLower(xRealIP), + }, + incomingHeaders: map[string][]string{ + connection: { + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xForwardedPrefix, + xRealIP, + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xForwardedPrefix: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "foo", + xForwardedFor: "foo", + xForwardedURI: "foo", + xForwardedMethod: "foo", + xForwardedHost: "foo", + xForwardedPort: "foo", + xForwardedTLSClientCert: "foo", + xForwardedTLSClientCertInfo: "foo", + xForwardedPrefix: "foo", + xRealIP: "foo", + connection: "", + }, + }, + { + desc: "Trusted (insecure) and Connection: Testing case sensitivity on X- forwarded headers", + insecure: true, + incomingHeaders: map[string][]string{ + connection: { + strings.ToLower(xForwardedProto), + strings.ToLower(xForwardedFor), + strings.ToLower(xForwardedURI), + strings.ToLower(xForwardedMethod), + strings.ToLower(xForwardedHost), + strings.ToLower(xForwardedPort), + strings.ToLower(xForwardedTLSClientCert), + strings.ToLower(xForwardedTLSClientCertInfo), + strings.ToLower(xForwardedPrefix), + strings.ToLower(xRealIP), + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xForwardedPrefix: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "foo", + xForwardedFor: "foo", + xForwardedURI: "foo", + xForwardedMethod: "foo", + xForwardedHost: "foo", + xForwardedPort: "foo", + xForwardedTLSClientCert: "foo", + xForwardedTLSClientCertInfo: "foo", + xForwardedPrefix: "foo", + xRealIP: "foo", + connection: "", + }, + }, { desc: "Connection: one remove, and one passthrough header", connectionHeaders: []string{ @@ -478,12 +573,14 @@ func TestServeHTTP(t *testing.T) { incomingHeaders: map[string][]string{ connection: { "foo", + "bar", }, "Foo": {"bar"}, "Bar": {"foo"}, }, expectedHeaders: map[string]string{ - "Bar": "foo", + "Bar": "", + "Foo": "bar", }, }, } From 288e4e2e2b4d632544a9106c24f2c3ffdafcf068 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:04:04 +0100 Subject: [PATCH 2/7] Upgrade golangci-lint --- .github/workflows/validate.yaml | 2 +- .golangci.yml | 2 +- cmd/internal/gen/centrifuge.go | 8 ++++---- internal/gendoc.go | 2 +- pkg/provider/acme/local_store_windows.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 027e5f846..57104ea3a 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -7,7 +7,7 @@ on: env: GO_VERSION: '1.25' - GOLANGCI_LINT_VERSION: v2.8.0 + GOLANGCI_LINT_VERSION: v2.10.1 MISSPELL_VERSION: v0.7.0 jobs: diff --git a/.golangci.yml b/.golangci.yml index a0fcc0112..7dd5763be 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -309,7 +309,7 @@ linters: text: 'var-naming: avoid meaningless package names' linters: - revive - - path: (pkg/muxer/http/.+|pkg/provider/http/.+)\.go + - path: ((cmd|pkg)/version/.*|pkg/config/runtime/.*|pkg/log/.*|pkg/(middlewares/)?metrics/.*|pkg/muxer/http/.+|pkg/provider/http/.+|pkg/tls/.+)\.go text: 'var-naming: avoid package names that conflict with Go standard library package names' linters: - revive diff --git a/cmd/internal/gen/centrifuge.go b/cmd/internal/gen/centrifuge.go index 2c0f6e634..5e363d280 100644 --- a/cmd/internal/gen/centrifuge.go +++ b/cmd/internal/gen/centrifuge.go @@ -158,7 +158,7 @@ func (c Centrifuge) run(sc *types.Scope, rootPkg string, pkgName string) map[str func (c Centrifuge) writeStruct(name string, obj *types.Struct, rootPkg string, elt *File) string { b := strings.Builder{} - b.WriteString(fmt.Sprintf("type %s struct {\n", name)) + fmt.Fprintf(&b, "type %s struct {\n", name) for i := range obj.NumFields() { field := obj.Field(i) @@ -175,7 +175,7 @@ func (c Centrifuge) writeStruct(name string, obj *types.Struct, rootPkg string, fType := c.TypeCleaner(field.Type(), rootPkg) if field.Embedded() { - b.WriteString(fmt.Sprintf("\t%s\n", fType)) + fmt.Fprintf(&b, "\t%s\n", fType) continue } @@ -184,10 +184,10 @@ func (c Centrifuge) writeStruct(name string, obj *types.Struct, rootPkg string, continue } - b.WriteString(fmt.Sprintf("\t%s %s", field.Name(), fType)) + fmt.Fprintf(&b, "\t%s %s", field.Name(), fType) if ok { - b.WriteString(fmt.Sprintf(" `json:\"%s\"`", strings.Join(values, ","))) + fmt.Fprintf(&b, " `json:\"%s\"`", strings.Join(values, ",")) } b.WriteString("\n") diff --git a/internal/gendoc.go b/internal/gendoc.go index 116040b1e..83e142a5e 100644 --- a/internal/gendoc.go +++ b/internal/gendoc.go @@ -300,7 +300,7 @@ THIS FILE MUST NOT BE EDITED BY HAND } if w.err != nil { - logger.Fatal(err) + logger.Fatal(w.err) } } diff --git a/pkg/provider/acme/local_store_windows.go b/pkg/provider/acme/local_store_windows.go index 747618eb8..63a903b97 100644 --- a/pkg/provider/acme/local_store_windows.go +++ b/pkg/provider/acme/local_store_windows.go @@ -3,7 +3,7 @@ package acme import "os" // CheckFile checks file content size -// Do not check file permissions on Windows right now +// Do not check file permissions on Windows right now. func CheckFile(name string) (bool, error) { f, err := os.Open(name) if err != nil { From 4595c7a9201aae268d04ed06e171fa96b62dfd3a Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:30:06 +0100 Subject: [PATCH 3/7] Add maxResponseBodySize configuration to forwardAuth middleware --- docs/content/middlewares/http/forwardauth.md | 61 ++++++++++++++ docs/content/migration/v2.md | 11 +++ .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 1 + .../reference/dynamic-configuration/file.yaml | 1 + .../kubernetes-crd-definition-v1.yml | 5 ++ .../reference/dynamic-configuration/kv-ref.md | 1 + .../marathon-labels.json | 1 + .../traefik.io_middlewares.yaml | 5 ++ integration/fixtures/k8s/01-traefik-crd.yml | 5 ++ pkg/config/dynamic/middlewares.go | 2 + pkg/config/dynamic/zz_generated.deepcopy.go | 5 ++ pkg/config/label/label_test.go | 4 + pkg/middlewares/auth/forward.go | 43 +++++++++- pkg/middlewares/auth/forward_test.go | 83 +++++++++++++++++++ pkg/provider/kubernetes/crd/kubernetes.go | 4 + .../crd/traefikio/v1alpha1/middleware.go | 2 + .../v1alpha1/zz_generated.deepcopy.go | 5 ++ pkg/provider/kv/kv_test.go | 2 + pkg/redactor/redactor_config_test.go | 1 + .../testdata/anonymized-dynamic-config.json | 3 +- .../testdata/secured-dynamic-config.json | 3 +- 22 files changed, 244 insertions(+), 5 deletions(-) diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 1b2ae3eb2..b98184cf8 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -637,4 +637,65 @@ http: [http.middlewares.test-auth.forwardAuth.tls] insecureSkipVerify: true ``` + +### `maxResponseBodySize` + +_Optional, Default=-1_ + +The `maxResponseBodySize` option defines the maximum allowed response body size in bytes from the authentication server. +If the response body exceeds the configured limit, the request is rejected with a 401 (Unauthorized) status. +If left unset, the request body size is unrestricted which can have performance or security implications. + +```yaml tab="Docker" +labels: + - "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-auth +spec: + forwardAuth: + address: https://example.com/auth + maxResponseBodySize: 10000 +``` + +```yaml tab="Consul Catalog" +- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize": "10000" +} +``` + +```yaml tab="Rancher" +labels: + - "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000" +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-auth: + forwardAuth: + address: "https://example.com/auth" + maxResponseBodySize: 10000 +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-auth.forwardAuth] + address = "https://example.com/auth" + maxResponseBodySize = 10000 +``` + +!!! warning + + It is strongly recommended to set this option to a suitable value. + Not setting it (or setting it to `-1`) allows unlimited response body sizes which can lead to DoS attacks and memory exhaustion. + {% include-markdown "includes/traefik-for-business-applications.md" %} diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 586952361..8321f1d04 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -764,3 +764,14 @@ in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3). Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation for more details. + +## v2.11.38 + +### `maxResponseBodySize` configuration on ForwardAuth middleware + +In `v2.11.38`, a new `maxResponseBodySize` option has been added to the ForwardAuth middleware configuration. +The default value for this option is -1, which means there is no limit to the response body size. +However, it is strongly recommended to set this option to a suitable value to avoid performance and security issues, +such as DoS attacks and memory exhaustion. + +Please check out the [ForwardAuth](../middlewares/http/forwardauth.md#maxresponsebodysize) middleware documentation for more details. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 02ac9eb1f..5609baa15 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -32,6 +32,7 @@ - "traefik.http.middlewares.middleware10.forwardauth.authrequestheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex=foobar" +- "traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize=42" - "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar" - "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.cert=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index fc04a29c6..9469eb118 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -155,6 +155,7 @@ authResponseHeaders = ["foobar", "foobar"] authResponseHeadersRegex = "foobar" authRequestHeaders = ["foobar", "foobar"] + maxResponseBodySize = 42 [http.middlewares.Middleware10.forwardAuth.tls] ca = "foobar" caOptional = true diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index ec861d0a2..f4820dc2f 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -176,6 +176,7 @@ http: authRequestHeaders: - foobar - foobar + maxResponseBodySize: 42 Middleware11: headers: customRequestHeaders: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index a968bea60..5173576a3 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1001,6 +1001,11 @@ spec: AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex type: string + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 0b2f7c5f0..2b23e402b 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -43,6 +43,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/0` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/1` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeadersRegex` | `foobar` | +| `traefik/http/middlewares/Middleware10/forwardAuth/maxResponseBodySize` | `42` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/caOptional` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/cert` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 41fa198bd..2a3f2a366 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -32,6 +32,7 @@ "traefik.http.middlewares.middleware10.forwardauth.authrequestheaders": "foobar, foobar", "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders": "foobar, foobar", "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex": "foobar", +"traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize": "42", "traefik.http.middlewares.middleware10.forwardauth.tls.ca": "foobar", "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional": "true", "traefik.http.middlewares.middleware10.forwardauth.tls.cert": "foobar", diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index defa3a5bf..fd0f7beaa 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -386,6 +386,11 @@ spec: AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex type: string + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index a968bea60..5173576a3 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1001,6 +1001,11 @@ spec: AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex type: string + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 8af6944a0..a1d49f75c 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -216,6 +216,8 @@ type ForwardAuth struct { // AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server. // If not set or empty then all request headers are passed. AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"` + // MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server. + MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty" toml:"maxResponseBodySize,omitempty" yaml:"maxResponseBodySize,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index b8f68c946..c9cf5d446 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -324,6 +324,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.MaxResponseBodySize != nil { + in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize + *out = new(int64) + **out = **in + } return } diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index b4c209e4c..e620ffb7a 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -50,6 +50,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true", "traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar", "traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true", + "traefik.http.middlewares.Middleware7.forwardauth.maxresponsebodysize": "42", "traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true", "traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar", "traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar", @@ -547,6 +548,7 @@ func TestDecodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + MaxResponseBodySize: pointer[int64](42), }, }, "Middleware8": { @@ -1060,6 +1062,7 @@ func TestEncodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + MaxResponseBodySize: pointer[int64](42), }, }, "Middleware8": { @@ -1259,6 +1262,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.MaxResponseBodySize": "42", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index 6004a01fa..cf0801af4 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -47,11 +47,13 @@ type forwardAuth struct { client http.Client trustForwardHeader bool authRequestHeaders []string + maxResponseBodySize int64 } // NewForward creates a forward auth middleware. func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) { - log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName)).Debug("Creating middleware") + logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName)) + logger.Debug("Creating middleware") fa := &forwardAuth{ address: config.Address, @@ -62,6 +64,13 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu authRequestHeaders: config.AuthRequestHeaders, } + if config.MaxResponseBodySize != nil { + fa.maxResponseBodySize = *config.MaxResponseBodySize + } else { + fa.maxResponseBodySize = -1 + logger.Warn("ForwardAuth 'maxResponseBodySize' is not configured, allowing unlimited response body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.") + } + // Ensure our request client does not follow redirects fa.client = http.Client{ CheckRedirect: func(r *http.Request, via []*http.Request) error { @@ -125,9 +134,16 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } defer forwardResponse.Body.Close() - body, readError := io.ReadAll(forwardResponse.Body) + body, readError := fa.readResponseBodyBytes(forwardResponse) if readError != nil { - logger.Debugf("Error reading body %s. Cause: %s", fa.address, readError) + if errors.Is(readError, errResponseBodyTooLarge) { + logger.Debugf("Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize) + + tracing.SetErrorWithEvent(req, "Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize) + rw.WriteHeader(http.StatusUnauthorized) + return + } + logger.Debugf("Error reading body %s", fa.address) tracing.SetErrorWithEvent(req, "Error reading body %s. Cause: %s", fa.address, readError) rw.WriteHeader(http.StatusInternalServerError) @@ -193,6 +209,27 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { fa.next.ServeHTTP(rw, req) } +var errResponseBodyTooLarge = errors.New("response body too large") + +func (fa *forwardAuth) readResponseBodyBytes(res *http.Response) ([]byte, error) { + if fa.maxResponseBodySize < 0 { + return io.ReadAll(res.Body) + } + + body := make([]byte, fa.maxResponseBodySize+1) + n, err := io.ReadFull(res.Body, body) + if errors.Is(err, io.EOF) { + return nil, nil + } + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("reading response body bytes: %w", err) + } + if errors.Is(err, io.ErrUnexpectedEOF) { + return body[:n], nil + } + return nil, errResponseBodyTooLarge +} + func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) { utils.CopyHeaders(forwardReq.Header, req.Header) diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 3dbd4eece..952fc8b75 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -482,6 +482,89 @@ func TestForwardAuthUsesTracing(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) } +func Test_ForwardAuthMaxResponseBodySize(t *testing.T) { + testCases := []struct { + name string + maxResponseBodySize int64 + status int + body string + expectedStatus int + expectedBody string + }{ + { + name: "auth failure, unlimited response body", + maxResponseBodySize: -1, + status: http.StatusForbidden, + body: "Forbidden", + expectedStatus: http.StatusForbidden, + expectedBody: "Forbidden", + }, + { + name: "auth failure, response body exceeds the limit", + maxResponseBodySize: 1, + status: http.StatusForbidden, + body: "Forbidden", + expectedStatus: http.StatusUnauthorized, + expectedBody: "", + }, + { + name: "auth success within limit", + maxResponseBodySize: 100, + status: http.StatusOK, + body: "ok", + expectedStatus: http.StatusOK, + expectedBody: "traefik\n", + }, + { + name: "auth success body exceeds limit", + maxResponseBodySize: 1, + status: http.StatusOK, + body: "large auth response", + expectedStatus: http.StatusUnauthorized, + expectedBody: "", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.status) + fmt.Fprint(w, test.body) + })) + t.Cleanup(server.Close) + + next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "traefik") + })) + + maxResponseBodySize := test.maxResponseBodySize + auth := dynamic.ForwardAuth{ + Address: server.URL, + MaxResponseBodySize: &maxResponseBodySize, + } + + middleware, err := NewForward(t.Context(), next, auth, "maxResponseBodySizeTest") + require.NoError(t, err) + + ts := httptest.NewServer(middleware) + t.Cleanup(ts.Close) + + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + assert.Equal(t, test.expectedStatus, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + require.NoError(t, err) + + assert.Equal(t, test.expectedBody, string(body)) + }) + } +} + type mockBackend struct { opentracing.Tracer } diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 79954288b..eca0a876d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -652,6 +652,10 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef AuthRequestHeaders: auth.AuthRequestHeaders, } + if auth.MaxResponseBodySize != nil { + forwardAuth.MaxResponseBodySize = auth.MaxResponseBodySize + } + if auth.TLS == nil { return forwardAuth, nil } diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index 21c1e3cfb..7e448faae 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go @@ -155,6 +155,8 @@ type ForwardAuth struct { AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"` // TLS defines the configuration used to secure the connection to the authentication server. TLS *ClientTLS `json:"tls,omitempty"` + // MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server. + MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty"` } // ClientTLS holds the client TLS configuration. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index ebd5e9ce2..07f33f0ca 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -215,6 +215,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { *out = new(ClientTLS) **out = **in } + if in.MaxResponseBodySize != nil { + in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize + *out = new(int64) + **out = **in + } return } diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 3d3fdad2f..92038905e 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -83,6 +83,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware08/forwardAuth/tls/cert": "foobar", "traefik/http/middlewares/Middleware08/forwardAuth/address": "foobar", "traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true", + "traefik/http/middlewares/Middleware08/forwardAuth/maxResponseBodySize": "42", "traefik/http/middlewares/Middleware15/redirectScheme/scheme": "foobar", "traefik/http/middlewares/Middleware15/redirectScheme/port": "foobar", "traefik/http/middlewares/Middleware15/redirectScheme/permanent": "true", @@ -427,6 +428,7 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, + MaxResponseBodySize: pointer[int64](42), }, }, "Middleware06": { diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index f96eb04fa..2364494b0 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -287,6 +287,7 @@ func init() { AuthResponseHeaders: []string{"foo"}, AuthResponseHeadersRegex: "foo", AuthRequestHeaders: []string{"foo"}, + MaxResponseBodySize: pointer[int64](42), }, InFlightReq: &dynamic.InFlightReq{ Amount: 42, diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index 8339f58c3..7501d8388 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -247,7 +247,8 @@ "authResponseHeadersRegex": "foo", "authRequestHeaders": [ "foo" - ] + ], + "maxResponseBodySize": 42 }, "inFlightReq": { "amount": 42, diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 352421884..34f072008 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -250,7 +250,8 @@ "authResponseHeadersRegex": "foo", "authRequestHeaders": [ "foo" - ] + ], + "maxResponseBodySize": 42 }, "inFlightReq": { "amount": 42, From 7a3ffcc3d9cb67fe00562d249c703dd3ee2390fb Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Mon, 23 Feb 2026 14:06:05 +0100 Subject: [PATCH 4/7] Fix TLS handshake error handling --- integration/fixtures/simple_ddos.toml | 34 +++++++++++++++++ integration/simple_test.go | 54 +++++++++++++++++++++++++++ pkg/server/router/tcp/router.go | 29 +++++--------- pkg/server/router/tcp/router_test.go | 4 +- 4 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 integration/fixtures/simple_ddos.toml diff --git a/integration/fixtures/simple_ddos.toml b/integration/fixtures/simple_ddos.toml new file mode 100644 index 000000000..c079d3506 --- /dev/null +++ b/integration/fixtures/simple_ddos.toml @@ -0,0 +1,34 @@ +[global] +checkNewVersion = false +sendAnonymousUsage = false + +[api] +insecure = true +[log] +level = "DEBUG" + +[entryPoints] + +[entryPoints.web] +address = ":8000" +[entryPoints.web.transport.respondingTimeouts] +readTimeout="200ms" + + +[entryPoints.tcp] +address = ":8001" +[entryPoints.tcp.transport.respondingTimeouts] +readTimeout="200ms" + + +[providers.file] +filename = "{{ .SelfFilename }}" + + +[tcp.routers.withtls] +rule="HostSNI(`*`)" +service="noop" +[tcp.routers.withtls.tls] + +[[tcp.services.noop.loadBalancer.servers]] +address="127.0.0.1:8080" diff --git a/integration/simple_test.go b/integration/simple_test.go index c2f1392c4..e8938c894 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1562,3 +1562,57 @@ func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { require.NoError(s.T(), err) } } + +func (s *SimpleSuite) TestDDOS() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + file := s.adaptFile("fixtures/simple_ddos.toml", struct{}{}) + + _, output := s.cmdTraefik(withConfigFile(file)) + + defer func() { + if s.T().Failed() { + s.T().Log("---- Traefik Logs ----") + s.T().Log(output) + } + }() + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("HostSNI(`*`)")) + require.NoError(s.T(), err) + + // Try with an http router. + conn, err := net.Dial("tcp", "127.0.0.1:8000") + require.NoError(s.T(), err) + + waitForWritePartial(s.T(), conn) + + // Try with a tcp router only. + conn, err = net.Dial("tcp", "127.0.0.1:8001") + require.NoError(s.T(), err) + + waitForWritePartial(s.T(), conn) +} + +func waitForWritePartial(t *testing.T, conn net.Conn) { + t.Helper() + + end := make(chan struct{}) + go func() { + if _, err := conn.Write([]byte{0x16, 0x03, 0x03, 0x00, 0x10}); err != nil { + require.NoError(t, err) + } + + _, err := conn.Read(make([]byte, 1)) + require.ErrorIs(t, err, io.EOF) + + close(end) + }() + + select { + case <-end: + case <-time.After(500 * time.Millisecond): + t.Fatalf("timeout waiting for connection timeout") + } +} diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 2d9c003fa..8b98c853d 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -6,6 +6,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "io" "net" "net/http" @@ -129,6 +130,11 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { br := bufio.NewReader(conn) hello, err := clientHelloInfo(br) if err != nil { + var opErr *net.OpError + if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || !opErr.Timeout()) { + log.WithoutContext().Debugf("Error while reading client hello: %s", err) + } + conn.Close() return } @@ -367,11 +373,7 @@ type clientHello struct { func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { hdr, err := br.Peek(1) if err != nil { - var opErr *net.OpError - if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || !opErr.Timeout()) { - log.WithoutContext().Debugf("Error while peeking first byte: %s", err) - } - return nil, err + return nil, fmt.Errorf("peeking first byte: %w", err) } // No valid TLS record has a type of 0x80, however SSLv2 handshakes start with an uint16 length @@ -395,20 +397,13 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { const recordHeaderLen = 5 hdr, err = br.Peek(recordHeaderLen) if err != nil { - log.WithoutContext().Errorf("Error while peeking client hello headers: %s", err) - return &clientHello{ - peeked: getPeeked(br), - }, nil + return nil, fmt.Errorf("peeking client hello headers: %w", err) } recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3] if recLen > maxTLSRecordLen { - log.WithoutContext().Debugf("Error while peeking client hello bytes, oversized record: %d", recLen) - return &clientHello{ - isTLS: true, - peeked: getPeeked(br), - }, nil + return nil, fmt.Errorf("peeking client hello bytes, oversized record: %d", recLen) } if recordHeaderLen+recLen > defaultBufSize { @@ -417,11 +412,7 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { helloBytes, err := br.Peek(recordHeaderLen + recLen) if err != nil { - log.WithoutContext().Errorf("Error while peeking client hello bytes: %s", err) - return &clientHello{ - isTLS: true, - peeked: getPeeked(br), - }, nil + return nil, fmt.Errorf("peeking client hello bytes: %w", err) } sni := "" diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 918c21da3..837923e57 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -1125,9 +1125,7 @@ func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) { // With the fix, it returns immediately. select { case r := <-resultCh: - require.NoError(t, r.err) - require.NotNil(t, r.hello) - assert.True(t, r.hello.isTLS) + require.Error(t, r.err) case <-time.After(5 * time.Second): t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped") } From c98fddbd03c95d0da3295528e2b253ab24c3b656 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:08:05 +0100 Subject: [PATCH 5/7] Simplify Go version refs in CI templates --- .github/workflows/build.yaml | 5 ++--- .github/workflows/codeql.yml | 1 + .github/workflows/experimental.yaml | 5 ++--- .github/workflows/release.yaml | 6 ++---- .github/workflows/test-integration.yaml | 13 ++++++------- .github/workflows/test-unit.yaml | 11 ++++------- .github/workflows/validate.yaml | 13 ++++++------- go.mod | 7 ++++++- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 64d9493a8..6db3a2a8e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,7 +10,6 @@ on: - 'script/gcg/**' env: - GO_VERSION: '1.25' CGO_ENABLED: 0 jobs: @@ -55,12 +54,12 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Artifact webui diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b0db53ca3..583023498 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,6 +35,7 @@ jobs: if: ${{ matrix.language == 'go' }} with: go-version-file: 'go.mod' + check-latest: true # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/experimental.yaml b/.github/workflows/experimental.yaml index 78b5c24d9..6e4a7f384 100644 --- a/.github/workflows/experimental.yaml +++ b/.github/workflows/experimental.yaml @@ -7,7 +7,6 @@ on: - v* env: - GO_VERSION: '1.25' CGO_ENABLED: 0 jobs: @@ -27,12 +26,12 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f7e8acdf8..91a9a0aea 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,6 @@ on: - 'v*.*.*' env: - GO_VERSION: '1.25' CGO_ENABLED: 0 VERSION: ${{ github.ref_name }} TRAEFIKER_EMAIL: "traefiker@traefik.io" @@ -34,13 +33,13 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: # Ensure cache consistency on Linux, see https://github.com/actions/setup-go/pull/383 ImageOS: ${{ matrix.os }} with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Artifact webui @@ -133,4 +132,3 @@ jobs: gh release create ${VERSION} ./dist/**/traefik*.{zip,tar.gz} ./dist/traefik*.{tar.gz,txt} --repo traefik/traefik --title ${VERSION} --notes ${VERSION} --latest=false ./script/deploy.sh - diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 4bcbb61c1..f903cc409 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -10,7 +10,6 @@ on: - 'script/gcg/**' env: - GO_VERSION: '1.25' CGO_ENABLED: 0 jobs: @@ -24,10 +23,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Avoid generating webui @@ -41,7 +40,7 @@ jobs: with: path: | ~/.cache/go-build - key: ${{ runner.os }}-go-build-cache-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.mod', '**/go.sum') }} - name: Artifact traefik binary uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 @@ -66,10 +65,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Avoid generating webui @@ -89,7 +88,7 @@ jobs: with: path: | ~/.cache/go-build - key: ${{ runner.os }}-go-build-cache-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.mod', '**/go.sum') }} - name: Generate go test Slice id: test_split diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index 80a1dcdab..7b9b01d4e 100644 --- a/.github/workflows/test-unit.yaml +++ b/.github/workflows/test-unit.yaml @@ -9,9 +9,6 @@ on: - '**.md' - 'script/gcg/**' -env: - GO_VERSION: '1.25' - jobs: generate-packages: @@ -25,10 +22,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Generate matrix @@ -51,10 +48,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Avoid generating webui diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 57104ea3a..f48a8213d 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -6,7 +6,6 @@ on: - '*' env: - GO_VERSION: '1.25' GOLANGCI_LINT_VERSION: v2.10.1 MISSPELL_VERSION: v0.7.0 @@ -21,10 +20,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: golangci-lint @@ -41,10 +40,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: Install misspell ${{ env.MISSPELL_VERSION }} @@ -65,10 +64,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' check-latest: true - name: go generate diff --git a/go.mod b/go.mod index 73df81318..2b0569879 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,11 @@ module github.com/traefik/traefik/v2 -go 1.25.0 +// Only specify the major.minor version here. +// CI workflows read this value via go-version-file and use check-latest +// to automatically pick up the newest patch release. +// Pinning a patch version would defeat that mechanism and require manual +// bumps for every Go patch release. +go 1.25 require ( github.com/BurntSushi/toml v1.5.0 From 95b3f4531122c15c574fc86ad321e7e0838e69c9 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Feb 2026 16:00:05 +0100 Subject: [PATCH 6/7] Fix Go version pinning in CI workflows --- .github/workflows/build.yaml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/experimental.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/test-integration.yaml | 4 ++-- .github/workflows/test-unit.yaml | 4 ++-- .github/workflows/validate.yaml | 6 +++--- .go-version | 1 + go.mod | 7 +------ 9 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 .go-version diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6db3a2a8e..9e4039888 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,7 +59,7 @@ jobs: env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Artifact webui diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 583023498..17a139d11 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,7 +34,7 @@ jobs: uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 if: ${{ matrix.language == 'go' }} with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true # Initializes the CodeQL tools for scanning. diff --git a/.github/workflows/experimental.yaml b/.github/workflows/experimental.yaml index 6e4a7f384..2ac51f4eb 100644 --- a/.github/workflows/experimental.yaml +++ b/.github/workflows/experimental.yaml @@ -31,7 +31,7 @@ jobs: env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 91a9a0aea..302380636 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -39,7 +39,7 @@ jobs: # Ensure cache consistency on Linux, see https://github.com/actions/setup-go/pull/383 ImageOS: ${{ matrix.os }} with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Artifact webui diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index f903cc409..8d2e5c788 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -26,7 +26,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Avoid generating webui @@ -68,7 +68,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Avoid generating webui diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index 7b9b01d4e..271be510f 100644 --- a/.github/workflows/test-unit.yaml +++ b/.github/workflows/test-unit.yaml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Generate matrix @@ -51,7 +51,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Avoid generating webui diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index f48a8213d..c2b075cba 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: golangci-lint @@ -43,7 +43,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: Install misspell ${{ env.MISSPELL_VERSION }} @@ -67,7 +67,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version-file: 'go.mod' + go-version-file: '.go-version' check-latest: true - name: go generate diff --git a/.go-version b/.go-version new file mode 100644 index 000000000..f1968aa88 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.25.7 diff --git a/go.mod b/go.mod index 2b0569879..73df81318 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,6 @@ module github.com/traefik/traefik/v2 -// Only specify the major.minor version here. -// CI workflows read this value via go-version-file and use check-latest -// to automatically pick up the newest patch release. -// Pinning a patch version would defeat that mechanism and require manual -// bumps for every Go patch release. -go 1.25 +go 1.25.0 require ( github.com/BurntSushi/toml v1.5.0 From 7c55452b21e7465d95bd731725c7222f7626265b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Feb 2026 16:28:04 +0100 Subject: [PATCH 7/7] Prepare release v2.11.38 --- 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 9e09a89b7..6d72b0592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [v2.11.38](https://github.com/traefik/traefik/tree/v2.11.38) (2026-02-23) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.37...v2.11.38) + +**Bug fixes:** +- **[middleware]** Fix case sensitivity on x-forwarded headers for Connection ([#12690](https://github.com/traefik/traefik/pull/12690) by [LBF38](https://github.com/LBF38)) +- **[middleware, authentication]** Add maxResponseBodySize configuration to forwardAuth middleware ([#12694](https://github.com/traefik/traefik/pull/12694) by [gndz07](https://github.com/gndz07)) +- **[server]** Fix TLS handshake error handling ([#12692](https://github.com/traefik/traefik/pull/12692) by [juliens](https://github.com/juliens)) + ## [v2.11.37](https://github.com/traefik/traefik/tree/v2.11.37) (2026-02-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.36...v2.11.37) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index aec357bc8..09dcfba47 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.37 +# example new bugfix v2.11.38 CurrentRef = "v2.11" -PreviousRef = "v2.11.36" +PreviousRef = "v2.11.37" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.37" +FutureCurrentRefName = "v2.11.38" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000