diff --git a/docs/content/migrate/nginx-to-traefik.md b/docs/content/migrate/nginx-to-traefik.md index 79499038ba..a8d5833996 100644 --- a/docs/content/migrate/nginx-to-traefik.md +++ b/docs/content/migrate/nginx-to-traefik.md @@ -93,6 +93,12 @@ For a complete list of supported annotations and behavioral differences, see the The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**. +!!! info "Legacy Scheme Headers" + + If your applications still depend on ingress-nginx's legacy `X-Forwarded-Scheme` or `X-Scheme` headers, + enable `entryPoints..forwardedHeaders.addXForwardedSchemeHeaders=true` on the entrypoints that receive this traffic. + This keeps `X-Forwarded-Proto` unchanged and restores the compatibility headers at the entrypoint level for every provider. + --- ## Prerequisites diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 4f5ea989c4..d44c54f081 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -86,6 +86,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | entrypoints._name_.allowacmebypass | Enables handling of ACME TLS and HTTP challenges with custom routers. | false | | entrypoints._name_.asdefault | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false | | entrypoints._name_.forwardedheaders.connection | List of Connection headers that are allowed to pass through the middleware chain before being removed. | | +| entrypoints._name_.forwardedheaders.addxforwardedschemeheaders | Add the X-Forwarded-Scheme and X-Scheme headers. | false | | entrypoints._name_.forwardedheaders.insecure | Trust all forwarded headers. | false | | entrypoints._name_.forwardedheaders.notappendxforwardedfor | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false | | entrypoints._name_.forwardedheaders.trustedips | Trust only forwarded headers from selected IPs. | | diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index e310ffa108..81f15aa5ae 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -89,6 +89,7 @@ additionalArguments: | `asDefault` | Mark the `entryPoint` to be in the list of default `entryPoints`.
`entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.
More information [here](#asdefault). | false | No | | `allowACMEByPass` | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No | | `forwardedHeaders.`
`connection`
| List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No | +| `forwardedHeaders.`
`addXForwardedSchemeHeaders`
| Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No | | `forwardedHeaders.`
`insecure`
| Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).
We recommend to use this option only for tests purposes, not in production. | false | No | | `forwardedHeaders.`
`trustedIPs`
| Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No | | `forwardedHeaders.`
`notAppendXForwardedFor`
| When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No | @@ -392,6 +393,37 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward --entryPoints.web.forwardedHeaders.connection=foobar ``` +??? info "`forwardedHeaders.addXForwardedSchemeHeaders`" + + Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme` next to `X-Forwarded-Proto`. + This is primarily useful when migrating from ingress-nginx and your applications still rely on these legacy headers. + When enabled, these compatibility headers follow the same value as `X-Forwarded-Proto`. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + websecure: + address: ":443" + forwardedHeaders: + addXForwardedSchemeHeaders: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.forwardedHeaders] + addXForwardedSchemeHeaders = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.websecure.address=:443 + --entryPoints.websecure.forwardedHeaders.addXForwardedSchemeHeaders=true + ``` + ### HTTP3 As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint` diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index 64a714e998..c9ce210d36 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -41,6 +41,7 @@ creating the corresponding routers, services, middlewares, and other components Important differences in default behaviors: - **Request buffering**: NGINX enables `proxy-request-buffering` by default, while Traefik requires explicit opt-in via the provider's `proxyRequestBuffering` option. + - **Legacy scheme headers**: If your applications depend on `X-Forwarded-Scheme` or `X-Scheme`, enable `entryPoints..forwardedHeaders.addXForwardedSchemeHeaders=true` on the relevant entrypoints. To ensure consistent behavior during migration, review and configure Traefik's provider-level options to match your current NGINX ConfigMap settings. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 33dacd1140..1395b69d6d 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -50,6 +50,7 @@ insecure = true trustedIPs = ["foobar", "foobar"] connection = ["foobar", "foobar"] + addXForwardedSchemeHeaders = true [entryPoints.EntryPoint0.http] middlewares = ["foobar", "foobar"] encodeQuerySemicolons = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index f2b1d45d8e..2f4f3b77a2 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -61,6 +61,7 @@ entryPoints: connection: - foobar - foobar + addXForwardedSchemeHeaders: true http: redirections: entryPoint: diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 6f6ac1fc34..2b7a28aaa9 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -150,10 +150,11 @@ type TLSConfig struct { // ForwardedHeaders Trust client forwarding headers. type ForwardedHeaders struct { - Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"` - TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"` - Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"` - NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"` + TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"` + Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"` + NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + AddXForwardedSchemeHeaders bool `description:"Add the X-Forwarded-Scheme and X-Scheme headers." json:"addXForwardedSchemeHeaders,omitempty" toml:"addXForwardedSchemeHeaders,omitempty" yaml:"addXForwardedSchemeHeaders,omitempty" export:"true"` } // ProxyProtocol contains Proxy-Protocol configuration. diff --git a/pkg/middlewares/forwardedheaders/forwarded_header.go b/pkg/middlewares/forwardedheaders/forwarded_header.go index 48187d5ef0..a99aaf5a62 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header.go @@ -18,12 +18,14 @@ const ( XForwardedFor = "X-Forwarded-For" XForwardedHost = "X-Forwarded-Host" XForwardedPort = "X-Forwarded-Port" + xForwardedScheme = "X-Forwarded-Scheme" xForwardedServer = "X-Forwarded-Server" XForwardedURI = "X-Forwarded-Uri" XForwardedMethod = "X-Forwarded-Method" XForwardedPrefix = "X-Forwarded-Prefix" xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert" xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info" + xScheme = "X-Scheme" xRealIP = "X-Real-Ip" connection = "Connection" upgrade = "Upgrade" @@ -34,6 +36,7 @@ const ( // that Go's HTTP server preserves (e.g. X_Forwarded_Proto). var XHeadersSet = map[string]struct{}{ XForwardedProto: {}, + xForwardedScheme: {}, XForwardedFor: {}, XForwardedHost: {}, XForwardedPort: {}, @@ -43,6 +46,7 @@ var XHeadersSet = map[string]struct{}{ XForwardedPrefix: {}, xForwardedTLSClientCert: {}, xForwardedTLSClientCertInfo: {}, + xScheme: {}, xRealIP: {}, } @@ -70,17 +74,18 @@ func isManagedXHeader(key string) bool { // Unless insecure is set, // it first removes all the existing values for those headers if the remote address is not one of the trusted ones. type XForwarded struct { - insecure bool - trustedIPs []string - connectionHeaders []string - notAppendXForwardedFor bool - ipChecker *ip.Checker - next http.Handler - hostname string + insecure bool + trustedIPs []string + connectionHeaders []string + notAppendXForwardedFor bool + addXForwardedSchemeHeaders bool + ipChecker *ip.Checker + next http.Handler + hostname string } // NewXForwarded creates a new XForwarded. -func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) { +func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, addXForwardedSchemeHeaders bool, next http.Handler) (*XForwarded, error) { var ipChecker *ip.Checker if len(trustedIPs) > 0 { var err error @@ -101,13 +106,14 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin } return &XForwarded{ - insecure: insecure, - trustedIPs: trustedIPs, - connectionHeaders: canonicalConnectionHeaders, - notAppendXForwardedFor: notAppendXForwardedFor, - ipChecker: ipChecker, - next: next, - hostname: hostname, + insecure: insecure, + trustedIPs: trustedIPs, + connectionHeaders: canonicalConnectionHeaders, + notAppendXForwardedFor: notAppendXForwardedFor, + addXForwardedSchemeHeaders: addXForwardedSchemeHeaders, + ipChecker: ipChecker, + next: next, + hostname: hostname, }, nil } @@ -168,6 +174,14 @@ func (x *XForwarded) rewrite(outreq *http.Request) { unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq)) } + if x.addXForwardedSchemeHeaders { + scheme := unsafeHeader(outreq.Header).Get(XForwardedProto) + if scheme != "" { + unsafeHeader(outreq.Header).Set(xForwardedScheme, scheme) + unsafeHeader(outreq.Header).Set(xScheme, scheme) + } + } + if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" { unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host) } diff --git a/pkg/middlewares/forwardedheaders/forwarded_header_test.go b/pkg/middlewares/forwardedheaders/forwarded_header_test.go index 57f33caed4..ba93ec1ee3 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header_test.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header_test.go @@ -17,6 +17,7 @@ func TestServeHTTP(t *testing.T) { insecure bool trustedIps []string connectionHeaders []string + addSchemeHeaders bool incomingHeaders map[string][]string remoteAddr string expectedHeaders map[string]string @@ -230,6 +231,16 @@ func TestServeHTTP(t *testing.T) { XForwardedProto: "https", }, }, + { + desc: "xForwardedScheme headers with tls", + tls: true, + addSchemeHeaders: true, + expectedHeaders: map[string]string{ + xForwardedProto: "https", + xForwardedScheme: "https", + xScheme: "https", + }, + }, { desc: "xForwardedProto with websocket", tls: false, @@ -238,6 +249,16 @@ func TestServeHTTP(t *testing.T) { XForwardedProto: "ws", }, }, + { + desc: "xForwardedScheme headers with websocket", + websocket: true, + addSchemeHeaders: true, + expectedHeaders: map[string]string{ + xForwardedProto: "ws", + xForwardedScheme: "ws", + xScheme: "ws", + }, + }, { desc: "xForwardedProto with websocket and tls", tls: true, @@ -246,6 +267,17 @@ func TestServeHTTP(t *testing.T) { XForwardedProto: "wss", }, }, + { + desc: "xForwardedScheme headers with websocket and tls", + tls: true, + websocket: true, + addSchemeHeaders: true, + expectedHeaders: map[string]string{ + xForwardedProto: "wss", + xForwardedScheme: "wss", + xScheme: "wss", + }, + }, { desc: "xForwardedProto with websocket and tls and already x-forwarded-proto with wss", tls: true, @@ -257,6 +289,21 @@ func TestServeHTTP(t *testing.T) { XForwardedProto: "wss", }, }, + { + desc: "xForwardedScheme headers overwrite trusted values", + insecure: true, + addSchemeHeaders: true, + incomingHeaders: map[string][]string{ + xForwardedProto: {"https"}, + xForwardedScheme: {"external-https"}, + xScheme: {"external-https"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "https", + xForwardedScheme: "https", + xScheme: "https", + }, + }, { desc: "xForwardedPort with explicit port", host: "foo.com:8080", @@ -643,7 +690,7 @@ func TestServeHTTP(t *testing.T) { } } - m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, + m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, test.addSchemeHeaders, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) require.NoError(t, err) @@ -782,7 +829,7 @@ func TestConnection(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil) + forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, false, nil) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "https://localhost", nil) diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 52e47a7852..84c9573132 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -643,6 +643,7 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E configuration.ForwardedHeaders.TrustedIPs, configuration.ForwardedHeaders.Connection, configuration.ForwardedHeaders.NotAppendXForwardedFor, + configuration.ForwardedHeaders.AddXForwardedSchemeHeaders, next) if err != nil { return nil, err