diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f616b6a41..58d6bc9285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [v2.11.44](https://github.com/traefik/traefik/tree/v2.11.44) (2026-04-29) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.43...v2.11.44) + +**Bug fixes:** +- **[middleware]** Add errorRequestHeaders option to Errors middleware ([#13034](https://github.com/traefik/traefik/pull/13034) @gndz07) +- **[acme]** Bump github.com/go-acme/lego to v4.35.2 ([#13052](https://github.com/traefik/traefik/pull/13052) @mmatur) + +**Misc:** +- Make FLAGS Make variable usable ([#13009](https://github.com/traefik/traefik/pull/13009) @twz123) + ## [v3.6.14](https://github.com/traefik/traefik/tree/v3.6.14) (2026-04-22) [All Commits](https://github.com/traefik/traefik/compare/v3.6.13...v3.6.14) diff --git a/Makefile b/Makefile index 7f361cfb25..283fce3300 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ generate: #? binary: Build the binary binary: generate-webui dist @echo SHA: $(VERSION) $(CODENAME) $(DATE) - CGO_ENABLED=0 GOGC=${GOGC} GOOS=${GOOS} GOARCH=${GOARCH} go build ${FLAGS[*]} -ldflags "-s -w \ + CGO_ENABLED=0 GOGC=${GOGC} GOOS=${GOOS} GOARCH=${GOARCH} go build ${FLAGS} -ldflags "-s -w \ -X github.com/traefik/traefik/v3/pkg/version.Version=$(VERSION) \ -X github.com/traefik/traefik/v3/pkg/version.Codename=$(CODENAME) \ -X github.com/traefik/traefik/v3/pkg/version.BuildDate=$(DATE)" \ diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index bf1ee1c8c8..ddc616a5d5 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,6 +9,15 @@ This guide provides detailed migration steps for upgrading between different Tra --- +## v3.6.15 + +In `v3.6.15`, a new `errorRequestHeaders` option has been added to the Errors middleware. + +By default, the behavior is unchanged: all original request headers are forwarded to the error page service. +If the error page service is in a separate trust domain, consider using `errorRequestHeaders` to restrict which headers are forwarded. + +Please check out the [Error Pages](../reference/routing-configuration/http/middlewares/errorpages.md#errorRequestHeaders) middleware documentation for more details. + ## v3.6.14 ### Kubernetes CRD: Chain middleware and `allowCrossNamespace` diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index eb525ee3b4..d57ef4bc58 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -30,6 +30,7 @@ - "traefik.http.middlewares.middleware08.digestauth.removeheader=true" - "traefik.http.middlewares.middleware08.digestauth.users=foobar, foobar" - "traefik.http.middlewares.middleware08.digestauth.usersfile=foobar" +- "traefik.http.middlewares.middleware09.errors.errorrequestheaders=foobar, foobar" - "traefik.http.middlewares.middleware09.errors.query=foobar" - "traefik.http.middlewares.middleware09.errors.service=foobar" - "traefik.http.middlewares.middleware09.errors.status=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 4b51878b07..dad9c61eb1 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -193,6 +193,7 @@ status = ["foobar", "foobar"] service = "foobar" query = "foobar" + errorRequestHeaders = ["foobar", "foobar"] [http.middlewares.Middleware09.errors.statusRewrites] name0 = 42 name1 = 42 diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index a5b70b3ec2..8810e1da17 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -208,6 +208,9 @@ http: name1: 42 service: foobar query: foobar + errorRequestHeaders: + - foobar + - foobar Middleware10: forwardAuth: address: foobar diff --git a/docs/content/reference/routing-configuration/http/middlewares/errorpages.md b/docs/content/reference/routing-configuration/http/middlewares/errorpages.md index 9f95b0acd4..94a3b83887 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/errorpages.md +++ b/docs/content/reference/routing-configuration/http/middlewares/errorpages.md @@ -129,3 +129,12 @@ The table below lists all the available variables and their associated values. | `{status}` | The response status code. | | `{originalStatus}` | The original response status code, if it has been modified by the `statusRewrites` option. | | `{url}` | The [escaped](https://pkg.go.dev/net/url#QueryEscape) request URL.| + +### `errorRequestHeaders` + +Defines the list of original request headers forwarded to the error page service. + +By default (`errorRequestHeaders` not set), all request headers — including authentication material such as `Authorization` and `Cookie` — are forwarded. +If the error page service is in a separate trust domain, use this option to restrict which headers cross the service boundary. + +Set to an explicit list to forward only those headers, or set to an empty list (`errorRequestHeaders: []`) to forward no headers. diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 8fffdd5929..e02b09a309 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -195,6 +195,7 @@ status = ["foobar", "foobar"] service = "foobar" query = "foobar" + errorRequestHeaders = ["foobar", "foobar"] [http.middlewares.Middleware09.errors.statusRewrites] name0 = 42 name1 = 42 diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index 0f9c5f5318..ae91fd329a 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -214,6 +214,9 @@ http: name1: 42 service: foobar query: foobar + errorRequestHeaders: + - foobar + - foobar Middleware10: forwardAuth: address: foobar diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 037dbe393e..befe299542 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -236,6 +236,10 @@ type ErrorPage struct { // The {originalStatus} variable can be used in order to insert the upstream status code in the URL. // The {url} variable can be used in order to insert the escaped request URL. Query string `json:"query,omitempty" toml:"query,omitempty" yaml:"query,omitempty" export:"true"` + // ErrorRequestHeaders defines the list of request headers forwarded to the error page service. + // When nil (not set), all original request headers are forwarded. + // Set to an empty list to forward no headers, or list specific headers to forward only those. + ErrorRequestHeaders []string `json:"errorRequestHeaders,omitempty" toml:"errorRequestHeaders,omitempty" yaml:"errorRequestHeaders,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 a14c1e97cb..f25e40158e 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -321,6 +321,11 @@ func (in *ErrorPage) DeepCopyInto(out *ErrorPage) { (*out)[key] = val } } + if in.ErrorRequestHeaders != nil { + in, out := &in.ErrorRequestHeaders, &out.ErrorRequestHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index 193ad43ec6..3617ca8155 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -37,6 +37,7 @@ type customErrors struct { backendHandler http.Handler httpCodeRanges types.HTTPCodeRanges backendQuery string + requestHeaders []string statusRewrites []statusRewrite } @@ -79,6 +80,7 @@ func New(ctx context.Context, next http.Handler, config dynamic.ErrorPage, servi backendHandler: backend, httpCodeRanges: httpCodeRanges, backendQuery: config.Query, + requestHeaders: config.ErrorRequestHeaders, statusRewrites: statusRewrites, }, nil } @@ -145,7 +147,15 @@ func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - utils.CopyHeaders(pageReq.Header, req.Header) + if c.requestHeaders != nil { + for _, header := range c.requestHeaders { + if values := req.Header.Values(header); len(values) > 0 { + pageReq.Header[http.CanonicalHeaderKey(header)] = values + } + } + } else { + utils.CopyHeaders(pageReq.Header, req.Header) + } c.backendHandler.ServeHTTP(newCodeModifier(rw, code), pageReq.WithContext(req.Context())) } diff --git a/pkg/middlewares/customerrors/custom_errors_test.go b/pkg/middlewares/customerrors/custom_errors_test.go index ec8eff1536..bf18840199 100644 --- a/pkg/middlewares/customerrors/custom_errors_test.go +++ b/pkg/middlewares/customerrors/custom_errors_test.go @@ -23,6 +23,7 @@ func TestHandler(t *testing.T) { backendCode int backendErrorHandler http.HandlerFunc validate func(t *testing.T, recorder *httptest.ResponseRecorder) + requestHeaders map[string]string }{ { desc: "no error", @@ -154,6 +155,60 @@ func TestHandler(t *testing.T) { assert.Contains(t, recorder.Body.String(), "My 503 page.") }, }, + { + desc: "forward all headers by default", + errorPage: &dynamic.ErrorPage{Service: "error", Query: "/test", Status: []string{"503"}}, + requestHeaders: map[string]string{ + "X-Request-Id": "trace-abc", + "Authorization": "Bearer secret", + }, + backendCode: http.StatusServiceUnavailable, + backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, r.Header.Get("X-Request-Id")) + fmt.Fprintln(w, r.Header.Get("Authorization")) + }), + validate: func(t *testing.T, recorder *httptest.ResponseRecorder) { + t.Helper() + assert.Contains(t, recorder.Body.String(), "trace-abc") + assert.Contains(t, recorder.Body.String(), "Bearer secret") + }, + }, + { + desc: "forward only allowlisted headers", + errorPage: &dynamic.ErrorPage{Service: "error", Query: "/test", Status: []string{"503"}, ErrorRequestHeaders: []string{"X-Request-Id"}}, + requestHeaders: map[string]string{ + "X-Request-Id": "trace-abc", + "Authorization": "Bearer secret", + }, + backendCode: http.StatusServiceUnavailable, + backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, r.Header.Get("X-Request-Id")) + fmt.Fprintln(w, r.Header.Get("Authorization")) + }), + validate: func(t *testing.T, recorder *httptest.ResponseRecorder) { + t.Helper() + assert.Contains(t, recorder.Body.String(), "trace-abc") + assert.NotContains(t, recorder.Body.String(), "Bearer secret") + }, + }, + { + desc: "forward no headers", + errorPage: &dynamic.ErrorPage{Service: "error", Query: "/test", Status: []string{"503"}, ErrorRequestHeaders: []string{}}, + requestHeaders: map[string]string{ + "X-Request-Id": "trace-abc", + "Authorization": "Bearer secret", + }, + backendCode: http.StatusServiceUnavailable, + backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, r.Header.Get("X-Request-Id")) + fmt.Fprintln(w, r.Header.Get("Authorization")) + }), + validate: func(t *testing.T, recorder *httptest.ResponseRecorder) { + t.Helper() + assert.NotContains(t, recorder.Body.String(), "trace-abc") + assert.NotContains(t, recorder.Body.String(), "Bearer secret") + }, + }, } for _, test := range testCases { @@ -174,6 +229,9 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost/test?foo=bar&baz=buz", nil) + for k, v := range test.requestHeaders { + req.Header.Set(k, v) + } // Client like browser and curl will issue a relative HTTP request, which not have a host and scheme in the URL. But the http.NewRequest will set them automatically. req.URL.Host = ""