From f1e850ae0bd6977e8ee0f62a088f075cbea506cf Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 2 Feb 2026 11:24:20 +0100 Subject: [PATCH 01/12] Prepare release v2.11.36 --- CHANGELOG.md | 10 ++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1736b318..5ec9d15b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [v2.11.36](https://github.com/traefik/traefik/tree/v2.11.36) (2026-02-02) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.35...v2.11.36) + +**Bug fixes:** +- **[service]** Avoid recursion with services ([#12591](https://github.com/traefik/traefik/pull/12591) by [juliens](https://github.com/juliens)) +- **[webui]** Bump dependencies of documentation and webui ([#12581](https://github.com/traefik/traefik/pull/12581) by [gndz07](https://github.com/gndz07)) + +**Documentation:** +- Remove extra dots in migration guide ([#12573](https://github.com/traefik/traefik/pull/12573) by [rtribotte](https://github.com/rtribotte)) + ## [v2.11.35](https://github.com/traefik/traefik/tree/v2.11.35) (2026-01-14) [All Commits](https://github.com/traefik/traefik/compare/v2.11.34...v2.11.35) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 640bbc3ea7..c25306bfc2 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.35 +# example new bugfix v2.11.36 CurrentRef = "v2.11" -PreviousRef = "v2.11.34" +PreviousRef = "v2.11.35" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.35" +FutureCurrentRefName = "v2.11.36" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 4b3c971ea3947772cb8c57f7550aa55bc9bdb2a1 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 10 Feb 2026 14:48:06 +0100 Subject: [PATCH 02/12] Use url.Parse to validate X-Forwarded-Prefix value --- pkg/api/dashboard/dashboard.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/api/dashboard/dashboard.go b/pkg/api/dashboard/dashboard.go index dd8c335b5d..8c344bbd5d 100644 --- a/pkg/api/dashboard/dashboard.go +++ b/pkg/api/dashboard/dashboard.go @@ -4,6 +4,7 @@ import ( "fmt" "io/fs" "net/http" + "net/url" "strings" "text/template" @@ -80,7 +81,9 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error { Path(basePath). HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { xfPrefix := req.Header.Get("X-Forwarded-Prefix") - if strings.Contains(xfPrefix, "//") { + + // Validates that the X-Forwarded-Prefix value contains a relative URL. + if u, err := url.Parse(xfPrefix); err != nil || u.Host != "" || u.Scheme != "" { log.Error().Msgf("X-Forwarded-Prefix contains an invalid value: %s, defaulting to empty prefix", xfPrefix) xfPrefix = "" } From 0beed101ec9e23d8e84e4282ce6b475041582797 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 10 Feb 2026 14:52:05 +0100 Subject: [PATCH 03/12] Validate healthcheck path configuration Co-authored-by: Michael --- docs/content/routing/services/index.md | 2 +- pkg/server/service/service.go | 5 + pkg/server/service/service_test.go | 129 +++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 3480b323b5..0f015626db 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -322,7 +322,7 @@ To propagate status changes (e.g. all servers of this service are down) upwards, Below are the available options for the health check mechanism: -- `path` (required), defines the server URL path for the health check endpoint . +- `path` (required), defines the server URL path for the health check endpoint (must be a relative URL). - `scheme` (optional), replaces the server URL `scheme` for the health check endpoint. - `hostname` (optional), sets the value of `hostname` in the `Host` header of the health check request. - `port` (optional), replaces the server URL `port` for the health check endpoint. diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 033736e2eb..442115fa94 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -356,6 +356,11 @@ func buildHealthCheckOptions(ctx context.Context, lb healthcheck.Balancer, backe return nil } + if u, err := url.Parse(hc.Path); err != nil || u.Scheme != "" || u.Host != "" { + logger.Errorf("Ignoring heath check configuration for '%s': path must not be an absolute URL, got %s", backend, hc.Path) + return nil + } + interval := defaultHealthCheckInterval if hc.Interval != "" { intervalOverride, err := time.ParseDuration(hc.Interval) diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index 2067a800f8..d734f1649e 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -9,11 +9,13 @@ import ( "net/textproto" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" + "github.com/traefik/traefik/v2/pkg/healthcheck" "github.com/traefik/traefik/v2/pkg/server/provider" "github.com/traefik/traefik/v2/pkg/testhelpers" ) @@ -601,3 +603,130 @@ func TestMultipleTypeOnBuildHTTP(t *testing.T) { _, err := manager.BuildHTTP(t.Context(), "test@file") assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead") } + +func TestBuildHealthCheckOptions(t *testing.T) { + testCases := []struct { + desc string + hc *dynamic.ServerHealthCheck + expected *healthcheck.Options + }{ + { + desc: "nil health check config", + hc: nil, + expected: nil, + }, + { + desc: "empty path returns nil", + hc: &dynamic.ServerHealthCheck{ + Path: "", + }, + expected: nil, + }, + { + desc: "absolute URL path returns nil", + hc: &dynamic.ServerHealthCheck{ + Path: "http://example.com/health", + }, + expected: nil, + }, + { + desc: "path with scheme returns nil", + hc: &dynamic.ServerHealthCheck{ + Path: "https://health", + }, + expected: nil, + }, + { + desc: "path with host returns nil", + hc: &dynamic.ServerHealthCheck{ + Path: "//example.com/health", + }, + expected: nil, + }, + { + desc: "full valid config", + hc: &dynamic.ServerHealthCheck{ + Scheme: "https", + Path: "/health", + Method: "GET", + Port: 8080, + Interval: "10s", + Timeout: "2s", + Hostname: "health.example.com", + Headers: map[string]string{"Authorization": "Bearer token"}, + }, + expected: &healthcheck.Options{ + Scheme: "https", + Path: "/health", + Method: "GET", + Port: 8080, + Interval: 10 * time.Second, + Timeout: 2 * time.Second, + Hostname: "health.example.com", + Headers: map[string]string{"Authorization": "Bearer token"}, + FollowRedirects: true, + }, + }, + { + desc: "invalid interval uses default", + hc: &dynamic.ServerHealthCheck{ + Path: "/health", + Interval: "invalid", + }, + expected: &healthcheck.Options{ + Path: "/health", + Interval: defaultHealthCheckInterval, + Timeout: defaultHealthCheckTimeout, + FollowRedirects: true, + }, + }, + { + desc: "negative interval uses default", + hc: &dynamic.ServerHealthCheck{ + Path: "/health", + Interval: "-10s", + }, + expected: &healthcheck.Options{ + Path: "/health", + Interval: defaultHealthCheckInterval, + Timeout: defaultHealthCheckTimeout, + FollowRedirects: true, + }, + }, + { + desc: "invalid timeout uses default", + hc: &dynamic.ServerHealthCheck{ + Path: "/health", + Timeout: "invalid", + }, + expected: &healthcheck.Options{ + Path: "/health", + Interval: defaultHealthCheckInterval, + Timeout: defaultHealthCheckTimeout, + FollowRedirects: true, + }, + }, + { + desc: "negative timeout uses default", + hc: &dynamic.ServerHealthCheck{ + Path: "/health", + Timeout: "-5s", + }, + expected: &healthcheck.Options{ + Path: "/health", + Interval: defaultHealthCheckInterval, + Timeout: defaultHealthCheckTimeout, + FollowRedirects: true, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := buildHealthCheckOptions(t.Context(), nil, "test-backend", test.hc) + assert.Equal(t, test.expected, result) + }) + } +} From 72e2454e421bd3e145f91c9d00b4418c9e8bbdb1 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 11 Feb 2026 09:22:04 +0100 Subject: [PATCH 04/12] Cap TLS record length to RFC 8446 limit in ClientHello peeking --- pkg/server/router/tcp/router.go | 18 ++++- pkg/server/router/tcp/router_test.go | 114 +++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index ad116cd0a5..2d9c003fa9 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -18,7 +18,15 @@ import ( "github.com/traefik/traefik/v2/pkg/tcp" ) -const defaultBufSize = 4096 +const ( + defaultBufSize = 4096 + // Per RFC 8446 Section 5.1, the maximum TLS record payload length is 2^14 (16384) bytes. + // A ClientHello is always a plaintext record, so any value exceeding this limit is invalid + // and likely indicates an attack attempting to force oversized per-connection buffer allocations. + // However, in practice the go server handshake can read up to 16384 + 2048 bytes, + // so we need to allow for some extra bytes to avoid rejecting valid handshakes. + maxTLSRecordLen = 16384 + 2048 +) // Router is a TCP router. type Router struct { @@ -395,6 +403,14 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { 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 + } + if recordHeaderLen+recLen > defaultBufSize { br = bufio.NewReaderSize(br, recordHeaderLen+recLen) } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 9783bd591b..918c21da35 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -1,6 +1,7 @@ package tcp import ( + "bufio" "bytes" "crypto/tls" "errors" @@ -630,6 +631,7 @@ func Test_Routing(t *testing.T) { _ = serverHTTPS.Serve(httpsForwarder) }() + // The HTTPS forwarder will be added as tcp.TLSHandler (to handle TLS). router.SetHTTPSForwarder(httpsForwarder) stoppedTCP := make(chan struct{}) @@ -1063,3 +1065,115 @@ func checkHTTPSTLS10(addr string, timeout time.Duration) error { func checkHTTPSTLS12(addr string, timeout time.Duration) error { return checkHTTPS(addr, timeout, tls.VersionTLS12) } + +// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo +// does not block or allocate excessive memory when a client sends a TLS +// record header with a maliciously large record length (up to 0xFFFF). +// +// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks +// on Peek(65540), waiting for bytes that never arrive (until readTimeout). +// With the fix, records exceeding the TLS maximum plaintext size (16384) +// are rejected immediately. +func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) { + testCases := []struct { + desc string + recLen uint16 + }{ + { + desc: "max uint16 record length (0xFFFF)", + recLen: 0xFFFF, + }, + { + desc: "just above TLS maximum (18433)", + recLen: 18433, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + type result struct { + hello *clientHello + err error + } + resultCh := make(chan result, 1) + + go func() { + br := bufio.NewReader(serverConn) + hello, err := clientHelloInfo(br) + resultCh <- result{hello, err} + }() + + // Send a TLS record header with an oversized record length. + // Only the 5-byte header is sent; the client then stalls. + hdr := []byte{ + 0x16, // Content Type: Handshake + 0x03, 0x03, // Version: TLS 1.2 + byte(test.recLen >> 8), // Length high byte + byte(test.recLen & 0xFF), // Length low byte + } + _, err := clientConn.Write(hdr) + require.NoError(t, err) + + // Without the fix, clientHelloInfo blocks on Peek(recLen+5) + // since only 5 bytes are available. The test would time out. + // 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) + case <-time.After(5 * time.Second): + t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped") + } + }) + } +} + +// Test_clientHelloInfo_validRecordLength verifies that clientHelloInfo +// still works correctly with legitimate TLS record sizes. +func Test_clientHelloInfo_validRecordLength(t *testing.T) { + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + type result struct { + hello *clientHello + err error + } + resultCh := make(chan result, 1) + + go func() { + br := bufio.NewReader(serverConn) + hello, err := clientHelloInfo(br) + resultCh <- result{hello, err} + }() + + // Build a TLS record header with a small (valid) record length. + recLen := 100 + hdr := []byte{ + 0x16, // Content Type: Handshake + 0x03, 0x03, // Version: TLS 1.2 + byte(recLen >> 8), // Length high byte + byte(recLen & 0xFF), // Length low byte + } + payload := make([]byte, recLen) + + _, err := clientConn.Write(append(hdr, payload...)) + require.NoError(t, err) + clientConn.Close() + + select { + case r := <-resultCh: + require.NoError(t, r.err) + require.NotNil(t, r.hello) + assert.True(t, r.hello.isTLS) + case <-time.After(5 * time.Second): + t.Fatal("clientHelloInfo blocked on valid TLS record") + } +} From 31e566e9f1d7888ccb6fbc18bfed427203c35678 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 11 Feb 2026 09:48:05 +0100 Subject: [PATCH 05/12] Remove conn deadline after STARTTLS negociation Co-authored-by: Michael --- pkg/server/router/tcp/postgres.go | 20 ++++++++++++++------ pkg/server/router/tcp/router.go | 9 +++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go index 5d773704fb..dedb7d6f4e 100644 --- a/pkg/server/router/tcp/postgres.go +++ b/pkg/server/router/tcp/postgres.go @@ -7,6 +7,7 @@ import ( "io" "net" "sync" + "time" "github.com/rs/zerolog/log" tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp" @@ -46,7 +47,7 @@ func isPostgres(br *bufio.Reader) (bool, error) { func (r *Router) servePostgres(conn tcp.WriteCloser) { _, err := conn.Write(PostgresStartTLSReply) if err != nil { - conn.Close() + _ = conn.Close() return } @@ -55,32 +56,39 @@ func (r *Router) servePostgres(conn tcp.WriteCloser) { b := make([]byte, len(PostgresStartTLSMsg)) _, err = br.Read(b) if err != nil { - conn.Close() + _ = conn.Close() return } hello, err := clientHelloInfo(br) if err != nil { - conn.Close() + _ = conn.Close() return } if !hello.isTLS { - conn.Close() + _ = conn.Close() return } + // The deadline was there to prevent hanging connections while waiting for the client, + // now that the STARTTLS message and Client Hello have been read, + // we can remove it and leave its handling to the TCP reverse proxy eventually. + if err := conn.SetDeadline(time.Time{}); err != nil { + log.Error().Err(err).Msg("Error while setting deadline") + } + connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) if err != nil { log.Error().Err(err).Msg("Error while reading TCP connection data") - conn.Close() + _ = conn.Close() return } // Contains also TCP TLS passthrough routes. handlerTCPTLS, _ := r.muxerTCPTLS.Match(connData) if handlerTCPTLS == nil { - conn.Close() + _ = conn.Close() return } diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index b2b4ec93f8..4a790f861a 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -126,11 +126,6 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } if postgres { - // Remove read/write deadline and delegate this to underlying TCP server. - if err := conn.SetDeadline(time.Time{}); err != nil { - log.Error().Err(err).Msg("Error while setting deadline") - } - r.servePostgres(r.GetConn(conn, getPeeked(br))) return } @@ -141,7 +136,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } - // Remove read/write deadline and delegate this to underlying TCP server (for now only handled by HTTP Server) + // The deadline was set to avoid blocking on the initial read of the ClientHello, + // but now that we have it, we can remove it, + // and delegate this to underlying TCP server (for now only handled by HTTP Server). if err := conn.SetDeadline(time.Time{}); err != nil { log.Error().Err(err).Msg("Error while setting deadline") } From 7747b40310c2652b975fc8a64a81954beb22baea Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 11 Feb 2026 10:42:04 +0100 Subject: [PATCH 06/12] Prepare release v2.11.37 --- CHANGELOG.md | 7 +++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec9d15b2b..9e09a89b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) + +**Bug fixes:** +- **[healthcheck]** Validate healthcheck path configuration (#12642 by @rtribotte) +- **[tls, server]** Cap TLS record length to RFC 8446 limit in ClientHello peeking (#12638 by @mmatur) + ## [v2.11.36](https://github.com/traefik/traefik/tree/v2.11.36) (2026-02-02) [All Commits](https://github.com/traefik/traefik/compare/v2.11.35...v2.11.36) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index c25306bfc2..aec357bc80 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.36 +# example new bugfix v2.11.37 CurrentRef = "v2.11" -PreviousRef = "v2.11.35" +PreviousRef = "v2.11.36" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.36" +FutureCurrentRefName = "v2.11.37" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From f6ce751a0628a83b2ed34ee6733621b1fd2b735c Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 11 Feb 2026 16:10:05 +0100 Subject: [PATCH 07/12] Reject absolute URL in healthcheck path configuration --- docs/content/migrate/v3.md | 6 ++ .../http/load-balancing/service.md | 6 +- .../kubernetes/crd/http/service.md | 60 +++++++++---------- pkg/healthcheck/healthcheck.go | 9 +++ pkg/healthcheck/healthcheck_test.go | 8 +++ 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index c222024a99..9dab3b6740 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -603,3 +603,9 @@ 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. + +## v3.6.8 + +### 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. diff --git a/docs/content/reference/routing-configuration/http/load-balancing/service.md b/docs/content/reference/routing-configuration/http/load-balancing/service.md index 7039ac3db8..89f5630982 100644 --- a/docs/content/reference/routing-configuration/http/load-balancing/service.md +++ b/docs/content/reference/routing-configuration/http/load-balancing/service.md @@ -292,9 +292,9 @@ To propagate status changes (e.g. all servers of this service are down) upwards, Below are the available options for the health check mechanism: -| Field | Description | Default | Required | -|---------------------|-------------------------------------------------------------------------------------------------------------------------------|---------|----------| -| `path` | Defines the server URL path for the health check endpoint. | "" | Yes | +| Field | Description | Default | Required | +|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `path` | Defines the server URL path for the health check endpoint. The configured path must be relative URL. | "" | Yes | | `scheme` | Replaces the server URL scheme for the health check endpoint. | | No | | `mode` | If defined to `grpc`, will use the gRPC health check protocol to probe the server. | http | No | | `hostname` | Defines the value of hostname in the Host header of the health check request. | "" | No | diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md index 429b654c1f..0d701d9a3d 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md @@ -80,36 +80,36 @@ spec: ## Configuration Options -| Field | Description | Default | Required | -|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | -| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | -| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | -| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | -| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | -| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | -| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | -| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | -| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | -| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | -| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | -| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `strategy` | Strategy defines the load balancing strategy between the servers.
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | +| Field | Description | Default | Required | +|:---------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | +| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | +| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | +| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | +| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | +| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | +| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | +| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | +| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | +| `healthCheck.path` | Server URL path for the health check endpoint.
The configured path must be relative URL.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | +| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | +| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | +| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | +| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | +| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | +| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | +| `strategy` | Strategy defines the load balancing strategy between the servers.
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
Evaluated only if the kind is **Service**. | "RoundRobin" | No | +| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | +| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | ### ExternalName Service diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index a0d1a833ab..d8b7c81294 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -246,6 +246,15 @@ func (shc *ServiceHealthChecker) checkHealthHTTP(ctx context.Context, target *ur } func (shc *ServiceHealthChecker) newRequest(ctx context.Context, target *url.URL) (*http.Request, error) { + pathURL, err := url.Parse(shc.config.Path) + if err != nil { + return nil, fmt.Errorf("parsing health check path: %w", err) + } + + if pathURL.Host != "" || pathURL.Scheme != "" { + return nil, fmt.Errorf("health check path must be a relative URL, got: %q", shc.config.Path) + } + u, err := target.Parse(shc.config.Path) if err != nil { return nil, err diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index cba259ce6e..9f39eaaaf4 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -147,6 +147,14 @@ func TestServiceHealthChecker_newRequest(t *testing.T) { expHostname: "backend1:80", expMethod: http.MethodGet, }, + { + desc: "path is an ablsolute URL", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "http://backend2/health?powpow=do", + }, + expError: true, + }, { desc: "path with param", targetURL: "http://backend1:80", From 2f215ab9a1068248669dacf77ec6901f21051c75 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 11 Feb 2026 17:26:05 +0100 Subject: [PATCH 08/12] Prepare release v3.6.8 --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++ script/gcg/traefik-bugfix.toml | 6 ++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4db215c07..c0472ec1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## [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) + +**Bug fixes:** +- **[acme]** Remove invalid private key in log ([#12574](https://github.com/traefik/traefik/pull/12574) by [juliens](https://github.com/juliens)) +- **[acme]** Alter TLS renewal period ([#12479](https://github.com/traefik/traefik/pull/12479) by [LtHummus](https://github.com/LtHummus)) +- **[healthcheck]** Reject absolute URL in healthcheck path configuration ([#12653](https://github.com/traefik/traefik/pull/12653) by [rtribotte](https://github.com/rtribotte)) +- **[http3]** Bump github.com/quic-go/quic-go to v0.59.0 ([#12553](https://github.com/traefik/traefik/pull/12553) by [jnoordsij](https://github.com/jnoordsij)) +- **[metrics,tracing,accesslogs]** Fix ObservabilityConfig SetDefaults ([#12636](https://github.com/traefik/traefik/pull/12636) by [mmatur](https://github.com/mmatur)) +- **[server]** Remove conn deadline after STARTTLS negociation ([#12639](https://github.com/traefik/traefik/pull/12639) by [rtribotte](https://github.com/rtribotte)) +- **[tls]** Fix verifyServerCertMatchesURI function behavior ([#12575](https://github.com/traefik/traefik/pull/12575) by [kevinpollet](https://github.com/kevinpollet)) +- **[tracing,otel]** Use ParentBased sampler to respect parent span sampling decision ([#12403](https://github.com/traefik/traefik/pull/12403) by [xe-leon](https://github.com/xe-leon)) +- **[webui]** Use url.Parse to validate X-Forwarded-Prefix value ([#12643](https://github.com/traefik/traefik/pull/12643) by [kevinpollet](https://github.com/kevinpollet)) +- **[healthcheck]** Validate healthcheck path configuration (#12642 by @rtribotte) +- **[tls, server]** Cap TLS record length to RFC 8446 limit in ClientHello peeking (#12638 by @mmatur) +- **[service]** Avoid recursion with services ([#12591](https://github.com/traefik/traefik/pull/12591) by [juliens](https://github.com/juliens)) +- **[webui]** Bump dependencies of documentation and webui ([#12581](https://github.com/traefik/traefik/pull/12581) by [gndz07](https://github.com/gndz07)) + +**Documentation:** +- **[k8s]** Fix kubernetes.md with correct http redirections ([#12603](https://github.com/traefik/traefik/pull/12603) by [MartenM](https://github.com/MartenM)) +- **[middleware,k8s/crd]** Fix the errors middleware's document for Kubernetes CRD ([#12600](https://github.com/traefik/traefik/pull/12600) by [yuito-it](https://github.com/yuito-it)) +- **[tls]** Clarify SNI selection ([#12482](https://github.com/traefik/traefik/pull/12482) by [AnuragEkkati](https://github.com/AnuragEkkati)) +- Fix typo on JWT documentation ([#12616](https://github.com/traefik/traefik/pull/12616) by [mdevino](https://github.com/mdevino)) +- Add @gndz07 as a current maintainer ([#12594](https://github.com/traefik/traefik/pull/12594) by [emilevauge](https://github.com/emilevauge)) +- Remove extraneous dots in migration guide ([#12571](https://github.com/traefik/traefik/pull/12571) by [dathbe](https://github.com/dathbe)) +- Document Path matcher placeholder removal in v3 migration guide ([#12570](https://github.com/traefik/traefik/pull/12570) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Improve Service Reference page ([#12541](https://github.com/traefik/traefik/pull/12541) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Document negative priority support for routers ([#12505](https://github.com/traefik/traefik/pull/12505) by [understood-the-assignment](https://github.com/understood-the-assignment)) +- Improve the structure of the routing reference pages ([#12429](https://github.com/traefik/traefik/pull/12429) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Clean Up Menu Entries & Update Expose Overview ([#12405](https://github.com/traefik/traefik/pull/12405) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Split Expose User Guides & Add Multi-Layer Routing Section ([#12238](https://github.com/traefik/traefik/pull/12238) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Remove extra dots in migration guide ([#12573](https://github.com/traefik/traefik/pull/12573) by [rtribotte](https://github.com/rtribotte)) + +**Misc:** +- Merge v2.11 into v3.6 ([#12652](https://github.com/traefik/traefik/pull/12652) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12644](https://github.com/traefik/traefik/pull/12644) by [mmatur](https://github.com/mmatur)) +- Merge branch v2.11 into v3.6 ([#12617](https://github.com/traefik/traefik/pull/12617) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12605](https://github.com/traefik/traefik/pull/12605) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12601](https://github.com/traefik/traefik/pull/12601) by [mmatur](https://github.com/mmatur)) +- Merge branch v2.11 into v3.6 ([#12556](https://github.com/traefik/traefik/pull/12556) by [mmatur](https://github.com/mmatur)) + ## [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 a893c77c61..92354ec43a 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.6.7 +# example new bugfix v3.6.8 CurrentRef = "v3.6" -PreviousRef = "v3.6.6" +PreviousRef = "v3.6.7" BaseBranch = "v3.6" -FutureCurrentRefName = "v3.6.7" +FutureCurrentRefName = "v3.6.8" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000 From 9eea5c4152e20d1b6aa404df6a860ef84f8cecdd Mon Sep 17 00:00:00 2001 From: Tobias Genannt Date: Thu, 12 Feb 2026 14:50:04 +0100 Subject: [PATCH 09/12] Increased content width in documentation --- docs/content/assets/css/content-width.css | 4 ++++ docs/mkdocs.yml | 1 + 2 files changed, 5 insertions(+) create mode 100644 docs/content/assets/css/content-width.css diff --git a/docs/content/assets/css/content-width.css b/docs/content/assets/css/content-width.css new file mode 100644 index 0000000000..e9c7141c88 --- /dev/null +++ b/docs/content/assets/css/content-width.css @@ -0,0 +1,4 @@ +/* Use a wider grid to accommodate table content and code blocks. */ +.md-grid { + max-width: 1650px; +} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 505749e129..e35cadda3a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -39,6 +39,7 @@ extra_javascript: extra_css: - assets/css/menu-icons.css - assets/css/code-copy.css + - assets/css/content-width.css plugins: - search From b8fca6e4600e5b11f2cf5380152aa0a235c1e283 Mon Sep 17 00:00:00 2001 From: Andreas <1084452+a-stangl@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:36:05 +0100 Subject: [PATCH 10/12] Handle empty/missing User-Agent header --- pkg/middlewares/auth/forward.go | 8 ++++ pkg/middlewares/auth/forward_test.go | 68 +++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index dbaddef2c5..6223627252 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -44,6 +44,8 @@ var hopHeaders = []string{ forward.Upgrade, } +var userAgentHeader = http.CanonicalHeaderKey("User-Agent") + type forwardAuth struct { address string authResponseHeaders []string @@ -358,6 +360,12 @@ func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowed RemoveConnectionHeaders(forwardReq) utils.RemoveHeaders(forwardReq.Header, hopHeaders...) + if _, ok := req.Header[userAgentHeader]; !ok { + // If the incoming request doesn't have a User-Agent header set, + // don't send the default Go HTTP client User-Agent for the forwarded request. + forwardReq.Header.Set(userAgentHeader, "") + } + forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders) if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 95e3d7910c..f530fb8ade 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -536,8 +536,7 @@ func Test_writeHeader(t *testing.T) { trustForwardHeader: false, emptyHost: true, expectedHeaders: map[string]string{ - "Accept": "application/json", - "X-Forwarded-Host": "", + "Accept": "application/json", }, }, { @@ -610,6 +609,7 @@ func Test_writeHeader(t *testing.T) { "X-Forwarded-Method": "GET", forward.ProxyAuthenticate: "ProxyAuthenticate", forward.ProxyAuthorization: "ProxyAuthorization", + "User-Agent": "", }, checkForUnexpectedHeaders: true, }, @@ -652,6 +652,65 @@ func Test_writeHeader(t *testing.T) { }, checkForUnexpectedHeaders: true, }, + { + name: "set empty User-Agent header if header is allowed but missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + authRequestHeaders: []string{ + "X-CustomHeader", + "Accept", + "User-Agent", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + "User-Agent": "", + }, + checkForUnexpectedHeaders: true, + }, + { + name: "ignore User-Agent header if header is not allowed and missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + authRequestHeaders: []string{ + "X-CustomHeader", + "Accept", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + }, + checkForUnexpectedHeaders: true, + }, + { + name: "set empty User-Agent header if header is missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + "User-Agent": "", + }, + checkForUnexpectedHeaders: true, + }, } for _, test := range testCases { @@ -673,9 +732,14 @@ func Test_writeHeader(t *testing.T) { expectedHeaders := test.expectedHeaders for key, value := range expectedHeaders { + _, headerExists := actualHeaders[http.CanonicalHeaderKey(key)] + + assert.True(t, headerExists, "Expected header %s not found", key) assert.Equal(t, value, actualHeaders.Get(key)) + actualHeaders.Del(key) } + if test.checkForUnexpectedHeaders { for key := range actualHeaders { assert.Fail(t, "Unexpected header found", key) From 2924e9ad5abd8f1c4ca774b767afd2e2b28b48c5 Mon Sep 17 00:00:00 2001 From: Patrick Evans <31580846+holysoles@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:22:04 +0000 Subject: [PATCH 11/12] Make labelSelector option casing more consistent --- docs/content/providers/kubernetes-gateway.md | 4 ++-- .../install-configuration/providers/kubernetes/knative.md | 2 +- .../providers/kubernetes/kubernetes-crd.md | 2 +- .../providers/kubernetes/kubernetes-gateway.md | 2 +- .../providers/kubernetes/kubernetes-ingress.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/content/providers/kubernetes-gateway.md b/docs/content/providers/kubernetes-gateway.md index 7842bb7217..419cb8cbd4 100644 --- a/docs/content/providers/kubernetes-gateway.md +++ b/docs/content/providers/kubernetes-gateway.md @@ -290,13 +290,13 @@ See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with- ```yaml tab="File (YAML)" providers: kubernetesGateway: - labelselector: "app=traefik" + labelSelector: "app=traefik" # ... ``` ```toml tab="File (TOML)" [providers.kubernetesGateway] - labelselector = "app=traefik" + labelSelector = "app=traefik" # ... ``` diff --git a/docs/content/reference/install-configuration/providers/kubernetes/knative.md b/docs/content/reference/install-configuration/providers/kubernetes/knative.md index 810bf2335c..a4626304fc 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/knative.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/knative.md @@ -91,7 +91,7 @@ The provider then watches for incoming Knative events and derives the correspond | providers.knative.token | Bearer token used for the Kubernetes client configuration. | | | providers.knative.certauthfilepath | Path to the certificate authority file.
Used for the Kubernetes client configuration. | | | providers.knative.namespaces | Array of namespaces to watch.
If left empty, watch all namespaces. | | -| providers.knative.labelselector | Allow filtering Knative Ingress objects using label selectors. | | +| providers.knative.labelSelector | Allow filtering Knative Ingress objects using label selectors. | | | providers.knative.throttleduration | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0 | | providers.knative.privateentrypoints | Entrypoint names used to expose the Ingress privately. If empty local Ingresses are skipped. | | | providers.knative.privateservice | Kubernetes service used to expose the networking controller privately. | | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md index 7f13dcd731..9b66f2a09f 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md @@ -59,7 +59,7 @@ providers: | `providers.kubernetesCRD.token` | Bearer token used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesCRD.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesCRD.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | -| `providers.kubernetesCRD.labelselector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesCRD.labelSelector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | | `providers.kubernetesCRD.ingressClass` | Value of `kubernetes.io/ingress.class` annotation that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No | | `providers.kubernetesCRD.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | | `providers.kubernetesCRD.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md index 2a59a8300f..6992eb3d2b 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md @@ -75,7 +75,7 @@ providers: | `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | -| `providers.kubernetesGateway.labelselector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](./kubernetes-crd.md#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesGateway.labelSelector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](./kubernetes-crd.md#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | | `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | | `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | | `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md index b9df965b79..c601e8166b 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md @@ -52,7 +52,7 @@ which in turn creates the resulting routers, services, handlers, etc. | `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesIngress.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No | -| `providers.kubernetesIngress.labelselector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesIngress.labelSelector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | | `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No | | `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No | | `providers.kubernetesIngress.`
`ingressEndpoint.hostname`
| Hostname used for Kubernetes Ingress endpoints. | "" | No | From 526a34c8ec69422e14d237228e0b5089eed7e439 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Fri, 13 Feb 2026 15:32:04 +0100 Subject: [PATCH 12/12] Remove deprecated swarm:1.0.0 image used in DockerSuite --- integration/docker_test.go | 32 +++--------------------- integration/resources/compose/docker.yml | 17 ++++++------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/integration/docker_test.go b/integration/docker_test.go index 70d38878e3..27c3cc38ab 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -1,8 +1,6 @@ package integration import ( - "encoding/json" - "io" "net/http" "testing" "time" @@ -75,16 +73,8 @@ func (s *DockerSuite) TestDefaultDockerContainers() { require.NoError(s.T(), err) req.Host = "simple.docker.localhost" - resp, err := try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) require.NoError(s.T(), err) - - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) } func (s *DockerSuite) TestDockerContainersWithTCPLabels() { @@ -139,16 +129,8 @@ func (s *DockerSuite) TestDockerContainersWithLabels() { require.NoError(s.T(), err) req.Host = "my.super.host" - resp, err := try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) require.NoError(s.T(), err) - - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) } func (s *DockerSuite) TestDockerContainersWithOneMissingLabels() { @@ -197,17 +179,9 @@ func (s *DockerSuite) TestRestartDockerContainers() { req.Host = "my.super.host" // TODO Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?) - resp, err := try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) require.NoError(s.T(), err) - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("powpow")) require.NoError(s.T(), err) diff --git a/integration/resources/compose/docker.yml b/integration/resources/compose/docker.yml index e594ec87c5..7971dec932 100644 --- a/integration/resources/compose/docker.yml +++ b/integration/resources/compose/docker.yml @@ -1,8 +1,7 @@ version: "3.8" services: simple: - image: swarm:1.0.0 - command: [ "manage", "token://blablabla" ] + image: traefik/whoami withtcplabels: image: traefik/whoamitcp @@ -13,26 +12,24 @@ services: traefik.tcp.Services.Super.Loadbalancer.server.port: 8080 withlabels1: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) withlabels2: - image: swarm:1.0.0 - command: [ "manage", "token://blablabla" ] + image: traefik/whoami labels: traefik.http.Routers.SuperHost.Rule: Host(`my-super.host`) withonelabelmissing: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami labels: traefik.random.value: my.super.host powpow: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami + command: + - --port=2375 labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) traefik.http.Services.powpow.LoadBalancer.server.Port: 2375