Merge branch v3.6 into v3.7

This commit is contained in:
mmatur 2026-04-22 11:07:55 +02:00
commit da808bda43
No known key found for this signature in database
GPG Key ID: 2FFE42FC256CFF8E
49 changed files with 3114 additions and 1235 deletions

View File

@ -75,11 +75,6 @@ jobs:
make generate
git diff --exit-code
- name: go mod tidy
run: |
go mod tidy
git diff --exit-code
- name: make generate-crd
run: |
make generate-crd

View File

@ -1,3 +1,37 @@
## [v3.6.14](https://github.com/traefik/traefik/tree/v3.6.14) (2026-04-22)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.13...v3.6.14)
**Bug fixes:**
- **[acme]** Bump github.com/go-acme/lego/v4 to v4.34.0 ([#12993](https://github.com/traefik/traefik/pull/12993) @ldez)
- **[docker]** Downgrade log level for missing container on inspect ([#12900](https://github.com/traefik/traefik/pull/12900) @Otoru)
- **[sticky-session, k8s/crd]** Make SameSite cookie value case-insensitive ([#12922](https://github.com/traefik/traefik/pull/12922) @murataslan1)
- **[k8s/crd, k8s]** Honor allowCrossNamespace with chain middleware CRD ([#12976](https://github.com/traefik/traefik/pull/12976) @rtribotte)
- **[middleware]** Remove untrusted X headers with underscores ([#12961](https://github.com/traefik/traefik/pull/12961) @rtribotte)
- **[middleware]** Sanitize the request URL after stripping the prefix ([#12990](https://github.com/traefik/traefik/pull/12990) @kevinpollet)
- **[middleware]** Deprecate ForwardAuth.TrustForwardHeader option ([#13012](https://github.com/traefik/traefik/pull/13012) @kevinpollet)
- **[middleware, authentication]** Remove map lookup making the basic auth notFoundSecret empty ([#12960](https://github.com/traefik/traefik/pull/12960) @rtribotte)
- **[middleware, authentication]** Fix trustForwardHeader on forward auth middleware ([#12994](https://github.com/traefik/traefik/pull/12994) @juliens)
- **[middleware, authentication]** Cleanup and make ForwardAuth logs consistent ([#13013](https://github.com/traefik/traefik/pull/13013) @kevinpollet)
- **[webui]** Upgrade form-data to 2.5.4, 3.0.4, 4.0.4 ([#12958](https://github.com/traefik/traefik/pull/12958) @orbisai0security)
**Documentation:**
- **[k8s]** Fix yaml indentation ([#12957](https://github.com/traefik/traefik/pull/12957) @isayme)
- **[k8s]** Clarify install config watchNamespace watches only one namespace ([#12962](https://github.com/traefik/traefik/pull/12962) @parkerfath)
- **[k8s/crd]** Update ingressroute.md ([#12916](https://github.com/traefik/traefik/pull/12916) @Rajakavitha1)
- Reverse versions order in migration guide ([#12959](https://github.com/traefik/traefik/pull/12959) @nmengin)
- Update vulnerability submission guidelines ([#12968](https://github.com/traefik/traefik/pull/12968) @emilevauge)
## [v2.11.43](https://github.com/traefik/traefik/tree/v2.11.43) (2026-04-22)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.42...v2.11.43)
**Bug fixes:**
- **[middleware, authentication]** Remove map lookup making the basic auth notFoundSecret empty ([#12960](https://github.com/traefik/traefik/pull/12960) @rtribotte)
- **[middleware, authentication]** Fix trustForwardHeader on forward auth middleware ([#12994](https://github.com/traefik/traefik/pull/12994) @juliens)
- **[middleware, authentication]** Cleanup and make ForwardAuth logs consistent ([#13013](https://github.com/traefik/traefik/pull/13013) @kevinpollet)
- **[middleware]** Remove untrusted X headers with underscores ([#12961](https://github.com/traefik/traefik/pull/12961) @rtribotte)
- **[middleware]** Sanitize the request URL after stripping the prefix ([#12990](https://github.com/traefik/traefik/pull/12990) @kevinpollet)
- **[k8s/crd, k8s]** Honor allowCrossNamespace with chain middleware CRD ([#12976](https://github.com/traefik/traefik/pull/12976) @rtribotte)
## [v3.7.0-rc.1](https://github.com/traefik/traefik/tree/v3.7.0-rc.1) (2026-04-07)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.0-ea.3...v3.7.0-rc.1)

View File

@ -15,6 +15,10 @@ You can subscribe by sending an email to security+subscribe@traefik.io or on [th
Reported vulnerabilities can be found on
[cve.mitre.org](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=traefik).
CVEs are only created for vulnerabilities affecting **Generally Available (GA) versions** of Traefik.
Vulnerabilities discovered in non-GA versions (release candidates, betas, early access, or development branches)
will be fixed without creating a CVE.
## Report a Vulnerability
We want to keep Traefik safe for everyone.
@ -22,6 +26,28 @@ If you've discovered a security vulnerability in Traefik,
we appreciate your help in disclosing it to us in a responsible manner,
by creating a [security advisory](https://github.com/traefik/traefik/security/advisories).
## Code of Conduct for Vulnerability Submissions
We are committed to handling every legitimate report responsibly,
and we expect submitters to engage with our security team in a respectful and collaborative manner.
The following behaviors are **not acceptable** and will not be tolerated:
- **Threats** to publicly disclose the vulnerability if it is not fixed within a timeframe you set unilaterally.
- **Ultimatums** or pressure tactics intended to force a faster response than our normal triage and remediation process allows.
- **Demands** for payment, bug bounties, or any form of compensation in exchange for not disclosing the issue
(Traefik does not operate a paid bug bounty program).
- **Aggressive, abusive, or disrespectful communication** with our security team.
Submitters who engage in any of the above may face the following consequences:
- The submitter **will not be credited** in the security advisory or any subsequent communication.
- The submitter's GitHub profile may be **reported to GitHub** for violation of platform terms of service.
- We may **decline to engage further** on the report, while still addressing the underlying issue if it is legitimate.
We take security seriously and act on legitimate reports as quickly as our resources allow.
Patience and constructive dialogue help us protect users effectively.
## Submission Quality Guidelines
We have been receiving an increasing number of low-quality vulnerability reports that are not actual security issues.

File diff suppressed because it is too large Load Diff

View File

@ -362,6 +362,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie
@ -1390,6 +1393,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -1529,8 +1535,10 @@ spec:
type: boolean
type: object
trustForwardHeader:
description: 'TrustForwardHeader defines whether to trust (ie:
forward) all X-Forwarded-* headers.'
description: |-
TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
type: boolean
type: object
grpcWeb:
@ -3202,6 +3210,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -3449,6 +3460,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -3709,6 +3723,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4075,6 +4092,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4224,6 +4244,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only
@ -4477,6 +4500,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4547,6 +4573,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only

View File

@ -363,6 +363,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie

View File

@ -526,6 +526,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -665,8 +668,10 @@ spec:
type: boolean
type: object
trustForwardHeader:
description: 'TrustForwardHeader defines whether to trust (ie:
forward) all X-Forwarded-* headers.'
description: |-
TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
type: boolean
type: object
grpcWeb:

View File

@ -283,6 +283,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -530,6 +533,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -790,6 +796,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -1156,6 +1165,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -1305,6 +1317,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only
@ -1558,6 +1573,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -1628,6 +1646,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only

View File

@ -39,7 +39,7 @@ This provider discovers all Ingresses in the cluster by default, which may lead
**Best Practices:**
- Use IngressClass to specify which Ingresses should be handled by this provider
- Configure `watchNamespace` to limit discovery to specific namespaces
- Configure `watchNamespace` to limit discovery to a single namespace
- Use `watchNamespaceSelector` to target Ingresses based on namespace labels
### IngressClass Selection Logic

View File

@ -336,7 +336,7 @@ When set to a negative number, the cookie expires immediately.
By default, the affinity cookie is created without those flags.
One however can change that through configuration.
`SameSite` can be `none`, `lax`, `strict` or empty.
`SameSite` can be `none`, `lax`, `strict` or empty. Values are case-insensitive.
#### Domain

View File

@ -54,9 +54,9 @@ spec:
## Configuration Options
| Field | Description | Default | Required |
|:-----------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
|:-----------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| <a id="opt-address" href="#opt-address" title="#opt-address">`address`</a> | Authentication server address. | "" | Yes |
| <a id="opt-trustForwardHeader" href="#opt-trustForwardHeader" title="#opt-trustForwardHeader">`trustForwardHeader`</a> | Trust all `X-Forwarded-*` headers. | false | No |
| <a id="opt-trustForwardHeader" href="#opt-trustForwardHeader" title="#opt-trustForwardHeader">`trustForwardHeader`</a> | Trust all `X-Forwarded-*` headers. <br/>The trustForwardHeader option is deprecated and will be removed in the next major version. <br/>More information [here](#trustforwardheader)| false | No |
| <a id="opt-authResponseHeaders" href="#opt-authResponseHeaders" title="#opt-authResponseHeaders">`authResponseHeaders`</a> | List of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers. | [] | No |
| <a id="opt-authResponseHeadersRegex" href="#opt-authResponseHeadersRegex" title="#opt-authResponseHeadersRegex">`authResponseHeadersRegex`</a> | Regex to match by the headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.<br /> More information [here](#authresponseheadersregex). | "" | No |
| <a id="opt-authRequestHeaders" href="#opt-authRequestHeaders" title="#opt-authRequestHeaders">`authRequestHeaders`</a> | List of the headers to copy from the request to the authentication server. <br /> It allows filtering headers that should not be passed to the authentication server. <br /> If not set or empty, then all request headers are passed. | [] | No |
@ -128,6 +128,25 @@ If left unset, the request body size is unrestricted which can have performance
It is strongly recommended to set this option to a suitable value.
Not setting it (or setting it to `-1`) allows unlimited response body sizes which can lead to DoS attacks and memory exhaustion.
### trustForwardHeader
!!! warning
`trustForwardHeader` option is deprecated and will be removed in the next major version.
Configure the trusted IPs at the [EntryPoint level](../../../install-configuration/entrypoints.md#forwarded-headers) using `forwardedHeaders.trustedIPs`,
and set `trustForwardHeader` to `true` on this middleware.
With this setup, the EntryPoint is responsible for sanitizing incoming `X-Forwarded-*` headers:
it strips any such headers sent by untrusted clients and only preserves those coming from trusted upstream proxies.
By the time the ForwardAuth middleware processes the request, all `X-Forwarded-*` headers are guaranteed to be trustworthy,
including those intentionally added by other middlewares in the chain — for example, the `X-Forwarded-Prefix` header set by the [StripPrefix](stripprefix.md) middleware.
If `trustForwardHeader` is not explicitly set, Traefik will log a warning at startup and use a legacy behavior where some `X-Forwarded-*` headers (e.g. `X-Forwarded-For`, `X-Forwarded-Proto`) are removed but others (e.g. `X-Forwarded-Prefix`) are forwarded untouched.
To silence this warning, explicitly set `trustForwardHeader` to `true` or `false`.
Set the `trustForwardHeader` option to `true` to trust all `X-Forwarded-*` headers.
## Forward-Request Headers
The following request properties are provided to the forward-auth target endpoint as `X-Forwarded-` headers.

View File

@ -5,7 +5,7 @@ description: "An IngressRoute is a Traefik CRD is in charge of connecting incomi
`IngressRoute` is the CRD implementation of a [Traefik HTTP router](../../../http/routing/rules-and-priority.md).
Before creating `IngressRoute` objects, you need to apply the [Traefik Kubernetes CRDs](https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions) to your Kubernetes cluster.
Before creating `IngressRoute` objects, you need to apply the Traefik Kubernetes CRDs such as [Definitions](/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml) and [RBAC](/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml) to your Kubernetes cluster.
This registers the `IngressRoute` kind and other Traefik-specific resources.

View File

@ -157,7 +157,7 @@ data:
| <a id="opt-sticky-cookie-name" href="#opt-sticky-cookie-name" title="#opt-sticky-cookie-name">`sticky.`<br />`cookie.name`</a> | Name of the cookie used for the stickiness at the WRR service level.<br />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.<br />On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.<br />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).<br />More information about WRR stickiness [here](#stickiness-on-multiple-levels) | Abbreviation of a sha1<br />(ex: `_1d52e`). | No |
| <a id="opt-sticky-cookie-httpOnly" href="#opt-sticky-cookie-httpOnly" title="#opt-sticky-cookie-httpOnly">`sticky.`<br />`cookie.httpOnly`</a> | Allow the cookie used for the stickiness at the WRR service level to be accessed by client-side APIs, such as JavaScript.<br />More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No |
| <a id="opt-sticky-cookie-secure" href="#opt-sticky-cookie-secure" title="#opt-sticky-cookie-secure">`sticky.`<br />`cookie.secure`</a> | Allow the cookie used for the stickiness at the WRR service level to be only transmitted over an encrypted connection (i.e. HTTPS).<br />More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No |
| <a id="opt-sticky-cookie-sameSite" href="#opt-sticky-cookie-sameSite" title="#opt-sticky-cookie-sameSite">`sticky.`<br />`cookie.sameSite`</a> | [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy for the cookie used for the stickiness at the WRR service level.<br />Allowed values:<br />-`none`<br />-`lax`<br />`strict`<br />More information about WRR stickiness [here](#stickiness-on-multiple-levels) | "" | No |
| <a id="opt-sticky-cookie-sameSite" href="#opt-sticky-cookie-sameSite" title="#opt-sticky-cookie-sameSite">`sticky.`<br />`cookie.sameSite`</a> | [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy for the cookie used for the stickiness at the WRR service level.<br />Allowed values:<br />-`none`<br />-`lax`<br />-`strict`<br />-`None`<br />-`Lax`<br />-`Strict`<br />More information about WRR stickiness [here](#stickiness-on-multiple-levels) | "" | No |
| <a id="opt-sticky-cookie-maxAge" href="#opt-sticky-cookie-maxAge" title="#opt-sticky-cookie-maxAge">`sticky.`<br />`cookie.maxAge`</a> | Number of seconds until the cookie used for the stickiness at the WRR service level expires.<br />Negative number, the cookie expires immediately.<br />0, the cookie never expires. | 0 | No |
#### Stickiness on multiple levels
@ -345,7 +345,7 @@ spec:
| <a id="opt-servicesm-sticky-cookie-name" href="#opt-servicesm-sticky-cookie-name" title="#opt-servicesm-sticky-cookie-name">`services[m].`<br />`sticky.`<br />`cookie.name`</a> | Name of the cookie used for the stickiness.<br />Evaluated only if the kind is **Service**. | Abbreviation of a sha1<br />(ex: `_1d52e`). | No |
| <a id="opt-servicesm-sticky-cookie-httpOnly" href="#opt-servicesm-sticky-cookie-httpOnly" title="#opt-servicesm-sticky-cookie-httpOnly">`services[m].`<br />`sticky.`<br />`cookie.httpOnly`</a> | Allow the cookie can be accessed by client-side APIs, such as JavaScript.<br />Evaluated only if the kind is **Service**. | false | No |
| <a id="opt-servicesm-sticky-cookie-secure" href="#opt-servicesm-sticky-cookie-secure" title="#opt-servicesm-sticky-cookie-secure">`services[m].`<br />`sticky.`<br />`cookie.secure`</a> | Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).<br />Evaluated only if the kind is **Service**. | false | No |
| <a id="opt-servicesm-sticky-cookie-sameSite" href="#opt-servicesm-sticky-cookie-sameSite" title="#opt-servicesm-sticky-cookie-sameSite">`services[m].`<br />`sticky.`<br />`cookie.sameSite`</a> | [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.<br />Allowed values:<br />-`none`<br />-`lax`<br />`strict`<br />Evaluated only if the kind is **Service**. | "" | No |
| <a id="opt-servicesm-sticky-cookie-sameSite" href="#opt-servicesm-sticky-cookie-sameSite" title="#opt-servicesm-sticky-cookie-sameSite">`services[m].`<br />`sticky.`<br />`cookie.sameSite`</a> | [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.<br />Allowed values (case-insensitive):<br />-`none`<br />-`lax`<br />-`strict`<br />-`None`<br />-`Lax`<br />-`Strict`<br />Evaluated only if the kind is **Service**. | "" | No |
| <a id="opt-servicesm-sticky-cookie-maxAge" href="#opt-servicesm-sticky-cookie-maxAge" title="#opt-servicesm-sticky-cookie-maxAge">`services[m].`<br />`sticky.`<br />`cookie.maxAge`</a> | Number of seconds until the cookie expires.<br />Negative number, the cookie expires immediately.<br />0, the cookie never expires.<br />Evaluated only if the kind is **Service**. | 0 | No |
| <a id="opt-servicesm-strategy" href="#opt-servicesm-strategy" title="#opt-servicesm-strategy">`services[m].`<br />`strategy`</a> | Load balancing strategy between the servers.<br />RoundRobin is the only supported value yet.<br />Evaluated only if the kind is **Service**. | "RoundRobin" | No |
| <a id="opt-servicesm-nativeLB" href="#opt-servicesm-nativeLB" title="#opt-servicesm-nativeLB">`services[m].`<br />`nativeLB`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.<br />Evaluated only if the kind is **Service**. | false | No |

View File

@ -153,9 +153,9 @@ ingressClass:
# Providers tell Traefik where to find routing configuration.
providers:
kubernetesIngress:
enabled: false
enabled: false
kubernetesGateway:
enabled: true
enabled: true
## Gateway Listeners
gateway:

155
go.mod
View File

@ -8,14 +8,15 @@ require (
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 // No tag on the repo.
github.com/andybalholm/brotli v1.2.0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.8
github.com/aws/aws-sdk-go-v2/credentials v1.19.8
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.32.14
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
github.com/aws/aws-sdk-go-v2/service/ec2 v1.203.1
github.com/aws/aws-sdk-go-v2/service/ecs v1.53.15
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13
github.com/aws/smithy-go v1.24.0
github.com/aws/smithy-go v1.24.2
github.com/cenkalti/backoff/v4 v4.3.0
github.com/containerd/errdefs v1.0.0
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
github.com/coreos/go-systemd/v22 v22.5.0
github.com/docker/cli v29.2.1+incompatible
@ -23,7 +24,7 @@ require (
github.com/docker/go-connections v0.6.0
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-acme/lego/v4 v4.33.0
github.com/go-acme/lego/v4 v4.34.0
github.com/go-kit/kit v0.13.0
github.com/go-kit/log v0.2.1
github.com/golang/protobuf v1.5.4
@ -33,7 +34,7 @@ require (
github.com/hashicorp/consul/api v1.26.1
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/go-version v1.8.0
github.com/hashicorp/go-version v1.9.0
github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b // No tag on the repo.
github.com/http-wasm/http-wasm-host-go v0.7.0
github.com/huandu/xstrings v1.5.0
@ -83,7 +84,7 @@ require (
go.opentelemetry.io/collector/pdata v1.41.0
go.opentelemetry.io/contrib/bridges/otellogrus v0.13.0
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
go.opentelemetry.io/otel v1.41.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0
@ -92,20 +93,20 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0
go.opentelemetry.io/otel/log v0.17.0
go.opentelemetry.io/otel/metric v1.41.0
go.opentelemetry.io/otel/sdk v1.41.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/log v0.17.0
go.opentelemetry.io/otel/sdk/metric v1.41.0
go.opentelemetry.io/otel/trace v1.41.0
golang.org/x/crypto v0.48.0
golang.org/x/mod v0.32.0
golang.org/x/net v0.51.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
golang.org/x/time v0.14.0
golang.org/x/tools v0.41.0
google.golang.org/grpc v1.79.3
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/crypto v0.50.0
golang.org/x/mod v0.35.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.44.0
google.golang.org/grpc v1.80.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.2
@ -124,7 +125,7 @@ require (
)
require (
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
@ -152,37 +153,38 @@ require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea v1.4.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
github.com/aliyun/credentials-go v1.4.7 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.53.0 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aziontech/azionapi-go-sdk v0.144.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.260 // indirect
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bodgit/tsig v1.2.2 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
@ -199,11 +201,11 @@ require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exoscale/egoscale/v3 v3.1.33 // indirect
github.com/exoscale/egoscale/v3 v3.1.34 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
@ -212,7 +214,7 @@ require (
github.com/go-acme/tencentclouddnspod v1.3.24 // indirect
github.com/go-acme/tencentedgdeone v1.3.38 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@ -225,22 +227,22 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-zookeeper/zk v1.0.3 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gravitational/trace v1.5.0 // indirect
@ -256,11 +258,18 @@ require (
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.192 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.11.0 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
@ -270,8 +279,8 @@ require (
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.65.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/linode/linodego v1.67.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magiconair/properties v1.8.10 // indirect
@ -280,7 +289,7 @@ require (
github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
@ -309,14 +318,15 @@ require (
github.com/nrdcg/mailinabox v0.3.0 // indirect
github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.111.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.111.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nrdcg/vegadns v0.3.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
@ -330,7 +340,7 @@ require (
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
github.com/sacloud/api-client-go v0.3.3 // indirect
github.com/sacloud/api-client-go v0.3.5 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.23.1 // indirect
github.com/sacloud/packages-go v0.0.12 // indirect
@ -338,7 +348,7 @@ require (
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/selectel/go-selvpcclient/v4 v4.2.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/softlayer/softlayer-go v1.2.1 // indirect
@ -349,33 +359,34 @@ require (
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.77 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/transip/gotransip/v6 v6.26.1 // indirect
github.com/transip/gotransip/v6 v6.26.2 // indirect
github.com/ucloud/ucloud-sdk-go v0.22.61 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vinyldns/go-vinyldns v0.9.17 // indirect
github.com/volcengine/volc-sdk-golang v1.0.237 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/volcengine/volc-sdk-golang v1.0.241 // indirect
github.com/vultr/govultr/v3 v3.30.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yandex-cloud/go-genproto v0.54.0 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.36 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.56.0 // indirect
github.com/yandex-cloud/go-genproto v0.71.0 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.52 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.88.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/v3 v3.6.5 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/featuregate v1.41.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
@ -388,13 +399,13 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/term v0.42.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/api v0.275.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect

1386
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -363,6 +363,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie
@ -1391,6 +1394,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -1530,8 +1536,10 @@ spec:
type: boolean
type: object
trustForwardHeader:
description: 'TrustForwardHeader defines whether to trust (ie:
forward) all X-Forwarded-* headers.'
description: |-
TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
type: boolean
type: object
grpcWeb:
@ -3203,6 +3211,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -3450,6 +3461,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -3710,6 +3724,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4076,6 +4093,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4225,6 +4245,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only
@ -4478,6 +4501,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can
@ -4548,6 +4574,9 @@ spec:
- none
- lax
- strict
- None
- Lax
- Strict
type: string
secure:
description: Secure defines whether the cookie can only

View File

@ -339,7 +339,7 @@ type Cookie struct {
HTTPOnly bool `json:"httpOnly,omitempty" toml:"httpOnly,omitempty" yaml:"httpOnly,omitempty" export:"true"`
// SameSite defines the same site policy.
// More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
// +kubebuilder:validation:Enum=none;lax;strict
// +kubebuilder:validation:Enum=none;lax;strict;None;Lax;Strict
SameSite string `json:"sameSite,omitempty" toml:"sameSite,omitempty" yaml:"sameSite,omitempty" export:"true"`
// MaxAge defines the number of seconds until the cookie expires.
// When set to a negative number, the cookie expires immediately.

View File

@ -288,7 +288,9 @@ type ForwardAuth struct {
// TLS defines the configuration used to secure the connection to the authentication server.
TLS *ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
// TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
TrustForwardHeader bool `json:"trustForwardHeader,omitempty" toml:"trustForwardHeader,omitempty" yaml:"trustForwardHeader,omitempty" export:"true"`
//
// Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
TrustForwardHeader *bool `json:"trustForwardHeader,omitempty" toml:"trustForwardHeader,omitempty" yaml:"trustForwardHeader,omitempty" export:"true"`
// AuthResponseHeaders defines the list of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers.
AuthResponseHeaders []string `json:"authResponseHeaders,omitempty" toml:"authResponseHeaders,omitempty" yaml:"authResponseHeaders,omitempty" export:"true"`
// AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.

View File

@ -473,6 +473,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = new(ClientTLS)
(*in).DeepCopyInto(*out)
}
if in.TrustForwardHeader != nil {
in, out := &in.TrustForwardHeader, &out.TrustForwardHeader
*out = new(bool)
**out = **in
}
if in.AuthResponseHeaders != nil {
in, out := &in.AuthResponseHeaders, &out.AuthResponseHeaders
*out = make([]string, len(*in))

View File

@ -586,7 +586,7 @@ func TestDecodeConfiguration(t *testing.T) {
InsecureSkipVerify: true,
CAOptional: pointer(true),
},
TrustForwardHeader: true,
TrustForwardHeader: pointer(true),
AuthResponseHeaders: []string{
"foobar",
"fiibar",
@ -1146,7 +1146,7 @@ func TestEncodeConfiguration(t *testing.T) {
InsecureSkipVerify: true,
CAOptional: pointer(true),
},
TrustForwardHeader: true,
TrustForwardHeader: pointer(true),
AuthResponseHeaders: []string{
"foobar",
"fiibar",

View File

@ -469,8 +469,8 @@ func usernameIfPresent(theURL *url.URL) string {
return "-"
}
var requestCounter uint64 // Request ID
var requestCounter atomic.Uint64 // Request ID
func nextRequestCount() uint64 {
return atomic.AddUint64(&requestCounter, 1)
return requestCounter.Add(1)
}

View File

@ -46,7 +46,7 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
// To prevent timing attacks, we need to compute a hash even if the user is not found.
// We assume it to be safe only when the users hashes are all from the same algorithm,
// so we can pick the first one as a random hash to compute.
notFoundSecret := users[slices.Collect(maps.Values(users))[0]]
notFoundSecret := slices.Collect(maps.Values(users))[0]
ba := &basicAuth{
next: next,

View File

@ -16,6 +16,17 @@ import (
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
func TestNewBasicNotFoundSecretIsSet(t *testing.T) {
auth := dynamic.BasicAuth{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
}
middleware, err := NewBasic(t.Context(), nil, auth, "authName")
require.NoError(t, err)
ba := middleware.(*basicAuth)
assert.Equal(t, "$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", ba.notFoundSecret)
}
func TestBasicAuthFail(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")

View File

@ -6,16 +6,19 @@ import (
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
"time"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/forwardedheaders"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/observability/tracing"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
@ -27,10 +30,7 @@ import (
const typeNameForward = "ForwardAuth"
const (
xForwardedURI = "X-Forwarded-Uri"
xForwardedMethod = "X-Forwarded-Method"
)
var errResponseBodyTooLarge = errors.New("response body too large")
// hopHeaders Hop-by-hop headers to be removed in the authentication request.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
@ -53,7 +53,7 @@ type forwardAuth struct {
next http.Handler
name string
client http.Client
trustForwardHeader bool
trustForwardHeader *bool
authRequestHeaders []string
maxResponseBodySize int64
addAuthCookiesToResponse map[string]struct{}
@ -94,14 +94,14 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
if config.MaxBodySize != nil {
fa.maxBodySize = *config.MaxBodySize
} else if fa.forwardBody {
logger.Warn().Msgf("ForwardAuth 'maxBodySize' is not configured with 'forwardBody: true', allowing unlimited request body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
logger.Warn().Msgf("maxBodySize is not configured with 'forwardBody: true', allowing unlimited request body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
}
if config.MaxResponseBodySize != nil {
fa.maxResponseBodySize = *config.MaxResponseBodySize
} else {
fa.maxResponseBodySize = -1
logger.Warn().Msg("ForwardAuth 'maxResponseBodySize' is not configured, allowing unlimited response body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
logger.Warn().Msg("maxResponseBodySize is not configured, allowing unlimited response body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
}
// Ensure our request client does not follow redirects
@ -114,7 +114,7 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
if config.TLS != nil {
if config.TLS.CAOptional != nil {
logger.Warn().Msg("CAOptional option is deprecated, TLS client authentication is a server side option, please remove any usage of this option.")
logger.Warn().Msg("tls.CAOptional option is deprecated, TLS client authentication is a server side option, please remove any usage of this option.")
}
clientTLS := &types.ClientTLS{
@ -142,6 +142,12 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
fa.authResponseHeadersRegex = re
}
if config.TrustForwardHeader == nil {
logger.Warn().Msg("trustForwardHeader is not configured: this creates an inconsistent security behavior where some X-Forwarded headers (e.g. X-Forwarded-For, X-Forwarded-Proto) are removed but others (e.g. X-Forwarded-Prefix) are forwarded untouched. Please set it to false to remove all X-Forwarded headers, or true to trust them all.")
} else if *config.TrustForwardHeader && len(fa.authRequestHeaders) > 0 {
fa.authRequestHeaders = append(fa.authRequestHeaders, slices.Collect(maps.Keys(forwardedheaders.XHeadersSet))...)
}
return fa, nil
}
@ -190,7 +196,11 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
writeHeader(req, forwardReq, fa.trustForwardHeader, fa.authRequestHeaders)
if fa.trustForwardHeader != nil {
writeHeader(req, forwardReq, *fa.trustForwardHeader, fa.authRequestHeaders)
} else {
oldWriteHeader(req, forwardReq, fa.authRequestHeaders)
}
var forwardSpan trace.Span
var tracer *tracing.Tracer
@ -380,8 +390,6 @@ func (fa *forwardAuth) readBodyBytes(req *http.Request) ([]byte, error) {
return nil, errBodyTooLarge
}
var errResponseBodyTooLarge = errors.New("response body too large")
func (fa *forwardAuth) readResponseBodyBytes(res *http.Response) ([]byte, error) {
if fa.maxResponseBodySize < 0 {
return io.ReadAll(res.Body)
@ -407,6 +415,10 @@ func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowed
RemoveConnectionHeaders(forwardReq)
utils.RemoveHeaders(forwardReq.Header, hopHeaders...)
if !trustForwardHeader {
forwardedheaders.DeleteXForwardedHeaders(forwardReq.Header)
}
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.
@ -416,59 +428,61 @@ func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowed
forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders)
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
if trustForwardHeader {
if prior, ok := req.Header[forward.XForwardedFor]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
if prior, ok := forwardReq.Header[forwardedheaders.XForwardedFor]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
forwardReq.Header.Set(forward.XForwardedFor, clientIP)
forwardReq.Header.Set(forwardedheaders.XForwardedFor, clientIP)
}
xMethod := req.Header.Get(xForwardedMethod)
switch {
case xMethod != "" && trustForwardHeader:
forwardReq.Header.Set(xForwardedMethod, xMethod)
case req.Method != "":
forwardReq.Header.Set(xForwardedMethod, req.Method)
default:
forwardReq.Header.Del(xForwardedMethod)
if _, ok := forwardReq.Header[forwardedheaders.XForwardedMethod]; !ok {
forwardReq.Header.Set(forwardedheaders.XForwardedMethod, req.Method)
}
xfp := req.Header.Get(forward.XForwardedProto)
switch {
case xfp != "" && trustForwardHeader:
forwardReq.Header.Set(forward.XForwardedProto, xfp)
case req.TLS != nil:
forwardReq.Header.Set(forward.XForwardedProto, "https")
default:
forwardReq.Header.Set(forward.XForwardedProto, "http")
if _, ok := forwardReq.Header[forwardedheaders.XForwardedProto]; !ok {
forwardReq.Header.Set(forwardedheaders.XForwardedProto, "http")
if req.TLS != nil {
forwardReq.Header.Set(forwardedheaders.XForwardedProto, "https")
}
}
if xfp := req.Header.Get(forward.XForwardedPort); xfp != "" && trustForwardHeader {
forwardReq.Header.Set(forward.XForwardedPort, xfp)
if _, ok := forwardReq.Header[forwardedheaders.XForwardedPort]; !ok {
forwardReq.Header.Set(forwardedheaders.XForwardedPort, forwardedPort(req))
}
xfh := req.Header.Get(forward.XForwardedHost)
switch {
case xfh != "" && trustForwardHeader:
forwardReq.Header.Set(forward.XForwardedHost, xfh)
case req.Host != "":
forwardReq.Header.Set(forward.XForwardedHost, req.Host)
default:
forwardReq.Header.Del(forward.XForwardedHost)
if _, ok := forwardReq.Header[forwardedheaders.XForwardedHost]; !ok {
forwardReq.Header.Set(forwardedheaders.XForwardedHost, req.Host)
}
xfURI := req.Header.Get(xForwardedURI)
switch {
case xfURI != "" && trustForwardHeader:
forwardReq.Header.Set(xForwardedURI, xfURI)
case req.URL.RequestURI() != "":
forwardReq.Header.Set(xForwardedURI, req.URL.RequestURI())
default:
forwardReq.Header.Del(xForwardedURI)
if _, ok := forwardReq.Header[forwardedheaders.XForwardedURI]; !ok {
forwardReq.Header.Set(forwardedheaders.XForwardedURI, req.URL.RequestURI())
}
}
// oldWriteHeader is the legacy implementation of writeHeader, which is used when TrustForwardHeader is not set (old false behavior).
// It is kept to avoid breaking existing configurations that rely on the previous behavior.
func oldWriteHeader(req, forwardReq *http.Request, allowedHeaders []string) {
utils.CopyHeaders(forwardReq.Header, req.Header)
RemoveConnectionHeaders(forwardReq)
utils.RemoveHeaders(forwardReq.Header, hopHeaders...)
forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders)
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
forwardReq.Header.Set(forwardedheaders.XForwardedFor, clientIP)
}
proto := "http"
if req.TLS != nil {
proto = "https"
}
forwardReq.Header.Set(forwardedheaders.XForwardedProto, proto)
forwardReq.Header.Set(forwardedheaders.XForwardedMethod, req.Method)
forwardReq.Header.Set(forwardedheaders.XForwardedHost, req.Host)
forwardReq.Header.Set(forwardedheaders.XForwardedURI, req.URL.RequestURI())
}
func filterForwardRequestHeaders(forwardRequestHeaders http.Header, allowedHeaders []string) http.Header {
if len(allowedHeaders) == 0 {
return forwardRequestHeaders
@ -476,11 +490,30 @@ func filterForwardRequestHeaders(forwardRequestHeaders http.Header, allowedHeade
filteredHeaders := http.Header{}
for _, headerName := range allowedHeaders {
values := forwardRequestHeaders.Values(headerName)
if len(values) > 0 {
filteredHeaders[http.CanonicalHeaderKey(headerName)] = append([]string(nil), values...)
if values := forwardRequestHeaders.Values(headerName); len(values) > 0 {
filteredHeaders[http.CanonicalHeaderKey(headerName)] = values
}
}
return filteredHeaders
}
func forwardedPort(req *http.Request) string {
if req == nil {
return ""
}
if _, port, err := net.SplitHostPort(req.Host); err == nil && port != "" {
return port
}
if req.Header.Get(forwardedheaders.XForwardedProto) == "https" || req.Header.Get(forwardedheaders.XForwardedProto) == "wss" {
return "443"
}
if req.TLS != nil {
return "443"
}
return "80"
}

View File

@ -480,9 +480,9 @@ func TestForwardAuthForwardError(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, recorder.Result().StatusCode)
}
func Test_writeHeader(t *testing.T) {
func TestForwardAuth_writeHeader(t *testing.T) {
testCases := []struct {
name string
desc string
headers map[string]string
authRequestHeaders []string
trustForwardHeader bool
@ -491,7 +491,7 @@ func Test_writeHeader(t *testing.T) {
checkForUnexpectedHeaders bool
}{
{
name: "trust Forward Header",
desc: "trust forward header",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -503,7 +503,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "not trust Forward Header",
desc: "not trust forward header",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -515,7 +515,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "trust Forward Header with empty Host",
desc: "trust forward header with empty Host",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -528,7 +528,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "not trust Forward Header with empty Host",
desc: "not trust forward header with empty Host",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -540,7 +540,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "trust Forward Header with forwarded URI",
desc: "trust forward header with forwarded URI",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -554,7 +554,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "not trust Forward Header with forward requested URI",
desc: "not trust forward header with forward requested URI",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
@ -568,7 +568,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "trust Forward Header with forwarded request Method",
desc: "trust forward header with forwarded request Method",
headers: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
@ -578,7 +578,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "not trust Forward Header with forward request Method",
desc: "not trust forward header with forward request Method",
headers: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
@ -588,7 +588,7 @@ func Test_writeHeader(t *testing.T) {
},
},
{
name: "remove hop-by-hop headers",
desc: "remove hop-by-hop headers",
headers: map[string]string{
forward.Connection: "Connection",
forward.KeepAlive: "KeepAlive",
@ -607,6 +607,7 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
forward.ProxyAuthenticate: "ProxyAuthenticate",
forward.ProxyAuthorization: "ProxyAuthorization",
"User-Agent": "",
@ -614,7 +615,7 @@ func Test_writeHeader(t *testing.T) {
checkForUnexpectedHeaders: true,
},
{
name: "filter forward request headers",
desc: "filter forward request headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
@ -629,11 +630,12 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
},
checkForUnexpectedHeaders: true,
},
{
name: "filter forward request headers doesn't add new headers",
desc: "filter forward request headers doesn't add new headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
@ -649,11 +651,12 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
},
checkForUnexpectedHeaders: true,
},
{
name: "set empty User-Agent header if header is allowed but missing",
desc: "set empty User-Agent header if header is allowed but missing",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Accept": "application/json",
@ -670,12 +673,13 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
"User-Agent": "",
},
checkForUnexpectedHeaders: true,
},
{
name: "ignore User-Agent header if header is not allowed and missing",
desc: "ignore User-Agent header if header is not allowed and missing",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Accept": "application/json",
@ -691,11 +695,12 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
},
checkForUnexpectedHeaders: true,
},
{
name: "set empty User-Agent header if header is missing",
desc: "set empty User-Agent header if header is missing",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Accept": "application/json",
@ -707,14 +712,64 @@ func Test_writeHeader(t *testing.T) {
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
"User-Agent": "",
},
checkForUnexpectedHeaders: true,
},
{
desc: "authRequestHeaders and XForwarded are kept if trusted",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Uri": "/path?q=2",
},
authRequestHeaders: []string{
"X-CustomHeader",
},
trustForwardHeader: true,
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=2",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
},
checkForUnexpectedHeaders: true,
},
{
desc: "X-Forwarded and X_forwarded headers are removed when not trusted",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"X_forwarded_for": "127.0.0.1",
"X-Forwarded-Proto": "xxx",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Port": "80",
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
cfg := dynamic.ForwardAuth{
TrustForwardHeader: &test.trustForwardHeader,
AuthRequestHeaders: test.authRequestHeaders,
}
hdl, err := NewForward(t.Context(), nil, cfg, "test")
require.NoError(t, err)
fwdAuth, ok := hdl.(*forwardAuth)
require.True(t, ok)
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
for key, value := range test.headers {
req.Header.Set(key, value)
@ -726,7 +781,7 @@ func Test_writeHeader(t *testing.T) {
forwardReq := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
writeHeader(req, forwardReq, test.trustForwardHeader, test.authRequestHeaders)
writeHeader(req, forwardReq, *fwdAuth.trustForwardHeader, fwdAuth.authRequestHeaders)
actualHeaders := forwardReq.Header
@ -735,13 +790,184 @@ func Test_writeHeader(t *testing.T) {
_, headerExists := actualHeaders[http.CanonicalHeaderKey(key)]
assert.True(t, headerExists, "Expected header %s not found", key)
assert.Equal(t, value, actualHeaders.Get(key))
assert.Equal(t, value, forwardReq.Header.Get(key))
actualHeaders.Del(key)
forwardReq.Header.Del(key)
}
if test.checkForUnexpectedHeaders {
for key := range actualHeaders {
for key := range forwardReq.Header {
assert.Fail(t, "Unexpected header found", key)
}
}
})
}
}
func TestForwardAuth_oldWriteHeader(t *testing.T) {
testCases := []struct {
desc string
headers map[string]string
authRequestHeaders []string
emptyHost bool
expectedHeaders map[string]string
checkForUnexpectedHeaders bool
}{
{
desc: "not trust forward header",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "foo.bar",
},
},
{
desc: "not trust forward header with empty Host",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
emptyHost: true,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "",
},
},
{
desc: "not trust forward header with forward requested URI",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
"X-Forwarded-Uri": "/forward?q=1",
},
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
},
},
{
desc: "not trust forward header with forward request Method",
headers: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
expectedHeaders: map[string]string{
"X-Forwarded-Method": "GET",
},
},
{
desc: "remove hop-by-hop headers",
headers: map[string]string{
forward.Connection: "Connection",
forward.KeepAlive: "KeepAlive",
forward.ProxyAuthenticate: "ProxyAuthenticate",
forward.ProxyAuthorization: "ProxyAuthorization",
forward.Te: "Te",
forward.Trailers: "Trailers",
forward.TransferEncoding: "TransferEncoding",
forward.Upgrade: "Upgrade",
"X-CustomHeader": "CustomHeader",
},
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
forward.ProxyAuthenticate: "ProxyAuthenticate",
forward.ProxyAuthorization: "ProxyAuthorization",
},
checkForUnexpectedHeaders: true,
},
{
desc: "filter forward request headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
},
authRequestHeaders: []string{
"X-CustomHeader",
},
expectedHeaders: map[string]string{
"x-customHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
},
checkForUnexpectedHeaders: true,
},
{
desc: "filter forward request headers doesn't add new headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
},
authRequestHeaders: []string{
"X-CustomHeader",
"X-Non-Exists-Header",
},
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
},
checkForUnexpectedHeaders: true,
},
{
desc: "X-Forwarded-Prefix is kept for non-breaking behavior",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Prefix": "foo.bar",
},
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
"X-Forwarded-Prefix": "foo.bar",
},
checkForUnexpectedHeaders: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
hdl, err := NewForward(t.Context(), nil, dynamic.ForwardAuth{AuthRequestHeaders: test.authRequestHeaders}, "test")
require.NoError(t, err)
fwdAuth, ok := hdl.(*forwardAuth)
require.True(t, ok)
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
for key, value := range test.headers {
req.Header.Set(key, value)
}
if test.emptyHost {
req.Host = ""
}
forwardReq := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
oldWriteHeader(req, forwardReq, fwdAuth.authRequestHeaders)
expectedHeaders := test.expectedHeaders
for key, value := range expectedHeaders {
assert.Equal(t, value, forwardReq.Header.Get(key))
forwardReq.Header.Del(key)
}
if test.checkForUnexpectedHeaders {
for key := range forwardReq.Header {
assert.Fail(t, "Unexpected header found", key)
}
}
@ -936,9 +1162,9 @@ func TestForwardAuthPreserveRequestMethod(t *testing.T) {
}
}
func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
func TestForwardAuthMaxResponseBodySize(t *testing.T) {
testCases := []struct {
name string
desc string
maxResponseBodySize int64
status int
body string
@ -946,7 +1172,7 @@ func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
expectedBody string
}{
{
name: "auth failure, unlimited response body",
desc: "auth failure, unlimited response body",
maxResponseBodySize: -1,
status: http.StatusForbidden,
body: "Forbidden",
@ -954,7 +1180,7 @@ func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
expectedBody: "Forbidden",
},
{
name: "auth failure, response body exceeds the limit",
desc: "auth failure, response body exceeds the limit",
maxResponseBodySize: 1,
status: http.StatusForbidden,
body: "Forbidden",
@ -962,7 +1188,7 @@ func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
expectedBody: "",
},
{
name: "auth success within limit",
desc: "auth success within limit",
maxResponseBodySize: 100,
status: http.StatusOK,
body: "ok",
@ -970,7 +1196,7 @@ func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
expectedBody: "traefik\n",
},
{
name: "auth success body exceeds limit",
desc: "auth success body exceeds limit",
maxResponseBodySize: 1,
status: http.StatusOK,
body: "large auth response",
@ -980,7 +1206,7 @@ func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.status)
fmt.Fprint(w, test.body)

View File

@ -14,14 +14,14 @@ import (
)
const (
xForwardedProto = "X-Forwarded-Proto"
xForwardedFor = "X-Forwarded-For"
xForwardedHost = "X-Forwarded-Host"
xForwardedPort = "X-Forwarded-Port"
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
xForwardedServer = "X-Forwarded-Server"
xForwardedURI = "X-Forwarded-Uri"
xForwardedMethod = "X-Forwarded-Method"
xForwardedPrefix = "X-Forwarded-Prefix"
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"
xRealIP = "X-Real-Ip"
@ -29,18 +29,40 @@ const (
upgrade = "Upgrade"
)
var xHeaders = []string{
xForwardedProto,
xForwardedFor,
xForwardedHost,
xForwardedPort,
xForwardedServer,
xForwardedURI,
xForwardedMethod,
xForwardedPrefix,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xRealIP,
// XHeadersSet contains the canonical X-headers managed by Traefik. Used by
// isManagedXHeader to detect both the canonical form and underscore variants
// that Go's HTTP server preserves (e.g. X_Forwarded_Proto).
var XHeadersSet = map[string]struct{}{
XForwardedProto: {},
XForwardedFor: {},
XForwardedHost: {},
XForwardedPort: {},
xForwardedServer: {},
XForwardedURI: {},
XForwardedMethod: {},
XForwardedPrefix: {},
xForwardedTLSClientCert: {},
xForwardedTLSClientCertInfo: {},
xRealIP: {},
}
// isManagedXHeader reports whether key matches one of Traefik's X-headers,
// treating '_' as '-'. Every managed header starts with 'X', so a byte check
// skips most headers without any map work; the underscore branch is only
// reached for the rare attacker-injected variants.
func isManagedXHeader(key string) bool {
if len(key) == 0 || key[0] != 'X' {
return false
}
if _, ok := XHeadersSet[key]; ok {
return true
}
if strings.IndexByte(key, '_') < 0 {
return false
}
canonical := http.CanonicalHeaderKey(strings.ReplaceAll(key, "_", "-"))
_, ok := XHeadersSet[canonical]
return ok
}
// XForwarded is an HTTP handler wrapper that sets the X-Forwarded headers,
@ -89,6 +111,132 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
}, nil
}
// ServeHTTP implements http.Handler.
func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !x.insecure && !x.isTrustedIP(r.RemoteAddr) {
DeleteXForwardedHeaders(r.Header)
}
x.rewrite(r)
x.removeConnectionHeaders(r)
if x.notAppendXForwardedFor {
r = r.WithContext(httputil.SetNotAppendXFF(r.Context()))
}
x.next.ServeHTTP(w, r)
}
func (x *XForwarded) isTrustedIP(ip string) bool {
if x.ipChecker == nil {
return false
}
return x.ipChecker.IsAuthorized(ip) == nil
}
func (x *XForwarded) rewrite(outreq *http.Request) {
if clientIP, _, err := net.SplitHostPort(outreq.RemoteAddr); err == nil {
clientIP = removeIPv6Zone(clientIP)
if unsafeHeader(outreq.Header).Get(xRealIP) == "" {
unsafeHeader(outreq.Header).Set(xRealIP, clientIP)
}
}
xfProto := unsafeHeader(outreq.Header).Get(XForwardedProto)
if xfProto == "" {
// TODO: is this expected to set the X-Forwarded-Proto header value to
// ws(s) as the underlying request used to upgrade the connection is
// made over HTTP(S)?
if isWebsocketRequest(outreq) {
if outreq.TLS != nil {
unsafeHeader(outreq.Header).Set(XForwardedProto, "wss")
} else {
unsafeHeader(outreq.Header).Set(XForwardedProto, "ws")
}
} else {
if outreq.TLS != nil {
unsafeHeader(outreq.Header).Set(XForwardedProto, "https")
} else {
unsafeHeader(outreq.Header).Set(XForwardedProto, "http")
}
}
}
if xfPort := unsafeHeader(outreq.Header).Get(XForwardedPort); xfPort == "" {
unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq))
}
if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" {
unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host)
}
// Per https://www.rfc-editor.org/rfc/rfc2616#section-4.2, the Forwarded IPs list is in
// the same order as the values in the X-Forwarded-For header(s).
if xffs := unsafeHeader(outreq.Header).Values(XForwardedFor); len(xffs) > 0 {
unsafeHeader(outreq.Header).Set(XForwardedFor, strings.Join(xffs, ", "))
}
if x.hostname != "" {
unsafeHeader(outreq.Header).Set(xForwardedServer, x.hostname)
}
}
func (x *XForwarded) removeConnectionHeaders(req *http.Request) {
var reqUpType string
if httpguts.HeaderValuesContainsToken(req.Header[connection], upgrade) {
reqUpType = unsafeHeader(req.Header).Get(upgrade)
}
var connectionHopByHopHeaders []string
for _, f := range req.Header[connection] {
for sf := range strings.SplitSeq(f, ",") {
if sf = textproto.TrimString(sf); sf != "" {
key := http.CanonicalHeaderKey(sf)
// Connection header cannot dictate to remove X- headers managed by Traefik,
// as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1,
// A proxy or gateway MUST ... and then remove the Connection header field itself
// (or replace it with the intermediary's own connection options for the forwarded message).
if isManagedXHeader(key) {
continue
}
// Keep headers allowed through the middleware chain.
if slices.Contains(x.connectionHeaders, key) {
connectionHopByHopHeaders = append(connectionHopByHopHeaders, key)
continue
}
// Apply Connection header option.
delete(req.Header, key)
}
}
}
if reqUpType != "" {
connectionHopByHopHeaders = append(connectionHopByHopHeaders, upgrade)
unsafeHeader(req.Header).Set(upgrade, reqUpType)
}
if len(connectionHopByHopHeaders) > 0 {
unsafeHeader(req.Header).Set(connection, strings.Join(connectionHopByHopHeaders, ","))
return
}
unsafeHeader(req.Header).Del(connection)
}
// DeleteXForwardedHeaders Strip X-Forwarded headers and their underscore variants
// (e.g. X_Forwarded_Proto), which Go's HTTP server preserves
// alongside the canonical dash form.
func DeleteXForwardedHeaders(headers http.Header) {
for key := range headers {
if isManagedXHeader(key) {
delete(headers, key)
}
}
}
// removeIPv6Zone removes the zone if the given IP is an ipv6 address and it has {zone} information in it,
// like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692".
func removeIPv6Zone(clientIP string) string {
@ -126,7 +274,7 @@ func forwardedPort(req *http.Request) string {
return port
}
if unsafeHeader(req.Header).Get(xForwardedProto) == "https" || unsafeHeader(req.Header).Get(xForwardedProto) == "wss" {
if unsafeHeader(req.Header).Get(XForwardedProto) == "https" || unsafeHeader(req.Header).Get(XForwardedProto) == "wss" {
return "443"
}
@ -137,123 +285,6 @@ func forwardedPort(req *http.Request) string {
return "80"
}
// ServeHTTP implements http.Handler.
func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !x.insecure && !x.isTrustedIP(r.RemoteAddr) {
for _, h := range xHeaders {
unsafeHeader(r.Header).Del(h)
}
}
x.rewrite(r)
x.removeConnectionHeaders(r)
if x.notAppendXForwardedFor {
r = r.WithContext(httputil.SetNotAppendXFF(r.Context()))
}
x.next.ServeHTTP(w, r)
}
func (x *XForwarded) isTrustedIP(ip string) bool {
if x.ipChecker == nil {
return false
}
return x.ipChecker.IsAuthorized(ip) == nil
}
func (x *XForwarded) rewrite(outreq *http.Request) {
if clientIP, _, err := net.SplitHostPort(outreq.RemoteAddr); err == nil {
clientIP = removeIPv6Zone(clientIP)
if unsafeHeader(outreq.Header).Get(xRealIP) == "" {
unsafeHeader(outreq.Header).Set(xRealIP, clientIP)
}
}
xfProto := unsafeHeader(outreq.Header).Get(xForwardedProto)
if xfProto == "" {
// TODO: is this expected to set the X-Forwarded-Proto header value to
// ws(s) as the underlying request used to upgrade the connection is
// made over HTTP(S)?
if isWebsocketRequest(outreq) {
if outreq.TLS != nil {
unsafeHeader(outreq.Header).Set(xForwardedProto, "wss")
} else {
unsafeHeader(outreq.Header).Set(xForwardedProto, "ws")
}
} else {
if outreq.TLS != nil {
unsafeHeader(outreq.Header).Set(xForwardedProto, "https")
} else {
unsafeHeader(outreq.Header).Set(xForwardedProto, "http")
}
}
}
if xfPort := unsafeHeader(outreq.Header).Get(xForwardedPort); xfPort == "" {
unsafeHeader(outreq.Header).Set(xForwardedPort, forwardedPort(outreq))
}
if xfHost := unsafeHeader(outreq.Header).Get(xForwardedHost); xfHost == "" && outreq.Host != "" {
unsafeHeader(outreq.Header).Set(xForwardedHost, outreq.Host)
}
// Per https://www.rfc-editor.org/rfc/rfc2616#section-4.2, the Forwarded IPs list is in
// the same order as the values in the X-Forwarded-For header(s).
if xffs := unsafeHeader(outreq.Header).Values(xForwardedFor); len(xffs) > 0 {
unsafeHeader(outreq.Header).Set(xForwardedFor, strings.Join(xffs, ", "))
}
if x.hostname != "" {
unsafeHeader(outreq.Header).Set(xForwardedServer, x.hostname)
}
}
func (x *XForwarded) removeConnectionHeaders(req *http.Request) {
var reqUpType string
if httpguts.HeaderValuesContainsToken(req.Header[connection], upgrade) {
reqUpType = unsafeHeader(req.Header).Get(upgrade)
}
var connectionHopByHopHeaders []string
for _, f := range req.Header[connection] {
for sf := range strings.SplitSeq(f, ",") {
if sf = textproto.TrimString(sf); sf != "" {
key := http.CanonicalHeaderKey(sf)
// Connection header cannot dictate to remove X- headers managed by Traefik,
// as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1,
// A proxy or gateway MUST ... and then remove the Connection header field itself
// (or replace it with the intermediary's own connection options for the forwarded message).
if slices.Contains(xHeaders, key) {
continue
}
// Keep headers allowed through the middleware chain.
if slices.Contains(x.connectionHeaders, key) {
connectionHopByHopHeaders = append(connectionHopByHopHeaders, key)
continue
}
// Apply Connection header option.
delete(req.Header, key)
}
}
}
if reqUpType != "" {
connectionHopByHopHeaders = append(connectionHopByHopHeaders, upgrade)
unsafeHeader(req.Header).Set(upgrade, reqUpType)
}
if len(connectionHopByHopHeaders) > 0 {
unsafeHeader(req.Header).Set(connection, strings.Join(connectionHopByHopHeaders, ","))
return
}
unsafeHeader(req.Header).Del(connection)
}
// unsafeHeader allows to manage Header values.
// Must be used only when the header name is already a canonical key.
type unsafeHeader map[string][]string

View File

@ -31,9 +31,9 @@ func TestServeHTTP(t *testing.T) {
remoteAddr: "",
incomingHeaders: map[string][]string{},
expectedHeaders: map[string]string{
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
},
@ -44,20 +44,24 @@ func TestServeHTTP(t *testing.T) {
trustedIps: nil,
remoteAddr: "",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
"X_forwarded_proto": {"https"},
"X_forwarded_for": {"10.0.0.1"},
},
expectedHeaders: map[string]string{
xForwardedFor: "10.0.1.0, 10.0.1.12",
xForwardedURI: "/bar",
xForwardedMethod: "GET",
XForwardedFor: "10.0.1.0, 10.0.1.12",
XForwardedURI: "/bar",
XForwardedMethod: "GET",
xForwardedTLSClientCert: "Cert",
xForwardedTLSClientCertInfo: "CertInfo",
xForwardedPrefix: "/prefix",
XForwardedPrefix: "/prefix",
"X_forwarded_proto": "https",
"X_forwarded_for": "10.0.0.1",
},
},
{
@ -66,20 +70,26 @@ func TestServeHTTP(t *testing.T) {
trustedIps: nil,
remoteAddr: "",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
"X_forwarded_proto": {"https"},
"X_forwarded_for": {"10.0.0.1"},
"X_forwarded_host": {"evil.example"},
},
expectedHeaders: map[string]string{
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
xForwardedPrefix: "",
XForwardedPrefix: "",
"X_forwarded_proto": "",
"X_forwarded_for": "",
"X_forwarded_host": "",
},
},
{
@ -88,20 +98,24 @@ func TestServeHTTP(t *testing.T) {
trustedIps: []string{"10.0.1.100"},
remoteAddr: "10.0.1.100:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
"X_forwarded_proto": {"https"},
"X_forwarded_for": {"10.0.0.1"},
},
expectedHeaders: map[string]string{
xForwardedFor: "10.0.1.0, 10.0.1.12",
xForwardedURI: "/bar",
xForwardedMethod: "GET",
XForwardedFor: "10.0.1.0, 10.0.1.12",
XForwardedURI: "/bar",
XForwardedMethod: "GET",
xForwardedTLSClientCert: "Cert",
xForwardedTLSClientCertInfo: "CertInfo",
xForwardedPrefix: "/prefix",
XForwardedPrefix: "/prefix",
"X_forwarded_proto": "https",
"X_forwarded_for": "10.0.0.1",
},
},
{
@ -110,20 +124,20 @@ func TestServeHTTP(t *testing.T) {
trustedIps: []string{"10.0.1.100"},
remoteAddr: "10.0.1.101:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
},
expectedHeaders: map[string]string{
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
xForwardedPrefix: "",
XForwardedPrefix: "",
},
},
{
@ -132,20 +146,20 @@ func TestServeHTTP(t *testing.T) {
trustedIps: []string{"1.2.3.4/24"},
remoteAddr: "1.2.3.156:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
},
expectedHeaders: map[string]string{
xForwardedFor: "10.0.1.0, 10.0.1.12",
xForwardedURI: "/bar",
xForwardedMethod: "GET",
XForwardedFor: "10.0.1.0, 10.0.1.12",
XForwardedURI: "/bar",
XForwardedMethod: "GET",
xForwardedTLSClientCert: "Cert",
xForwardedTLSClientCertInfo: "CertInfo",
xForwardedPrefix: "/prefix",
XForwardedPrefix: "/prefix",
},
},
{
@ -154,34 +168,34 @@ func TestServeHTTP(t *testing.T) {
trustedIps: []string{"1.2.3.4/24"},
remoteAddr: "10.0.1.101:80",
incomingHeaders: map[string][]string{
xForwardedFor: {"10.0.1.0, 10.0.1.12"},
xForwardedURI: {"/bar"},
xForwardedMethod: {"GET"},
XForwardedFor: {"10.0.1.0, 10.0.1.12"},
XForwardedURI: {"/bar"},
XForwardedMethod: {"GET"},
xForwardedTLSClientCert: {"Cert"},
xForwardedTLSClientCertInfo: {"CertInfo"},
xForwardedPrefix: {"/prefix"},
XForwardedPrefix: {"/prefix"},
},
expectedHeaders: map[string]string{
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
xForwardedPrefix: "",
XForwardedPrefix: "",
},
},
{
desc: "xForwardedFor with multiple header(s) values",
insecure: true,
incomingHeaders: map[string][]string{
xForwardedFor: {
XForwardedFor: {
"10.0.0.4, 10.0.0.3",
"10.0.0.2, 10.0.0.1",
"10.0.0.0",
},
},
expectedHeaders: map[string]string{
xForwardedFor: "10.0.0.4, 10.0.0.3, 10.0.0.2, 10.0.0.1, 10.0.0.0",
XForwardedFor: "10.0.0.4, 10.0.0.3, 10.0.0.2, 10.0.0.1, 10.0.0.0",
},
},
{
@ -206,14 +220,14 @@ func TestServeHTTP(t *testing.T) {
desc: "xForwardedProto with no tls",
tls: false,
expectedHeaders: map[string]string{
xForwardedProto: "http",
XForwardedProto: "http",
},
},
{
desc: "xForwardedProto with tls",
tls: true,
expectedHeaders: map[string]string{
xForwardedProto: "https",
XForwardedProto: "https",
},
},
{
@ -221,7 +235,7 @@ func TestServeHTTP(t *testing.T) {
tls: false,
websocket: true,
expectedHeaders: map[string]string{
xForwardedProto: "ws",
XForwardedProto: "ws",
},
},
{
@ -229,7 +243,7 @@ func TestServeHTTP(t *testing.T) {
tls: true,
websocket: true,
expectedHeaders: map[string]string{
xForwardedProto: "wss",
XForwardedProto: "wss",
},
},
{
@ -237,17 +251,17 @@ func TestServeHTTP(t *testing.T) {
tls: true,
websocket: true,
incomingHeaders: map[string][]string{
xForwardedProto: {"wss"},
XForwardedProto: {"wss"},
},
expectedHeaders: map[string]string{
xForwardedProto: "wss",
XForwardedProto: "wss",
},
},
{
desc: "xForwardedPort with explicit port",
host: "foo.com:8080",
expectedHeaders: map[string]string{
xForwardedPort: "8080",
XForwardedPort: "8080",
},
},
{
@ -255,25 +269,25 @@ func TestServeHTTP(t *testing.T) {
// setting insecure just so our initial xForwardedProto does not get cleaned
insecure: true,
incomingHeaders: map[string][]string{
xForwardedProto: {"https"},
XForwardedProto: {"https"},
},
expectedHeaders: map[string]string{
xForwardedProto: "https",
xForwardedPort: "443",
XForwardedProto: "https",
XForwardedPort: "443",
},
},
{
desc: "xForwardedPort with implicit tls port from TLS in req",
tls: true,
expectedHeaders: map[string]string{
xForwardedPort: "443",
XForwardedPort: "443",
},
},
{
desc: "xForwardedHost from req host",
host: "foo.com:8080",
expectedHeaders: map[string]string{
xForwardedHost: "foo.com:8080",
XForwardedHost: "foo.com:8080",
},
},
{
@ -288,39 +302,42 @@ func TestServeHTTP(t *testing.T) {
insecure: false,
incomingHeaders: map[string][]string{
connection: {
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
"X_forwarded_proto",
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
"X_forwarded_proto": {"spoofed"},
},
expectedHeaders: map[string]string{
xForwardedProto: "http",
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
xForwardedHost: "",
xForwardedPort: "80",
XForwardedProto: "http",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
XForwardedHost: "",
XForwardedPort: "80",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
xForwardedPrefix: "",
XForwardedPrefix: "",
xRealIP: "",
"X_forwarded_proto": "",
connection: "",
},
},
@ -329,38 +346,38 @@ func TestServeHTTP(t *testing.T) {
insecure: true,
incomingHeaders: map[string][]string{
connection: {
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
},
expectedHeaders: map[string]string{
xForwardedProto: "foo",
xForwardedFor: "foo",
xForwardedURI: "foo",
xForwardedMethod: "foo",
xForwardedHost: "foo",
xForwardedPort: "foo",
XForwardedProto: "foo",
XForwardedFor: "foo",
XForwardedURI: "foo",
XForwardedMethod: "foo",
XForwardedHost: "foo",
XForwardedPort: "foo",
xForwardedTLSClientCert: "foo",
xForwardedTLSClientCertInfo: "foo",
xForwardedPrefix: "foo",
XForwardedPrefix: "foo",
xRealIP: "foo",
connection: "",
},
@ -369,51 +386,51 @@ func TestServeHTTP(t *testing.T) {
desc: "Untrusted and Connection: Connection header has no effect on X- forwarded headers",
insecure: false,
connectionHeaders: []string{
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
incomingHeaders: map[string][]string{
connection: {
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
},
expectedHeaders: map[string]string{
xForwardedProto: "http",
xForwardedFor: "",
xForwardedURI: "",
xForwardedMethod: "",
xForwardedHost: "",
xForwardedPort: "80",
XForwardedProto: "http",
XForwardedFor: "",
XForwardedURI: "",
XForwardedMethod: "",
XForwardedHost: "",
XForwardedPort: "80",
xForwardedTLSClientCert: "",
xForwardedTLSClientCertInfo: "",
xForwardedPrefix: "",
XForwardedPrefix: "",
xRealIP: "",
connection: "",
},
@ -422,51 +439,51 @@ func TestServeHTTP(t *testing.T) {
desc: "Trusted (insecure) and Connection: Connection header has no effect on X- forwarded headers",
insecure: true,
connectionHeaders: []string{
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
incomingHeaders: map[string][]string{
connection: {
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
},
expectedHeaders: map[string]string{
xForwardedProto: "foo",
xForwardedFor: "foo",
xForwardedURI: "foo",
xForwardedMethod: "foo",
xForwardedHost: "foo",
xForwardedPort: "foo",
XForwardedProto: "foo",
XForwardedFor: "foo",
XForwardedURI: "foo",
XForwardedMethod: "foo",
XForwardedHost: "foo",
XForwardedPort: "foo",
xForwardedTLSClientCert: "foo",
xForwardedTLSClientCertInfo: "foo",
xForwardedPrefix: "foo",
XForwardedPrefix: "foo",
xRealIP: "foo",
connection: "",
},
@ -475,51 +492,51 @@ func TestServeHTTP(t *testing.T) {
desc: "Trusted (insecure) and Connection: Testing case sensitivity on connection Headers param",
insecure: true,
connectionHeaders: []string{
strings.ToLower(xForwardedProto),
strings.ToLower(xForwardedFor),
strings.ToLower(xForwardedURI),
strings.ToLower(xForwardedMethod),
strings.ToLower(xForwardedHost),
strings.ToLower(xForwardedPort),
strings.ToLower(XForwardedProto),
strings.ToLower(XForwardedFor),
strings.ToLower(XForwardedURI),
strings.ToLower(XForwardedMethod),
strings.ToLower(XForwardedHost),
strings.ToLower(XForwardedPort),
strings.ToLower(xForwardedTLSClientCert),
strings.ToLower(xForwardedTLSClientCertInfo),
strings.ToLower(xForwardedPrefix),
strings.ToLower(XForwardedPrefix),
strings.ToLower(xRealIP),
},
incomingHeaders: map[string][]string{
connection: {
xForwardedProto,
xForwardedFor,
xForwardedURI,
xForwardedMethod,
xForwardedHost,
xForwardedPort,
XForwardedProto,
XForwardedFor,
XForwardedURI,
XForwardedMethod,
XForwardedHost,
XForwardedPort,
xForwardedTLSClientCert,
xForwardedTLSClientCertInfo,
xForwardedPrefix,
XForwardedPrefix,
xRealIP,
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
},
expectedHeaders: map[string]string{
xForwardedProto: "foo",
xForwardedFor: "foo",
xForwardedURI: "foo",
xForwardedMethod: "foo",
xForwardedHost: "foo",
xForwardedPort: "foo",
XForwardedProto: "foo",
XForwardedFor: "foo",
XForwardedURI: "foo",
XForwardedMethod: "foo",
XForwardedHost: "foo",
XForwardedPort: "foo",
xForwardedTLSClientCert: "foo",
xForwardedTLSClientCertInfo: "foo",
xForwardedPrefix: "foo",
XForwardedPrefix: "foo",
xRealIP: "foo",
connection: "",
},
@ -529,38 +546,38 @@ func TestServeHTTP(t *testing.T) {
insecure: true,
incomingHeaders: map[string][]string{
connection: {
strings.ToLower(xForwardedProto),
strings.ToLower(xForwardedFor),
strings.ToLower(xForwardedURI),
strings.ToLower(xForwardedMethod),
strings.ToLower(xForwardedHost),
strings.ToLower(xForwardedPort),
strings.ToLower(XForwardedProto),
strings.ToLower(XForwardedFor),
strings.ToLower(XForwardedURI),
strings.ToLower(XForwardedMethod),
strings.ToLower(XForwardedHost),
strings.ToLower(XForwardedPort),
strings.ToLower(xForwardedTLSClientCert),
strings.ToLower(xForwardedTLSClientCertInfo),
strings.ToLower(xForwardedPrefix),
strings.ToLower(XForwardedPrefix),
strings.ToLower(xRealIP),
},
xForwardedProto: {"foo"},
xForwardedFor: {"foo"},
xForwardedURI: {"foo"},
xForwardedMethod: {"foo"},
xForwardedHost: {"foo"},
xForwardedPort: {"foo"},
XForwardedProto: {"foo"},
XForwardedFor: {"foo"},
XForwardedURI: {"foo"},
XForwardedMethod: {"foo"},
XForwardedHost: {"foo"},
XForwardedPort: {"foo"},
xForwardedTLSClientCert: {"foo"},
xForwardedTLSClientCertInfo: {"foo"},
xForwardedPrefix: {"foo"},
XForwardedPrefix: {"foo"},
xRealIP: {"foo"},
},
expectedHeaders: map[string]string{
xForwardedProto: "foo",
xForwardedFor: "foo",
xForwardedURI: "foo",
xForwardedMethod: "foo",
xForwardedHost: "foo",
xForwardedPort: "foo",
XForwardedProto: "foo",
XForwardedFor: "foo",
XForwardedURI: "foo",
XForwardedMethod: "foo",
XForwardedHost: "foo",
XForwardedPort: "foo",
xForwardedTLSClientCert: "foo",
xForwardedTLSClientCertInfo: "foo",
xForwardedPrefix: "foo",
XForwardedPrefix: "foo",
xRealIP: "foo",
connection: "",
},
@ -583,6 +600,19 @@ func TestServeHTTP(t *testing.T) {
"Foo": "bar",
},
},
{
desc: "insecure false preserves non-matching underscore headers",
insecure: false,
remoteAddr: "10.0.1.101:80",
incomingHeaders: map[string][]string{
"X_custom_header": {"value"},
"X_forwarded_proto": {"spoofed"},
},
expectedHeaders: map[string]string{
"X_custom_header": "value",
"X_forwarded_proto": "",
},
},
}
for _, test := range testCases {

View File

@ -36,8 +36,13 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, nam
// Handle default value (here because of deprecation and the removal of setDefault).
forceSlash := config.ForceSlash != nil && *config.ForceSlash
prefixes := make([]string, len(config.Prefixes))
for i, p := range config.Prefixes {
prefixes[i] = strings.TrimSpace(p)
}
return &stripPrefix{
prefixes: config.Prefixes,
prefixes: prefixes,
next: next,
name: name,
forceSlash: forceSlash,
@ -55,19 +60,22 @@ func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.URL.RawPath != "" {
req.URL.RawPath = s.getRawPathStripped(req.URL.RawPath, prefix)
}
s.serveRequest(rw, req, strings.TrimSpace(prefix))
return
// Here we are sanitizing the URL when the path is not empty,
// as the JoinPath method is adding a leading slash if the path is empty
// to be aligned with ensureLeadingSlash behavior.
if req.URL.Path != "" {
req.URL = req.URL.JoinPath()
}
req.Header.Add(ForwardedPrefixHeader, prefix)
req.RequestURI = req.URL.RequestURI()
break
}
}
s.next.ServeHTTP(rw, req)
}
func (s *stripPrefix) serveRequest(rw http.ResponseWriter, req *http.Request, prefix string) {
req.Header.Add(ForwardedPrefixHeader, prefix)
req.RequestURI = req.URL.RequestURI()
s.next.ServeHTTP(rw, req)
}
func (s *stripPrefix) getPathStripped(urlPath, prefix string) string {
if s.forceSlash {
// Only for compatibility reason with the previous behavior,

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"k8s.io/utils/ptr"
)
func TestStripPrefix(t *testing.T) {
@ -141,6 +142,40 @@ func TestStripPrefix(t *testing.T) {
expectedRawPath: "/a%2Fb",
expectedHeader: "/api/",
},
{
desc: "dot in the path not stripped by the prefix",
config: dynamic.StripPrefix{
Prefixes: []string{"/api"},
},
path: "/api./foo",
expectedStatusCode: http.StatusOK,
expectedPath: "/foo",
expectedRawPath: "",
expectedHeader: "/api",
},
{
desc: "multiple dots in the path not stripped by the prefix",
config: dynamic.StripPrefix{
Prefixes: []string{"/api"},
},
path: "/api../foo",
expectedStatusCode: http.StatusOK,
expectedPath: "/foo",
expectedRawPath: "",
expectedHeader: "/api",
},
{
desc: "multiple dots in the path not stripped by the prefix with forceSlash",
config: dynamic.StripPrefix{
Prefixes: []string{"/api"},
ForceSlash: ptr.To(true),
},
path: "/api../foo",
expectedStatusCode: http.StatusOK,
expectedPath: "/foo",
expectedRawPath: "",
expectedHeader: "/api",
},
}
for _, test := range testCases {

View File

@ -62,9 +62,15 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request)
req.URL.RawPath = ensureLeadingSlash(req.URL.RawPath[encodedPrefixLen(req.URL.RawPath, prefix):])
}
// Here we are sanitizing the URL when the path is not empty,
// as the JoinPath method is adding a leading slash if the path is empty
// to be aligned with ensureLeadingSlash behavior.
if req.URL.Path != "" {
req.URL = req.URL.JoinPath()
}
req.RequestURI = req.URL.RequestURI()
s.next.ServeHTTP(rw, req)
return
break
}
}

View File

@ -153,7 +153,7 @@ func TestStripPrefixRegex(t *testing.T) {
path: "/b/ap%69/test",
expectedStatusCode: http.StatusOK,
expectedPath: "/test",
expectedRawPath: "/test",
expectedRawPath: "",
expectedRequestURI: "/test",
expectedHeader: "/b/api/",
},
@ -197,6 +197,26 @@ func TestStripPrefixRegex(t *testing.T) {
expectedRequestURI: "/a%2Fb",
expectedHeader: "/t /test",
},
{
desc: "/api./foo",
config: dynamic.StripPrefixRegex{Regex: []string{"/api"}},
path: "/api./foo",
expectedStatusCode: http.StatusOK,
expectedPath: "/foo",
expectedRawPath: "",
expectedRequestURI: "/foo",
expectedHeader: "/api",
},
{
desc: "/api../foo",
config: dynamic.StripPrefixRegex{Regex: []string{"/api"}},
path: "/api../foo",
expectedStatusCode: http.StatusOK,
expectedPath: "/foo",
expectedRawPath: "",
expectedRequestURI: "/foo",
expectedHeader: "/api",
},
}
for _, test := range testCases {

View File

@ -121,7 +121,7 @@ func (c *ChallengeHTTP) getTokenValue(ctx context.Context, token, domain string)
}
func getPathParam(uri *url.URL) (string, error) {
exp := regexp.MustCompile(fmt.Sprintf(`^%s([^/]+)/?$`, http01.ChallengePath("")))
exp := regexp.MustCompile(fmt.Sprintf(`^%s([^/]+)/?$`, http01.PathPrefix))
parts := exp.FindStringSubmatch(uri.Path)
if len(parts) != 2 {

View File

@ -8,6 +8,7 @@ import (
"text/template"
"time"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/connhelper"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
@ -39,7 +40,11 @@ type Shared struct {
func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData {
containerInspected, err := dockerClient.ContainerInspect(ctx, containerID)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID)
if errdefs.IsNotFound(err) {
log.Ctx(ctx).Debug().Err(err).Msgf("Failed to inspect container %s", containerID)
} else {
log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID)
}
return dockerData{}
}

View File

@ -37,6 +37,16 @@ spec:
port: 80
middlewares:
- name: cross-ns-stripprefix@kubernetescrd
- match: Host(`foo.com`) && PathPrefix(`/chain`)
kind: Rule
priority: 12
services:
- name: whoami
namespace: default
port: 80
middlewares:
- name: test-chain
- name: test-chain-cross-provider
---
apiVersion: traefik.io/v1alpha1
@ -50,6 +60,31 @@ spec:
prefixes:
- /stripit
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-chain
namespace: default
spec:
chain:
middlewares:
- name: stripprefix
namespace: cross-ns
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-chain-cross-provider
namespace: default
spec:
chain:
middlewares:
- name: other-middleware@kubernetescrd
---
apiVersion: traefik.io/v1alpha1
kind: Middleware

View File

@ -36,6 +36,8 @@ type ForwardAuthApplyConfiguration struct {
// Address defines the authentication server address.
Address *string `json:"address,omitempty"`
// TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
//
// Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
TrustForwardHeader *bool `json:"trustForwardHeader,omitempty"`
// AuthResponseHeaders defines the list of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers.
AuthResponseHeaders []string `json:"authResponseHeaders,omitempty"`

View File

@ -303,13 +303,19 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
continue
}
chain, err := createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain, p.AllowCrossNamespace)
if err != nil {
logger.Error().Err(err).Msg("Error while reading chain middleware")
continue
}
conf.HTTP.Middlewares[id] = &dynamic.Middleware{
AddPrefix: middleware.Spec.AddPrefix,
StripPrefix: middleware.Spec.StripPrefix,
StripPrefixRegex: middleware.Spec.StripPrefixRegex,
ReplacePath: middleware.Spec.ReplacePath,
ReplacePathRegex: middleware.Spec.ReplacePathRegex,
Chain: createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain),
Chain: chain,
IPWhiteList: middleware.Spec.IPWhiteList,
IPAllowList: middleware.Spec.IPAllowList,
Headers: middleware.Spec.Headers,
@ -1272,13 +1278,20 @@ func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
return credentials, nil
}
func createChainMiddleware(ctx context.Context, namespace string, chain *traefikv1alpha1.Chain) *dynamic.Chain {
func createChainMiddleware(ctx context.Context, parentNamespace string, chain *traefikv1alpha1.Chain, allowCrossNamespace bool) (*dynamic.Chain, error) {
if chain == nil {
return nil
return nil, nil
}
var mds []string
for _, mi := range chain.Middlewares {
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross-namespace references.
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
}
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.Ctx(ctx).Warn().Msgf("namespace %q is ignored in cross-provider context", mi.Namespace)
@ -1287,13 +1300,19 @@ func createChainMiddleware(ctx context.Context, namespace string, chain *traefik
continue
}
ns := mi.Namespace
if len(ns) == 0 {
ns = namespace
ns := parentNamespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(allowCrossNamespace, parentNamespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the chain namespace %s", mi.Namespace, mi.Name, parentNamespace)
}
ns = mi.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
}
return &dynamic.Chain{Middlewares: mds}
return &dynamic.Chain{Middlewares: mds}, nil
}
func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options {

View File

@ -189,8 +189,8 @@ func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []tra
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
return nil, fmt.Errorf("invalid reference to middleware %s: with crossnamespace disallowed, the namespace field needs to be explicitly specified", mi.Name)
// we don't allow this format to avoid cross-namespace references.
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
}
if strings.Contains(name, providerNamespaceSeparator) {

View File

@ -7405,6 +7405,16 @@ func TestCrossNamespace(t *testing.T) {
Priority: 12,
Middlewares: []string{"default-test-errorpage"},
},
"default-test-crossnamespace-route-4932ffbbcd99474df323": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-4932ffbbcd99474df323",
Rule: "Host(`foo.com`) && PathPrefix(`/chain`)",
Priority: 12,
Middlewares: []string{
"default-test-chain",
"default-test-chain-cross-provider",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
@ -7431,6 +7441,23 @@ func TestCrossNamespace(t *testing.T) {
},
},
},
"default-test-crossnamespace-route-4932ffbbcd99474df323": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: pointer(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
@ -7477,6 +7504,16 @@ func TestCrossNamespace(t *testing.T) {
Priority: 12,
Middlewares: []string{"cross-ns-stripprefix@kubernetescrd"},
},
"default-test-crossnamespace-route-4932ffbbcd99474df323": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-4932ffbbcd99474df323",
Rule: "Host(`foo.com`) && PathPrefix(`/chain`)",
Priority: 12,
Middlewares: []string{
"default-test-chain",
"default-test-chain-cross-provider",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
@ -7491,6 +7528,16 @@ func TestCrossNamespace(t *testing.T) {
Query: "/{status}.html",
},
},
"default-test-chain": {
Chain: &dynamic.Chain{
Middlewares: []string{"cross-ns-stripprefix"},
},
},
"default-test-chain-cross-provider": {
Chain: &dynamic.Chain{
Middlewares: []string{"other-middleware@kubernetescrd"},
},
},
},
Services: map[string]*dynamic.Service{
"default-test-crossnamespace-route-6b204d94623b3df4370c": {
@ -7561,6 +7608,23 @@ func TestCrossNamespace(t *testing.T) {
},
},
},
"default-test-crossnamespace-route-4932ffbbcd99474df323": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: pointer(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},

View File

@ -187,7 +187,9 @@ type ForwardAuth struct {
// Address defines the authentication server address.
Address string `json:"address,omitempty"`
// TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers.
TrustForwardHeader bool `json:"trustForwardHeader,omitempty"`
//
// Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware.
TrustForwardHeader *bool `json:"trustForwardHeader,omitempty"`
// AuthResponseHeaders defines the list of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers.
AuthResponseHeaders []string `json:"authResponseHeaders,omitempty"`
// AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.

View File

@ -331,6 +331,11 @@ func (in *FailoverError) DeepCopy() *FailoverError {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = *in
if in.TrustForwardHeader != nil {
in, out := &in.TrustForwardHeader, &out.TrustForwardHeader
*out = new(bool)
**out = **in
}
if in.AuthResponseHeaders != nil {
in, out := &in.AuthResponseHeaders, &out.AuthResponseHeaders
*out = make([]string, len(*in))

View File

@ -443,7 +443,7 @@ func Test_buildConfiguration(t *testing.T) {
InsecureSkipVerify: true,
CAOptional: pointer(true),
},
TrustForwardHeader: true,
TrustForwardHeader: pointer(true),
AuthResponseHeaders: []string{
"foobar",
"foobar",

View File

@ -275,7 +275,7 @@ func init() {
Key: "cert.pem",
InsecureSkipVerify: true,
},
TrustForwardHeader: true,
TrustForwardHeader: pointer(true),
AuthResponseHeaders: []string{"foo"},
AuthResponseHeadersRegex: "foo",
AuthRequestHeaders: []string{"foo"},

View File

@ -139,7 +139,7 @@ func TestMirroringWithBody(t *testing.T) {
const numMirrors = 10
var (
countMirror int32
countMirror atomic.Int32
body = []byte(`body`)
)
@ -161,7 +161,7 @@ func TestMirroringWithBody(t *testing.T) {
bb, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, body, bb)
atomic.AddInt32(&countMirror, 1)
countMirror.Add(1)
}), 100)
assert.NoError(t, err)
}
@ -172,7 +172,7 @@ func TestMirroringWithBody(t *testing.T) {
pool.Stop()
val := atomic.LoadInt32(&countMirror)
val := countMirror.Load()
assert.Equal(t, numMirrors, int(val))
}
@ -180,7 +180,7 @@ func TestMirroringWithIgnoredBody(t *testing.T) {
const numMirrors = 10
var (
countMirror int32
countMirror atomic.Int32
body = []byte(`body`)
emptyBody = []byte(``)
)
@ -203,7 +203,7 @@ func TestMirroringWithIgnoredBody(t *testing.T) {
bb, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, emptyBody, bb)
atomic.AddInt32(&countMirror, 1)
countMirror.Add(1)
}), 100)
assert.NoError(t, err)
}
@ -214,7 +214,7 @@ func TestMirroringWithIgnoredBody(t *testing.T) {
pool.Stop()
val := atomic.LoadInt32(&countMirror)
val := countMirror.Load()
assert.Equal(t, numMirrors, int(val))
}

View File

@ -8,6 +8,7 @@ import (
"hash/fnv"
"net/http"
"strconv"
"strings"
"sync"
"time"
@ -150,7 +151,7 @@ func (s *Sticky) WriteStickyCookie(rw http.ResponseWriter, name string) error {
}
func convertSameSite(sameSite string) http.SameSite {
switch sameSite {
switch strings.ToLower(sameSite) {
case "none":
return http.SameSiteNoneMode
case "lax":

View File

@ -140,3 +140,27 @@ func TestSticky_WriteStickyCookie(t *testing.T) {
assert.Equal(t, "/foo", cookie.Path)
assert.Equal(t, "foo.com", cookie.Domain)
}
func TestConvertSameSite_CaseInsensitive(t *testing.T) {
tests := []struct {
input string
expected http.SameSite
}{
{"none", http.SameSiteNoneMode},
{"None", http.SameSiteNoneMode},
{"NONE", http.SameSiteNoneMode},
{"lax", http.SameSiteLaxMode},
{"Lax", http.SameSiteLaxMode},
{"LAX", http.SameSiteLaxMode},
{"strict", http.SameSiteStrictMode},
{"Strict", http.SameSiteStrictMode},
{"STRICT", http.SameSiteStrictMode},
{"", http.SameSiteDefaultMode},
{"invalid", http.SameSiteDefaultMode},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.expected, convertSameSite(tt.input))
})
}
}

View File

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v3.6.13
# example new bugfix v3.6.14
CurrentRef = "v3.6"
PreviousRef = "v3.6.12"
PreviousRef = "v3.6.13"
BaseBranch = "v3.6"
FutureCurrentRefName = "v3.6.13"
FutureCurrentRefName = "v3.6.14"
ThresholdPreviousRef = 10000
ThresholdCurrentRef = 10000

View File

@ -103,6 +103,7 @@
},
"packageManager": "yarn@4.13.0",
"resolutions": {
"form-data": "4.0.4",
"lodash": "4.18.1",
"lodash-es": "4.18.1"
}

View File

@ -10247,15 +10247,16 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
version: 4.0.2
resolution: "form-data@npm:4.0.2"
"form-data@npm:4.0.4":
version: 4.0.4
resolution: "form-data@npm:4.0.4"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/e534b0cf025c831a0929bf4b9bbe1a9a6b03e273a8161f9947286b9b13bf8fb279c6944aae0070c4c311100c6d6dbb815cd955dc217728caf73fad8dc5b8ee9c
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
languageName: node
linkType: hard