Move legacy scheme headers to forwarded headers middleware

This commit is contained in:
qwerty8811 2026-04-10 16:48:00 +00:00 committed by traefiker
parent 6cc3dd8d40
commit 06d0bd5222
10 changed files with 126 additions and 21 deletions

View file

@ -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.<name>.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

View file

@ -86,6 +86,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-entrypoints-name-allowacmebypass" href="#opt-entrypoints-name-allowacmebypass" title="#opt-entrypoints-name-allowacmebypass">entrypoints._name_.allowacmebypass</a> | Enables handling of ACME TLS and HTTP challenges with custom routers. | false |
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
| <a id="opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" href="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" title="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders">entrypoints._name_.forwardedheaders.addxforwardedschemeheaders</a> | Add the X-Forwarded-Scheme and X-Scheme headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
| <a id="opt-entrypoints-name-forwardedheaders-trustedips" href="#opt-entrypoints-name-forwardedheaders-trustedips" title="#opt-entrypoints-name-forwardedheaders-trustedips">entrypoints._name_.forwardedheaders.trustedips</a> | Trust only forwarded headers from selected IPs. | |

View file

@ -89,6 +89,7 @@ additionalArguments:
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
| <a id="opt-forwardedHeaders-connection" href="#opt-forwardedHeaders-connection" title="#opt-forwardedHeaders-connection">`forwardedHeaders.`<br />`connection`</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No |
| <a id="opt-forwardedHeaders-addXForwardedSchemeHeaders" href="#opt-forwardedHeaders-addXForwardedSchemeHeaders" title="#opt-forwardedHeaders-addXForwardedSchemeHeaders">`forwardedHeaders.`<br />`addXForwardedSchemeHeaders`</a> | Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No |
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.`<br />`insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.`<br />`trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | 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`

View file

@ -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.<name>.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.

View file

@ -50,6 +50,7 @@
insecure = true
trustedIPs = ["foobar", "foobar"]
connection = ["foobar", "foobar"]
addXForwardedSchemeHeaders = true
[entryPoints.EntryPoint0.http]
middlewares = ["foobar", "foobar"]
encodeQuerySemicolons = true

View file

@ -61,6 +61,7 @@ entryPoints:
connection:
- foobar
- foobar
addXForwardedSchemeHeaders: true
http:
redirections:
entryPoint:

View file

@ -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.

View file

@ -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)
}

View file

@ -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)

View file

@ -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