diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 084ec0ba8..2594684f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,11 +3,11 @@ PLEASE READ THIS MESSAGE. Documentation fixes or enhancements: - for Traefik v2: use branch v2.11 -- for Traefik v3: use branch v3.3 +- for Traefik v3: use branch v3.4 Bug fixes: - for Traefik v2: use branch v2.11 -- for Traefik v3: use branch v3.3 +- for Traefik v3: use branch v3.4 Enhancements: - for Traefik v2: we only accept bug fixes diff --git a/CHANGELOG.md b/CHANGELOG.md index f494dbb3d..d3516b689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +## [v3.4.0](https://github.com/traefik/traefik/tree/v3.4.0) (2025-05-05) +[All Commits](https://github.com/traefik/traefik/compare/v3.3.0-rc1...v3.4.0) + +**Enhancements:** +- **[acme]** Add acme.profile and acme.emailAddresses options ([#11597](https://github.com/traefik/traefik/pull/11597) by [ldez](https://github.com/ldez)) +- **[docker,ecs,docker/swarm,consulcatalog,nomad]** Allow configuring server URLs with label providers ([#11374](https://github.com/traefik/traefik/pull/11374) by [yelvert](https://github.com/yelvert)) +- **[k8s/crd]** Improve CEL validation on Ingress CRD resources ([#11311](https://github.com/traefik/traefik/pull/11311) by [mloiseleur](https://github.com/mloiseleur)) +- **[k8s/crd]** Remove default load-balancing strategy from CRD ([#11701](https://github.com/traefik/traefik/pull/11701) by [kevinpollet](https://github.com/kevinpollet)) +- **[k8s/crd]** Restrict regex validation of HTTP status codes for Ingress CRD resources ([#11670](https://github.com/traefik/traefik/pull/11670) by [jnoordsij](https://github.com/jnoordsij)) +- **[k8s/gatewayapi]** Set rule priority in Gateway API TLSRoute ([#11443](https://github.com/traefik/traefik/pull/11443) by [augustozanellato](https://github.com/augustozanellato)) +- **[k8s/ingress]** Add ingress status for ClusterIP and NodePort Service Type ([#11100](https://github.com/traefik/traefik/pull/11100) by [mlec1](https://github.com/mlec1)) +- **[middleware,authentication]** Add option to preserve request method in forwardAuth ([#11473](https://github.com/traefik/traefik/pull/11473) by [an09mous](https://github.com/an09mous)) +- **[middleware]** Support rewriting status codes in error page middleware ([#11520](https://github.com/traefik/traefik/pull/11520) by [sevensolutions](https://github.com/sevensolutions)) +- **[middleware]** Add Redis rate limiter ([#10211](https://github.com/traefik/traefik/pull/10211) by [longquan0104](https://github.com/longquan0104)) +- **[service]** Add p2c load-balancing strategy for servers load-balancer ([#11547](https://github.com/traefik/traefik/pull/11547) by [rtribotte](https://github.com/rtribotte)) +- **[sticky-session]** Support domain configuration for sticky cookies ([#11556](https://github.com/traefik/traefik/pull/11556) by [jleal52](https://github.com/jleal52)) +- **[tls,k8s/crd,service]** Allow root CA to be added through config maps ([#11475](https://github.com/traefik/traefik/pull/11475) by [Nelwhix](https://github.com/Nelwhix)) +- **[tls]** Add support to disable session ticket ([#11609](https://github.com/traefik/traefik/pull/11609) by [avdhoot](https://github.com/avdhoot)) +- **[udp]** Add support for UDP routing in systemd socket activation ([#11022](https://github.com/traefik/traefik/pull/11022) by [tsiid](https://github.com/tsiid)) +- **[webui]** Add auto webui theme option and default to it ([#11455](https://github.com/traefik/traefik/pull/11455) by [zizzfizzix](https://github.com/zizzfizzix)) +- Replace experimental maps and slices with stdlib ([#11350](https://github.com/traefik/traefik/pull/11350) by [Juneezee](https://github.com/Juneezee)) +- Bump github.com/redis/go-redis/v9 to v9.7.3 ([#11687](https://github.com/traefik/traefik/pull/11687) by [kevinpollet](https://github.com/kevinpollet)) + +**Documentation:** +- Prepare release v3.4.0-rc1 ([#11654](https://github.com/traefik/traefik/pull/11654) by [kevinpollet](https://github.com/kevinpollet)) +- Prepare release v3.4.0-rc2 ([#11707](https://github.com/traefik/traefik/pull/11707) by [rtribotte](https://github.com/rtribotte)) +- Deprecate defaultRuleSyntax and ruleSyntax options ([#11619](https://github.com/traefik/traefik/pull/11619) by [rtribotte](https://github.com/rtribotte)) + +**Misc:** +- Merge branch v3.3 into master ([#11653](https://github.com/traefik/traefik/pull/11653) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into master ([#11595](https://github.com/traefik/traefik/pull/11595) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into master ([#11541](https://github.com/traefik/traefik/pull/11541) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into master ([#11504](https://github.com/traefik/traefik/pull/11504) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into master ([#11420](https://github.com/traefik/traefik/pull/11420) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into master ([#11394](https://github.com/traefik/traefik/pull/11394) by [mmatur](https://github.com/mmatur)) +- Merge branch v3.3 into v3.4 ([#11736](https://github.com/traefik/traefik/pull/11736) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v3.3 into v3.4 ([#11705](https://github.com/traefik/traefik/pull/11705) by [kevinpollet](https://github.com/kevinpollet)) + +## [v3.3.7](https://github.com/traefik/traefik/tree/v3.3.7) (2025-05-05) +[All Commits](https://github.com/traefik/traefik/compare/v3.3.6...v3.3.7) + +**Bug fixes:** +- **[logs,middleware,accesslogs]** Add SpanID and TraceID accessLogs fields only when tracing is enabled ([#11715](https://github.com/traefik/traefik/pull/11715) by [rtribotte](https://github.com/rtribotte)) + ## [v3.4.0-rc2](https://github.com/traefik/traefik/tree/v3.4.0-rc2) (2025-04-18) [All Commits](https://github.com/traefik/traefik/compare/v3.4.0-rc1...v3.4.0-rc2) diff --git a/docs/content/middlewares/http/buffering.md b/docs/content/middlewares/http/buffering.md index c609fe469..1c1259419 100644 --- a/docs/content/middlewares/http/buffering.md +++ b/docs/content/middlewares/http/buffering.md @@ -267,4 +267,4 @@ The retry expression is defined as a logical combination of the functions below ### Content-Length -See [Best Practices: Content‑Length](../../security/best-practices/content-length.md) \ No newline at end of file +See [Best Practices: Content‑Length](../../security/content-length.md) diff --git a/docs/content/routing/overview.md b/docs/content/routing/overview.md index 650a72929..e65bea351 100644 --- a/docs/content/routing/overview.md +++ b/docs/content/routing/overview.md @@ -327,6 +327,11 @@ serversTransport: --serversTransport.maxIdleConnsPerHost=7 ``` +!!! info "Disable connection reuse" + + The default value of `maxIdleConnsPerHost` is 2, and the zero value is the fallback to the default (2). + If you want to disable connection reuse, set `maxIdleConnsPerHost` to -1. + #### `spiffe` Please note that [SPIFFE](../https/spiffe.md) must be enabled in the static configuration diff --git a/docs/content/security/best-practices/content-length.md b/docs/content/security/content-length.md similarity index 100% rename from docs/content/security/best-practices/content-length.md rename to docs/content/security/content-length.md diff --git a/docs/content/security/tls-certs-in-multi-tenant-kubernetes.md b/docs/content/security/tls-certs-in-multi-tenant-kubernetes.md new file mode 100644 index 000000000..8fd7ef959 --- /dev/null +++ b/docs/content/security/tls-certs-in-multi-tenant-kubernetes.md @@ -0,0 +1,38 @@ +--- +title: "TLS Certificates in Multi‑Tenant Kubernetes" +description: "Isolate TLS certificates in multi‑tenant clusters by keeping Secrets and routes in the same namespace and disabling cross‑namespace look‑ups in Traefik. Read the technical guidelines." +--- + +# TLS Certificates in Multi‑Tenant Kubernetes + +In a shared cluster, different teams can create `Ingress` or `IngressRoute` objects that Traefik consumes. + +Traefik does not support multi-tenancy when using the Kubernetes `Ingress` or `IngressRoute` specifications due to the way TLS certificate management is handled. + +At the core of this limitation is the TLS Store, which holds all the TLS certificates used by Traefik. +As this Store is global in Traefik, it is shared across all namespaces, meaning any `Ingress` or `IngressRoute` in the cluster can potentially reference or affect TLS configurations intended for other tenants. + +This lack of isolation poses a risk in multi-tenant environments where different teams or applications require strict boundaries between resources, especially around sensitive data like TLS certificates. + +In contrast, the [Kubernetes Gateway API](../providers/kubernetes-gateway.md) provides better primitives for secure multi-tenancy. +Specifically, the `Listener` resource in the Gateway API allows administrators to explicitly define which Route resources (e.g., `HTTPRoute`) are permitted to bind to which domain names or ports. +This capability enforces stricter ownership and isolation, making it a safer choice for multi-tenant use cases. + +## Recommended setup + +When strict boundaries are required between resources and teams, we recommend using one Traefik instance per tenant. + +In Kubernetes one way to isolate a tenant is to restrict it to a namespace. +In that case, the namespace options from the Kubernetes [CRD](../providers/kubernetes-crd.md#namespaces) and [Ingress](../providers/kubernetes-ingress.md#namespaces) providers can be leveraged. + +!!! tip "Dedicate one Traefik instance per tenant using the Helm Chart" + + ```yaml + providers: + kubernetesCRD: + namespaces: + - tenant + kubernetesIngress: + namespaces: + - tenant + ``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3e60ab843..fe10e3740 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -164,8 +164,8 @@ nav: - 'Overview': 'observability/tracing/overview.md' - 'OpenTelemetry': 'observability/tracing/opentelemetry.md' - 'Security': - - 'Best Practices': - - 'security/best-practices/content-length.md' + - 'Content-Length': 'security/content-length.md' + - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' - 'User Guides': - 'FastProxy': 'user-guides/fastproxy.md' - 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md' diff --git a/integration/redis_sentinel_test.go b/integration/redis_sentinel_test.go index ec60f8c74..e229ceb60 100644 --- a/integration/redis_sentinel_test.go +++ b/integration/redis_sentinel_test.go @@ -92,6 +92,9 @@ func (s *RedisSentinelSuite) setupSentinelConfiguration(ports []string) { require.NoError(s.T(), err) defer tmpFile.Close() + err = tmpFile.Chmod(0o666) + require.NoError(s.T(), err) + model := structs.Map(templateValue) model["SelfFilename"] = tmpFile.Name() diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 20b639774..6ec8ea4c9 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -212,8 +212,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http if span := trace.SpanFromContext(req.Context()); span != nil { spanContext := span.SpanContext() - logDataTable.Core[TraceID] = spanContext.TraceID().String() - logDataTable.Core[SpanID] = spanContext.SpanID().String() + if spanContext.HasTraceID() && spanContext.HasSpanID() { + logDataTable.Core[TraceID] = spanContext.TraceID().String() + logDataTable.Core[SpanID] = spanContext.SpanID().String() + } } reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable)) diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 936e1a787..6d6a8188f 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -27,8 +27,10 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/capture" "github.com/traefik/traefik/v3/pkg/types" "go.opentelemetry.io/collector/pdata/plog/plogotlp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" + "go.opentelemetry.io/otel/trace/embedded" ) const delta float64 = 1e-10 @@ -310,7 +312,7 @@ func TestLoggerHeaderFields(t *testing.T) { func TestLoggerCLF(t *testing.T) { logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} - doLogging(t, config) + doLogging(t, config, false) logData, err := os.ReadFile(logFilePath) require.NoError(t, err) @@ -322,7 +324,7 @@ func TestLoggerCLF(t *testing.T) { func TestLoggerCLFWithBufferingSize(t *testing.T) { logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024} - doLogging(t, config) + doLogging(t, config, false) // wait a bit for the buffer to be written in the file. time.Sleep(50 * time.Millisecond) @@ -371,10 +373,11 @@ func TestLoggerJSON(t *testing.T) { desc string config *types.AccessLog tls bool + tracing bool expected map[string]func(t *testing.T, value interface{}) }{ { - desc: "default config", + desc: "default config without tracing", config: &types.AccessLog{ FilePath: "", Format: JSONFormat, @@ -410,8 +413,48 @@ func TestLoggerJSON(t *testing.T) { "time": assertNotEmpty(), "StartLocal": assertNotEmpty(), "StartUTC": assertNotEmpty(), - TraceID: assertNotEmpty(), - SpanID: assertNotEmpty(), + }, + }, + { + desc: "default config with tracing", + config: &types.AccessLog{ + FilePath: "", + Format: JSONFormat, + }, + tracing: true, + expected: map[string]func(t *testing.T, value interface{}){ + RequestContentSize: assertFloat64(0), + RequestHost: assertString(testHostname), + RequestAddr: assertString(testHostname), + RequestMethod: assertString(testMethod), + RequestPath: assertString(testPath), + RequestProtocol: assertString(testProto), + RequestScheme: assertString(testScheme), + RequestPort: assertString("-"), + DownstreamStatus: assertFloat64(float64(testStatus)), + DownstreamContentSize: assertFloat64(float64(len(testContent))), + OriginContentSize: assertFloat64(float64(len(testContent))), + OriginStatus: assertFloat64(float64(testStatus)), + RequestRefererHeader: assertString(testReferer), + RequestUserAgentHeader: assertString(testUserAgent), + RouterName: assertString(testRouterName), + ServiceURL: assertString(testServiceName), + ClientUsername: assertString(testUsername), + ClientHost: assertString(testHostname), + ClientPort: assertString(strconv.Itoa(testPort)), + ClientAddr: assertString(fmt.Sprintf("%s:%d", testHostname, testPort)), + "level": assertString("info"), + "msg": assertString(""), + "downstream_Content-Type": assertString("text/plain; charset=utf-8"), + RequestCount: assertFloat64NotZero(), + Duration: assertFloat64NotZero(), + Overhead: assertFloat64NotZero(), + RetryAttempts: assertFloat64(float64(testRetryAttempts)), + "time": assertNotEmpty(), + "StartLocal": assertNotEmpty(), + "StartUTC": assertNotEmpty(), + TraceID: assertString("01000000000000000000000000000000"), + SpanID: assertString("0100000000000000"), }, }, { @@ -455,8 +498,6 @@ func TestLoggerJSON(t *testing.T) { "time": assertNotEmpty(), StartLocal: assertNotEmpty(), StartUTC: assertNotEmpty(), - TraceID: assertNotEmpty(), - SpanID: assertNotEmpty(), }, }, { @@ -578,9 +619,9 @@ func TestLoggerJSON(t *testing.T) { test.config.FilePath = logFilePath if test.tls { - doLoggingTLS(t, test.config) + doLoggingTLS(t, test.config, test.tracing) } else { - doLogging(t, test.config) + doLogging(t, test.config, test.tracing) } logData, err := os.ReadFile(logFilePath) @@ -632,8 +673,6 @@ func TestLogger_AbortedRequest(t *testing.T) { "downstream_Content-Type": assertString("text/plain"), "downstream_Transfer-Encoding": assertString("chunked"), "downstream_Cache-Control": assertString("no-cache"), - TraceID: assertNotEmpty(), - SpanID: assertNotEmpty(), } config := &types.AccessLog{ @@ -854,7 +893,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) { file, restoreStdout := captureStdout(t) defer restoreStdout() - doLogging(t, test.config) + doLogging(t, test.config, false) written, err := os.ReadFile(file.Name()) require.NoError(t, err, "unable to read captured stdout from file") @@ -913,7 +952,7 @@ func captureStdout(t *testing.T) (out *os.File, restoreStdout func()) { return file, restoreStdout } -func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS bool) { +func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS, tracing bool) { t.Helper() logger, err := NewHandler(config) require.NoError(t, err) @@ -952,9 +991,10 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS bool) { } } - tracer := noop.Tracer{} - spanCtx, _ := tracer.Start(req.Context(), "test") - req = req.WithContext(spanCtx) + if tracing { + contextWithSpan := trace.ContextWithSpan(req.Context(), &mockSpan{}) + req = req.WithContext(contextWithSpan) + } chain := alice.New() chain = chain.Append(capture.Wrap) @@ -965,16 +1005,16 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS bool) { handler.ServeHTTP(httptest.NewRecorder(), req) } -func doLoggingTLS(t *testing.T, config *types.AccessLog) { +func doLoggingTLS(t *testing.T, config *types.AccessLog, tracing bool) { t.Helper() - doLoggingTLSOpt(t, config, true) + doLoggingTLSOpt(t, config, true, tracing) } -func doLogging(t *testing.T, config *types.AccessLog) { +func doLogging(t *testing.T, config *types.AccessLog, tracing bool) { t.Helper() - doLoggingTLSOpt(t, config, false) + doLoggingTLSOpt(t, config, false, tracing) } func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) { @@ -1091,3 +1131,27 @@ func streamBackend(rw http.ResponseWriter, r *http.Request) { } } } + +// mockSpan is an implementation of Span that preforms no operations. +type mockSpan struct { + embedded.Span +} + +var _ trace.Span = &mockSpan{} + +func (*mockSpan) SpanContext() trace.SpanContext { + return trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID{1}, SpanID: trace.SpanID{1}}) +} +func (*mockSpan) IsRecording() bool { return true } +func (s *mockSpan) SetStatus(_ codes.Code, _ string) {} +func (s *mockSpan) SetAttributes(...attribute.KeyValue) {} +func (s *mockSpan) End(...trace.SpanEndOption) {} +func (s *mockSpan) RecordError(_ error, _ ...trace.EventOption) {} +func (s *mockSpan) AddEvent(_ string, _ ...trace.EventOption) {} +func (s *mockSpan) AddLink(_ trace.Link) {} + +func (s *mockSpan) SetName(_ string) {} + +func (s *mockSpan) TracerProvider() trace.TracerProvider { + return nil +} diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index cedcc34d0..18f1a7c29 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.3.6 +# example new bugfix v3.3.7 CurrentRef = "v3.3" -PreviousRef = "v3.3.5" +PreviousRef = "v3.3.6" BaseBranch = "v3.3" -FutureCurrentRefName = "v3.3.6" +FutureCurrentRefName = "v3.3.7" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 diff --git a/script/gcg/traefik-final-release-part1.toml b/script/gcg/traefik-final-release-part1.toml index f8313d1ca..edba19179 100644 --- a/script/gcg/traefik-final-release-part1.toml +++ b/script/gcg/traefik-final-release-part1.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example final release of v3.3.0 -CurrentRef = "v3.3" -PreviousRef = "v3.3.0-rc1" -BaseBranch = "v3.3" -FutureCurrentRefName = "v3.3.0" +# example final release of v3.4.0 +CurrentRef = "v3.4" +PreviousRef = "v3.4.0-rc1" +BaseBranch = "v3.4" +FutureCurrentRefName = "v3.4.0" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 diff --git a/script/gcg/traefik-final-release-part2.toml b/script/gcg/traefik-final-release-part2.toml index 72fbfdcb5..453ff650f 100644 --- a/script/gcg/traefik-final-release-part2.toml +++ b/script/gcg/traefik-final-release-part2.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example final release of v3.3.0 -CurrentRef = "v3.3.0-rc1" -PreviousRef = "v3.2.0-rc1" +# example final release of v3.4.0 +CurrentRef = "v3.4.0-rc1" +PreviousRef = "v3.3.0-rc1" BaseBranch = "master" -FutureCurrentRefName = "v3.3.0" +FutureCurrentRefName = "v3.4.0" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10