diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 64d9493a8..9e4039888 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-version' check-latest: true - name: Artifact webui diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b0db53ca3..17a139d11 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,7 +34,8 @@ 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. - name: Initialize CodeQL diff --git a/.github/workflows/experimental.yaml b/.github/workflows/experimental.yaml index 78b5c24d9..2ac51f4eb 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-version' check-latest: true - name: Build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f29768640..0fc38bc50 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-version' 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-gateway-api-conformance.yaml b/.github/workflows/test-gateway-api-conformance.yaml index 13c361252..110d5538a 100644 --- a/.github/workflows/test-gateway-api-conformance.yaml +++ b/.github/workflows/test-gateway-api-conformance.yaml @@ -12,7 +12,6 @@ on: - 'integration/integration_test.go' env: - GO_VERSION: '1.25' CGO_ENABLED: 0 jobs: @@ -26,10 +25,11 @@ 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-version' + check-latest: true - name: Avoid generating webui run: | diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 84bda71d9..30386f28d 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-version' check-latest: true - name: Avoid generating webui @@ -42,7 +41,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 @@ -67,10 +66,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-version' check-latest: true - name: Download traefik binary @@ -87,7 +86,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-knative-conformance.yaml b/.github/workflows/test-knative-conformance.yaml index 4ae0fbd2a..ecba548c8 100644 --- a/.github/workflows/test-knative-conformance.yaml +++ b/.github/workflows/test-knative-conformance.yaml @@ -12,7 +12,6 @@ on: - 'integration/integration_test.go' env: - GO_VERSION: '1.25' CGO_ENABLED: 0 jobs: @@ -26,10 +25,11 @@ 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-version' + check-latest: true - name: Set up KO uses: ko-build/setup-ko@ace48d793556083a76f1e3e6068850c1f4a369aa # v0.6 diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index 0b3a6247a..0ca142248 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: name: List Go Packages @@ -24,10 +21,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-version' check-latest: true - name: Generate matrix @@ -50,10 +47,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-version' check-latest: true - name: Tests diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 397475a45..ea4b54c19 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -6,8 +6,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: @@ -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-version' 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-version' check-latest: true - name: Install misspell ${{ env.MISSPELL_VERSION }} @@ -62,10 +61,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-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/.golangci.yml b/.golangci.yml index d49dcdcae..6b94f9941 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -312,7 +312,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/.+|pkg/proxy/httputil/.+|pkg/observability/metrics/.+)\.go text: 'var-naming: avoid package names that conflict with Go standard library package names' linters: - revive diff --git a/CHANGELOG.md b/CHANGELOG.md index c0472ec1c..5832865ca 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)) + ## [v3.6.8](https://github.com/traefik/traefik/tree/v3.6.8) (2026-02-11) [All Commits](https://github.com/traefik/traefik/compare/v3.6.7...v3.6.8) 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/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 81bbcedd5..85017a9d7 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -785,4 +785,54 @@ http: preserveRequestMethod = 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" +``` + +```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/migrate/v3.md b/docs/content/migrate/v3.md index 9dab3b674..033b63a58 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -609,3 +609,24 @@ for more details. ### Health Check Request Path Since `v3.6.8`, the configured path for the health check request is now verified to be a relative URL, and the health check will fail if it is not. + +## v3.6.9 + +### `maxResponseBodySize` configuration on ForwardAuth middleware + +In `v3.6.9`, 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](../reference/routing-configuration/http/middlewares/forwardauth.md#maxresponsebodysize) middleware documentation for more details. + +### Kubernetes CRD Provider + +To use the new `maxResponseBodySize` option in the ForwardAuth middleware with the Kubernetes CRD provider, you need to update your CRDs. + +**Apply Updated CRDs:** + +```shell +kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +``` diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index b2881a594..eb525ee3b 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -43,6 +43,7 @@ - "traefik.http.middlewares.middleware10.forwardauth.forwardbody=true" - "traefik.http.middlewares.middleware10.forwardauth.headerfield=foobar" - "traefik.http.middlewares.middleware10.forwardauth.maxbodysize=42" +- "traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize=42" - "traefik.http.middlewares.middleware10.forwardauth.preservelocationheader=true" - "traefik.http.middlewares.middleware10.forwardauth.preserverequestmethod=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index e0e2139e5..4b51878b0 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -207,6 +207,7 @@ headerField = "foobar" forwardBody = true maxBodySize = 42 + maxResponseBodySize = 42 preserveLocationHeader = true preserveRequestMethod = true [http.middlewares.Middleware10.forwardAuth.tls] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index e2ab16e54..a5b70b3ec 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -231,6 +231,7 @@ http: headerField: foobar forwardBody: true maxBodySize: 42 + maxResponseBodySize: 42 preserveLocationHeader: true preserveRequestMethod: true Middleware11: 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 6634b09b2..eabd347f4 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1403,6 +1403,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer preserveLocationHeader: description: PreserveLocationHeader defines whether to forward the Location header to the client as is or prefix it with the diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index a9d2984b4..8d9c9f26f 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -56,6 +56,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware10/forwardAuth/forwardBody` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/headerField` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/maxBodySize` | `42` | +| `traefik/http/middlewares/Middleware10/forwardAuth/maxResponseBodySize` | `42` | | `traefik/http/middlewares/Middleware10/forwardAuth/preserveLocationHeader` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/preserveRequestMethod` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index e5ececaec..62b37479a 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -570,6 +570,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer preserveLocationHeader: description: PreserveLocationHeader defines whether to forward the Location header to the client as is or prefix it with the diff --git a/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md b/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md index 30bc78ec7..dd2f1aad5 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md +++ b/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md @@ -63,6 +63,7 @@ spec: | `addAuthCookiesToResponse` | List of cookies to copy from the authentication server to the response, replacing any existing conflicting cookie from the forwarded response.
Please note that all backend cookies matching the configured list will not be added to the response. | [] | No | | `forwardBody` | Sets the `forwardBody` option to `true` to send the Body. As body is read inside Traefik before forwarding, this breaks streaming. | false | No | | `maxBodySize` | Set the `maxBodySize` to limit the body size in bytes. If body is bigger than this, it returns a 401 (unauthorized). If left unset, the request body size is unrestricted which can have performance or security implications. < br/>More information [here](#maxbodysize).| -1 | No | +| `maxResponseBodySize` | Set the `maxResponseBodySize` to limit the response body size from the authentication server in bytes. If the response body exceeds this limit, it returns a 401 (unauthorized). If left unset, the response body size is unrestricted which can have performance or security implications.
More information [here](#maxresponsebodysize).| -1 | No | | `headerField` | Defines a header field to store the authenticated user. | "" | No | | `preserveLocationHeader` | Defines whether to forward the Location header to the client as is or prefix it with the domain name of the authentication server. | false | No | | `preserveRequestMethod` | Defines whether to preserve the original request method while forwarding the request to the authentication server. | false | No | @@ -115,6 +116,17 @@ maxBodySize: 104857600 # 100MB in bytes - **File Uploads**: Set based on your maximum expected file size - **High-Traffic Services**: Use smaller limits to prevent resource exhaustion +### maxResponseBodySize + +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. + +!!! 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. + ## Forward-Request Headers The following request properties are provided to the forward-auth target endpoint as `X-Forwarded-` headers. diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 9e00b358f..8fffdd592 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -205,6 +205,7 @@ authResponseHeaders = ["foobar", "foobar"] authResponseHeadersRegex = "foobar" authRequestHeaders = ["foobar", "foobar"] + maxResponseBodySize = 42 addAuthCookiesToResponse = ["foobar", "foobar"] headerField = "foobar" forwardBody = true diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index fdb8f2c1e..0f9c5f531 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -231,6 +231,7 @@ http: authRequestHeaders: - foobar - foobar + maxResponseBodySize: 42 addAuthCookiesToResponse: - foobar - foobar diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 5a426f7a5..bd3e9c58e 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -129,6 +129,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 @@ -165,6 +168,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 @@ -191,6 +195,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 @@ -611,6 +616,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 @@ -619,6 +625,7 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward address: ":80" forwardedHeaders: connection: + - X-Foo - foobar ``` @@ -629,13 +636,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/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index dfd63fe83..51685381a 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1404,6 +1404,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer preserveLocationHeader: description: PreserveLocationHeader defines whether to forward the Location header to the client as is or prefix it with the 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 16c743829..816e06a59 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -2173,3 +2173,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/internal/gendoc.go b/internal/gendoc.go index df8098b0c..276b96b1a 100644 --- a/internal/gendoc.go +++ b/internal/gendoc.go @@ -187,7 +187,7 @@ THIS FILE MUST NOT BE EDITED BY HAND } if w.err != nil { - logger.Fatal().Err(err).Send() + logger.Fatal().Err(w.err).Send() } } diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 5b2157d9b..8032f6155 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -258,6 +258,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"` // AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response. AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty" toml:"addAuthCookiesToResponse,omitempty" yaml:"addAuthCookiesToResponse,omitempty" export:"true"` // HeaderField defines a header field to store the authenticated user. diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 6074f0998..7e774bc92 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -373,6 +373,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 + } if in.AddAuthCookiesToResponse != nil { in, out := &in.AddAuthCookiesToResponse, &out.AddAuthCookiesToResponse *out = make([]string, len(*in)) diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index b17222410..390b2ebb6 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -54,6 +54,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware7.forwardauth.forwardbody": "true", "traefik.http.middlewares.Middleware7.forwardauth.maxbodysize": "42", "traefik.http.middlewares.Middleware7.forwardauth.preserveRequestMethod": "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", @@ -587,6 +588,7 @@ func TestDecodeConfiguration(t *testing.T) { ForwardBody: true, MaxBodySize: pointer(int64(42)), PreserveRequestMethod: true, + MaxResponseBodySize: pointer[int64](42), }, }, "Middleware8": { @@ -1141,6 +1143,7 @@ func TestEncodeConfiguration(t *testing.T) { ForwardBody: true, MaxBodySize: pointer(int64(42)), PreserveRequestMethod: true, + MaxResponseBodySize: pointer[int64](42), }, }, "Middleware8": { @@ -1361,6 +1364,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.PreserveLocationHeader": "false", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.PreserveRequestMethod": "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 622362725..bbd2a3196 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -55,6 +55,7 @@ type forwardAuth struct { client http.Client trustForwardHeader bool authRequestHeaders []string + maxResponseBodySize int64 addAuthCookiesToResponse map[string]struct{} headerField string forwardBody bool @@ -94,6 +95,13 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu logger.Warn().Msgf("ForwardAuth 'maxBodySize' is not configured with 'forwardBody: true', allowing unlimited request body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.") } + if config.MaxResponseBodySize != nil { + fa.maxResponseBodySize = *config.MaxResponseBodySize + } else { + fa.maxResponseBodySize = -1 + logger.Warn().Msg("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 { @@ -210,8 +218,15 @@ 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 { + if errors.Is(readError, errResponseBodyTooLarge) { + logger.Debug().Msgf("Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize) + + observability.SetStatusErrorf(req.Context(), "Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize) + rw.WriteHeader(http.StatusUnauthorized) + return + } logger.Debug().Err(readError).Msgf("Error reading body %s", fa.address) observability.SetStatusErrorf(req.Context(), "Error reading body %s. Cause: %s", fa.address, readError) @@ -354,6 +369,27 @@ func (fa *forwardAuth) readBodyBytes(req *http.Request) ([]byte, error) { return nil, errBodyTooLarge } +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 f530fb8ad..da3d03dd1 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -936,6 +936,89 @@ func TestForwardAuthPreserveRequestMethod(t *testing.T) { } } +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 mockTracer struct { embedded.Tracer diff --git a/pkg/middlewares/forwardedheaders/forwarded_header.go b/pkg/middlewares/forwardedheaders/forwarded_header.go index efc15e469..9ba697d57 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", }, }, } diff --git a/pkg/observability/tracing/tracing_test.go b/pkg/observability/tracing/tracing_test.go index 698a1f435..91dc19e60 100644 --- a/pkg/observability/tracing/tracing_test.go +++ b/pkg/observability/tracing/tracing_test.go @@ -492,8 +492,6 @@ func resourceAttributes(traces ptrace.Traces) map[string]string { } // mainSpan gets the main span from traces (assumes single span for testing). -// -//nolint:unqueryvet // False positive: This is OTel trace iteration, not SQLBoiler. func mainSpan(traces ptrace.Traces) ptrace.Span { for _, resourceSpans := range traces.ResourceSpans().All() { for _, scopeSpans := range resourceSpans.ScopeSpans().All() { 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 { diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go index ab9d603b3..6e94d4988 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go @@ -35,6 +35,7 @@ type ForwardAuthApplyConfiguration struct { AuthResponseHeadersRegex *string `json:"authResponseHeadersRegex,omitempty"` AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"` TLS *ClientTLSWithCAOptionalApplyConfiguration `json:"tls,omitempty"` + MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty"` AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"` HeaderField *string `json:"headerField,omitempty"` ForwardBody *bool `json:"forwardBody,omitempty"` @@ -101,6 +102,14 @@ func (b *ForwardAuthApplyConfiguration) WithTLS(value *ClientTLSWithCAOptionalAp return b } +// WithMaxResponseBodySize sets the MaxResponseBodySize field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MaxResponseBodySize field is set to the value of the last call. +func (b *ForwardAuthApplyConfiguration) WithMaxResponseBodySize(value int64) *ForwardAuthApplyConfiguration { + b.MaxResponseBodySize = &value + return b +} + // WithAddAuthCookiesToResponse adds the given value to the AddAuthCookiesToResponse field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the AddAuthCookiesToResponse field. diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index d2fc71306..f83a8d1a0 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -967,6 +967,10 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef } forwardAuth.SetDefaults() + if auth.MaxResponseBodySize != nil { + forwardAuth.MaxResponseBodySize = auth.MaxResponseBodySize + } + if auth.MaxBodySize != nil { forwardAuth.MaxBodySize = auth.MaxBodySize } diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index d00e839d1..891f9c1a8 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go @@ -171,6 +171,8 @@ type ForwardAuth struct { AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"` // TLS defines the configuration used to secure the connection to the authentication server. TLS *ClientTLSWithCAOptional `json:"tls,omitempty"` + // MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server. + MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty"` // AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response. AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"` // HeaderField defines a header field to store the authenticated user. 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 3137d46ec..38cbbe963 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -285,6 +285,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { *out = new(ClientTLSWithCAOptional) (*in).DeepCopyInto(*out) } + if in.MaxResponseBodySize != nil { + in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize + *out = new(int64) + **out = **in + } if in.AddAuthCookiesToResponse != nil { in, out := &in.AddAuthCookiesToResponse, &out.AddAuthCookiesToResponse *out = make([]string, len(*in)) diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index c8dc84bbc..0a4ed49dd 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -93,6 +93,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware08/forwardAuth/maxBodySize": "42", "traefik/http/middlewares/Middleware08/forwardAuth/preserveLocationHeader": "true", "traefik/http/middlewares/Middleware08/forwardAuth/preserveRequestMethod": "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", @@ -445,6 +446,7 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, + MaxResponseBodySize: pointer[int64](42), ForwardBody: true, MaxBodySize: pointer(int64(42)), PreserveLocationHeader: true, diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index 9edf2363f..a461f474c 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -279,6 +279,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 4b71f1c76..fc981af88 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -246,7 +246,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 c9674639b..c074284cf 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -249,7 +249,8 @@ "authResponseHeadersRegex": "foo", "authRequestHeaders": [ "foo" - ] + ], + "maxResponseBodySize": 42 }, "inFlightReq": { "amount": 42, diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 872aa19d1..81400e3f1 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" @@ -140,6 +141,11 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { hello, err := clientHelloInfo(br) if err != nil { + var opErr *net.OpError + if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || !opErr.Timeout()) { + log.Debug().Err(err).Msg("Error while reading client hello") + } + conn.Close() return } @@ -378,11 +384,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.Debug().Err(err).Msg("Error while peeking first byte") - } - 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 @@ -406,20 +408,13 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { const recordHeaderLen = 5 hdr, err = br.Peek(recordHeaderLen) if err != nil { - log.Error().Err(err).Msg("Error while peeking client hello header") - 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.Debug().Msgf("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 { @@ -428,11 +423,7 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { helloBytes, err := br.Peek(recordHeaderLen + recLen) if err != nil { - log.Error().Err(err).Msg("Error while peeking client hello bytes") - 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 6c22c6a4c..0634c0f60 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -1143,9 +1143,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") }