Merge branch v3.6 into v3.7

This commit is contained in:
romain 2026-03-16 16:44:07 +01:00
commit d1a6841275
8 changed files with 64 additions and 25 deletions

View File

@ -21,3 +21,33 @@ We want to keep Traefik safe for everyone.
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).
## Submission Quality Guidelines
We have been receiving an increasing number of low-quality vulnerability reports that are not actual security issues.
Many of these reports originate from AI/LLM tools and are submitted without any human validation or testing.
This wastes the time of our security team and delays the handling of legitimate vulnerabilities.
Before submitting a security advisory, you **must**:
- **Carefully test and validate** the vulnerability yourself before submitting.
You must be able to demonstrate a working proof of concept with clear reproduction steps.
- **Understand the impact** of the vulnerability and explain how it can be exploited in a realistic scenario.
- **Verify that the issue is not a false positive**.
Ensure the behavior you are reporting is actually a security concern and not expected behavior.
### Policy on AI-Generated Reports
Security reports that are **directly generated by AI/LLM tools without proper human validation** will be **closed immediately**.
Indicators of unvalidated AI-generated reports include (but are not limited to):
- No working proof of concept or reproduction steps.
- Generic or theoretical vulnerability descriptions with no evidence of actual testing.
- Misunderstanding of Traefik's architecture or threat model.
- Hallucinated code paths, configuration options, or behaviors that do not exist.
**Contributors who repeatedly submit low-quality or unvalidated reports may have their accounts blocked.**
We appreciate the work of security researchers who take the time to rigorously validate their findings.
Quality over quantity helps keep Traefik safe for everyone.

View File

@ -281,8 +281,10 @@ accessLog:
| `TLSVersion` | The TLS version used by the connection (e.g. `1.2`) (if connection is TLS). |
| `TLSCipher` | The TLS cipher used by the connection (e.g. `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`) (if connection is TLS) |
| `TLSClientSubject` | The string representation of the TLS client certificate's Subject (e.g. `CN=username,O=organization`) |
| `TraceId` | A consistent identifier for tracking requests across services, including upstream ones managed by Traefik, shown as a 32-hex digit string |
| `SpanId` | A unique identifier for Traefiks root span (EntryPoint) within a request trace, formatted as a 16-hex digit string. |
| `TraceId` | (Deprecated) A consistent identifier for tracking requests across services, including upstream ones managed by Traefik, shown as a 32-hex digit string |
| `SpanId` | (Deprecated) A unique identifier for Traefiks root span (EntryPoint) within a request trace, formatted as a 16-hex digit string. |
| `trace_id` | OTel-conformant trace identifier for tracking requests across services, including upstream ones managed by Traefik, shown as a 32-hex digit string |
| `span_id` | OTel-conformant span identifier for Traefiks root span (EntryPoint) within a request trace, formatted as a 16-hex digit string. |
## Log Rotation

View File

@ -41,8 +41,8 @@ The `Header` and `HeaderRegexp` matchers allow matching requests that contain sp
| Behavior | Rule |
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-requests-with-a-Content-Type-header-set-to-applicationyaml" href="#opt-Match-requests-with-a-Content-Type-header-set-to-applicationyaml" title="#opt-Match-requests-with-a-Content-Type-header-set-to-applicationyaml">Match requests with a `Content-Type` header set to `application/yaml`.</a> | ```Header(`Content-Type`, `application/yaml`)``` |
| <a id="opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml" href="#opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml" title="#opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml">Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`.</a> | ```HeaderRegexp(`Content-Type`, `^application/(json\|yaml)$`)``` |
| <a id="opt-Match-headers-case-insensitively" href="#opt-Match-headers-case-insensitively" title="#opt-Match-headers-case-insensitively">Match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```HeaderRegexp(`Content-Type`, `(?i)^application/(json\|yaml)$`)``` |
| <a id="opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml" href="#opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml" title="#opt-Match-requests-with-a-Content-Type-header-set-to-either-applicationjson-or-applicationyaml">Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`.</a> | ```HeaderRegexp(`Content-Type`, `^application/(json|yaml)$`)``` |
| <a id="opt-Match-headers-case-insensitively" href="#opt-Match-headers-case-insensitively" title="#opt-Match-headers-case-insensitively">Match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```HeaderRegexp(`Content-Type`, `(?i)^application/(json|yaml)$`)``` |
### Host and HostRegexp
@ -58,8 +58,8 @@ These matchers will match the request's host in lowercase.
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-requests-with-Host-set-to-example-com" href="#opt-Match-requests-with-Host-set-to-example-com" title="#opt-Match-requests-with-Host-set-to-example-com">Match requests with `Host` set to `example.com`.</a> | ```Host(`example.com`)``` |
| <a id="opt-Match-requests-sent-to-any-subdomain-of-example-com" href="#opt-Match-requests-sent-to-any-subdomain-of-example-com" title="#opt-Match-requests-sent-to-any-subdomain-of-example-com">Match requests sent to any subdomain of `example.com`.</a> | ```HostRegexp(`^.+\.example\.com$`)``` |
| <a id="opt-Match-requests-with-Host-set-to-either-example-com-or-example-org" href="#opt-Match-requests-with-Host-set-to-either-example-com-or-example-org" title="#opt-Match-requests-with-Host-set-to-either-example-com-or-example-org">Match requests with `Host` set to either `example.com` or `example.org`.</a> | ```HostRegexp(`^example\.(com\|org)$`)``` |
| <a id="opt-Match-Host-case-insensitively" href="#opt-Match-Host-case-insensitively" title="#opt-Match-Host-case-insensitively">Match `Host` [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```HostRegexp(`(?i)^example\.(com\|org)$`)``` |
| <a id="opt-Match-requests-with-Host-set-to-either-example-com-or-example-org" href="#opt-Match-requests-with-Host-set-to-either-example-com-or-example-org" title="#opt-Match-requests-with-Host-set-to-either-example-com-or-example-org">Match requests with `Host` set to either `example.com` or `example.org`.</a> | ```HostRegexp(`^example\.(com|org)$`)``` |
| <a id="opt-Match-Host-case-insensitively" href="#opt-Match-Host-case-insensitively" title="#opt-Match-Host-case-insensitively">Match `Host` [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```HostRegexp(`(?i)^example\.(com|org)$`)``` |
### Method
@ -81,8 +81,8 @@ Path are always starting with a `/`, except for `PathRegexp`.
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-products-but-neither-productsshoes-nor-products" href="#opt-Match-products-but-neither-productsshoes-nor-products" title="#opt-Match-products-but-neither-productsshoes-nor-products">Match `/products` but neither `/products/shoes` nor `/products/`.</a> | ```Path(`/products`)``` |
| <a id="opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale" href="#opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale" title="#opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale">Match `/products` as well as everything under `/products`, such as `/products/shoes`, `/products/` but also `/products-for-sale`.</a> | ```PathPrefix(`/products`)``` |
| <a id="opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31" href="#opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31" title="#opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31">Match both `/products/shoes` and `/products/socks` with and ID like `/products/shoes/31`.</a> | ```PathRegexp(`^/products/(shoes\|socks)/[0-9]+$`)``` |
| <a id="opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png" href="#opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png" title="#opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png">Match requests with a path ending in either `.jpeg`, `.jpg` or `.png`.</a> | ```PathRegexp(`\.(jpeg\|jpg\|png)$`)``` |
| <a id="opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31" href="#opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31" title="#opt-Match-both-productsshoes-and-productssocks-with-and-ID-like-productsshoes31">Match both `/products/shoes` and `/products/socks` with and ID like `/products/shoes/31`.</a> | ```PathRegexp(`^/products/(shoes|socks)/[0-9]+$`)``` |
| <a id="opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png" href="#opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png" title="#opt-Match-requests-with-a-path-ending-in-either-jpeg-jpg-or-png">Match requests with a path ending in either `.jpeg`, `.jpg` or `.png`.</a> | ```PathRegexp(`\.(jpeg|jpg|png)$`)``` |
| <a id="opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale-case-insensitively" href="#opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale-case-insensitively" title="#opt-Match-products-as-well-as-everything-under-products-such-as-productsshoes-products-but-also-products-for-sale-case-insensitively">Match `/products` as well as everything under `/products`, such as `/products/shoes`, `/products/` but also `/products-for-sale`, [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```PathRegexp(`(?i)^/products`)``` |
### Query and QueryRegexp
@ -93,9 +93,9 @@ The `Query` and `QueryRegexp` matchers allow matching requests based on query pa
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-requests-with-a-mobile-query-parameter-set-to-true-such-as-in-searchmobiletrue" href="#opt-Match-requests-with-a-mobile-query-parameter-set-to-true-such-as-in-searchmobiletrue" title="#opt-Match-requests-with-a-mobile-query-parameter-set-to-true-such-as-in-searchmobiletrue">Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`.</a> | ```Query(`mobile`, `true`)``` |
| <a id="opt-Match-requests-with-a-query-parameter-mobile-that-has-no-value-such-as-in-searchmobile" href="#opt-Match-requests-with-a-query-parameter-mobile-that-has-no-value-such-as-in-searchmobile" title="#opt-Match-requests-with-a-query-parameter-mobile-that-has-no-value-such-as-in-searchmobile">Match requests with a query parameter `mobile` that has no value, such as in `/search?mobile`.</a> | ```Query(`mobile`)``` |
| <a id="opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes" href="#opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes" title="#opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes">Match requests with a `mobile` query parameter set to either `true` or `yes`.</a> | ```QueryRegexp(`mobile`, `^(true\|yes)$`)``` |
| <a id="opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes" href="#opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes" title="#opt-Match-requests-with-a-mobile-query-parameter-set-to-either-true-or-yes">Match requests with a `mobile` query parameter set to either `true` or `yes`.</a> | ```QueryRegexp(`mobile`, `^(true|yes)$`)``` |
| <a id="opt-Match-requests-with-a-mobile-query-parameter-set-to-any-value-including-the-empty-value" href="#opt-Match-requests-with-a-mobile-query-parameter-set-to-any-value-including-the-empty-value" title="#opt-Match-requests-with-a-mobile-query-parameter-set-to-any-value-including-the-empty-value">Match requests with a `mobile` query parameter set to any value (including the empty value).</a> | ```QueryRegexp(`mobile`, `^.*$`)``` |
| <a id="opt-Match-query-parameters-case-insensitively" href="#opt-Match-query-parameters-case-insensitively" title="#opt-Match-query-parameters-case-insensitively">Match query parameters [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```QueryRegexp(`mobile`, `(?i)^(true\|yes)$`)``` |
| <a id="opt-Match-query-parameters-case-insensitively" href="#opt-Match-query-parameters-case-insensitively" title="#opt-Match-query-parameters-case-insensitively">Match query parameters [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity).</a> | ```QueryRegexp(`mobile`, `(?i)^(true|yes)$`)``` |
### ClientIP

View File

@ -78,10 +78,15 @@ const (
// TLSClientSubject is the string representation of the TLS client certificate's Subject.
TLSClientSubject = "TLSClientSubject"
// TraceID is the consistent identifier for tracking requests across services, including upstream ones managed by Traefik, shown as a 32-hex digit string.
// Deprecated: TraceID is the consistent identifier for tracking requests across services, including upstream ones managed by Traefik, shown as a 32-hex digit string.
TraceID = "TraceId"
// SpanID is the unique identifier for Traefiks root span (EntryPoint) within a request trace, formatted as a 16-hex digit string.
// Deprecated: SpanID is the unique identifier for Traefiks root span (EntryPoint) within a request trace, formatted as a 16-hex digit string.
SpanID = "SpanId"
// OTelTraceID is the OTel-conformant log attribute for the trace identifier.
OTelTraceID = "trace_id"
// OTelSpanID is the OTel-conformant log attribute for the span identifier.
OTelSpanID = "span_id"
)
// These are written out in the default case when no config is provided to specify keys of interest.
@ -126,6 +131,8 @@ func init() {
allCoreKeys[TLSVersion] = struct{}{}
allCoreKeys[TLSCipher] = struct{}{}
allCoreKeys[TLSClientSubject] = struct{}{}
allCoreKeys[OTelTraceID] = struct{}{}
allCoreKeys[OTelSpanID] = struct{}{}
}
// CoreLogData holds the fields computed from the request/response.

View File

@ -209,6 +209,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
if spanContext.HasTraceID() && spanContext.HasSpanID() {
logDataTable.Core[TraceID] = spanContext.TraceID().String()
logDataTable.Core[SpanID] = spanContext.SpanID().String()
logDataTable.Core[OTelTraceID] = spanContext.TraceID().String()
logDataTable.Core[OTelSpanID] = spanContext.SpanID().String()
}
}

View File

@ -622,6 +622,8 @@ func TestLoggerJSON(t *testing.T) {
"StartUTC": assertNotEmpty(),
TraceID: assertString("01000000000000000000000000000000"),
SpanID: assertString("0100000000000000"),
OTelTraceID: assertString("01000000000000000000000000000000"),
OTelSpanID: assertString("0100000000000000"),
},
},
{

View File

@ -1093,24 +1093,13 @@ func findMatchingHostname(h1, h2 gatev1.Hostname) gatev1.Hostname {
}
trimmedH1 := strings.TrimPrefix(string(h1), "*")
// root domain doesn't match subdomain wildcard.
if trimmedH1 == string(h2) {
return ""
}
if !strings.HasSuffix(string(h2), trimmedH1) {
return ""
}
return lessWildcards(h1, h2)
}
func lessWildcards(h1, h2 gatev1.Hostname) gatev1.Hostname {
if strings.Count(string(h1), "*") > strings.Count(string(h2), "*") {
return h2
}
return h1
// since h1 is a suffix of h2, we know h2 is the more specific host
return h2
}
func allowRoute(listener gatewayListener, routeNamespace, routeKind string) bool {

View File

@ -7702,6 +7702,13 @@ func Test_findMatchingHostnames(t *testing.T) {
want: []gatev1.Hostname{"toto.foo.com", "test.foo.com"},
wantOk: true,
},
{
desc: "Matching wildcard subsubdomain with listener wildcard subdomain",
listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")),
routeHostnames: []gatev1.Hostname{"*.bar.foo.com"},
want: []gatev1.Hostname{"*.bar.foo.com"},
wantOk: true,
},
}
for _, test := range testCases {