mirror of
https://github.com/traefik/traefik.git
synced 2026-06-17 19:09:29 +03:00
Merge branch v3.7 into master
This commit is contained in:
@@ -1,3 +1,30 @@
|
||||
## [v3.7.5](https://github.com/traefik/traefik/tree/v3.7.5) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.7.4...v3.7.5)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[k8s/ingress-nginx]** Skip ingress when auth-secret resolution fails ([#13323](https://github.com/traefik/traefik/pull/13323) @gndz07)
|
||||
- **[k8s/ingress-nginx]** Pass endpointslice fencing on ingress-nginx provider ([#13290](https://github.com/traefik/traefik/pull/13290) @Learloj)
|
||||
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
|
||||
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v3.6.21](https://github.com/traefik/traefik/tree/v3.6.21) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.6.20...v3.6.21)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
|
||||
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v2.11.50](https://github.com/traefik/traefik/tree/v2.11.50) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v2.11.49...v2.11.50)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/moby/moby/client v0.4.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
|
||||
@@ -1748,8 +1748,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
|
||||
[entryPoints.websecure]
|
||||
address = ":4443"
|
||||
|
||||
[entryPoints.websecure2]
|
||||
address = ":4444"
|
||||
|
||||
[api]
|
||||
insecure = true
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
# --- Same host, same options, same entryPoint: no conflict, the options are applied. ---
|
||||
[http.routers.same-1]
|
||||
rule = "Host(`same.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.same-1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.same-2]
|
||||
rule = "Host(`same.www.snitest.com`) && PathPrefix(`/same`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.same-2.tls]
|
||||
options = "tls12"
|
||||
|
||||
# --- Same host, different options, same entryPoint: conflict, fallback to default options. ---
|
||||
[http.routers.conflict-1]
|
||||
rule = "Host(`conflict.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.conflict-1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.conflict-2]
|
||||
rule = "Host(`conflict.www.snitest.com`) && PathPrefix(`/conflict`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.conflict-2.tls]
|
||||
options = "tls13"
|
||||
|
||||
# --- Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options. ---
|
||||
[http.routers.cross-ep1]
|
||||
rule = "Host(`cross.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.cross-ep1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.cross-ep2]
|
||||
rule = "Host(`cross.www.snitest.com`)"
|
||||
entryPoints = ["websecure2"]
|
||||
service = "service1"
|
||||
[http.routers.cross-ep2.tls]
|
||||
options = "tls13"
|
||||
|
||||
# --- Domain fronting (Host header != SNI): same options follow the header, different options are rejected. ---
|
||||
[http.routers.df-a]
|
||||
rule = "Host(`df-a.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-a.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.df-b]
|
||||
rule = "Host(`df-b.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-b.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.df-c]
|
||||
rule = "Host(`df-c.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-c.tls]
|
||||
options = "tls13"
|
||||
|
||||
[http.services.service1]
|
||||
[[http.services.service1.loadBalancer.servers]]
|
||||
url = "http://127.0.0.1:9010"
|
||||
|
||||
[[tls.certificates]]
|
||||
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
|
||||
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
|
||||
|
||||
[tls.options]
|
||||
[tls.options.tls12]
|
||||
maxVersion = "VersionTLS12"
|
||||
[tls.options.tls13]
|
||||
minVersion = "VersionTLS13"
|
||||
+148
-1
@@ -415,7 +415,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
|
||||
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
|
||||
|
||||
// with unknown tls option
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"))
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
@@ -1262,6 +1262,153 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithTLSOptionsConflict checks how TLS options are resolved when several routers
|
||||
// target the same host (SNI), across the different conflict situations:
|
||||
// - same options on the same entryPoint: no conflict, the options are applied;
|
||||
// - different options on the same entryPoint: conflict, fallback to the default options;
|
||||
// - different options on different entryPoints: no conflict, each entryPoint keeps its
|
||||
// own options (they are selected independently on each listener);
|
||||
// - domain fronting (Host header != SNI): allowed when both resolve to the same options,
|
||||
// rejected with a 421 otherwise.
|
||||
//
|
||||
// The effective TLS options are probed through the negotiated TLS version: the "tls12"
|
||||
// options cap the version to TLS 1.2, while the "tls13" options require at least TLS 1.3.
|
||||
func (s *HTTPSSuite) TestWithTLSOptionsConflict() {
|
||||
backend := startTestServer("9010", http.StatusOK, "server1")
|
||||
defer backend.Close()
|
||||
|
||||
file := s.adaptFile("fixtures/https/https_tls_options_conflict.toml", struct{}{})
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// wait for Traefik
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`cross.www.snitest.com`)"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
addr string // entryPoint address to reach
|
||||
hostHeader string
|
||||
serverName string // SNI
|
||||
minVersion uint16 // 0 means the crypto/tls library default
|
||||
maxVersion uint16 // 0 means the crypto/tls library default
|
||||
// expectHandshakeError is set when the TLS handshake itself is expected to fail
|
||||
// (i.e. the probed options reject the client's TLS version). Otherwise
|
||||
// expectedStatusCode is asserted on the HTTP response.
|
||||
expectHandshakeError bool
|
||||
expectedStatusCode int
|
||||
}{
|
||||
// Same host, same options, same entryPoint: no conflict, the "tls12" options are applied.
|
||||
{
|
||||
desc: "same options / same entryPoint: TLS 1.2 client is accepted",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "same.www.snitest.com",
|
||||
serverName: "same.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "same options / same entryPoint: TLS 1.3 client is rejected (maxVersion TLS1.2 enforced)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "same.www.snitest.com",
|
||||
serverName: "same.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
|
||||
// Same host, different options, same entryPoint: conflict, both routers fall back to the default options.
|
||||
{
|
||||
desc: "conflicting options / same entryPoint: TLS 1.3 client is accepted (default options used)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "conflict.www.snitest.com",
|
||||
serverName: "conflict.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "conflicting options / same entryPoint: TLS 1.2 client is accepted (default options used)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "conflict.www.snitest.com",
|
||||
serverName: "conflict.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
|
||||
// Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options.
|
||||
{
|
||||
desc: "different entryPoints: websecure keeps tls12, TLS 1.2 client is accepted",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure keeps tls12, TLS 1.3 client is rejected",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.3 client is accepted",
|
||||
addr: "127.0.0.1:4444",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.2 client is rejected",
|
||||
addr: "127.0.0.1:4444",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
|
||||
// Domain fronting (Host header != SNI) on the same entryPoint.
|
||||
{
|
||||
desc: "domain fronting / same options: request follows the Host header (200)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "df-a.www.snitest.com",
|
||||
serverName: "df-b.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "domain fronting / different options: request is misdirected (421)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "df-a.www.snitest.com",
|
||||
serverName: "df-c.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusMisdirectedRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: test.serverName,
|
||||
MinVersion: test.minVersion,
|
||||
MaxVersion: test.maxVersion,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+test.addr+"/", nil)
|
||||
require.NoError(s.T(), err)
|
||||
req.Host = test.hostHeader
|
||||
|
||||
if test.expectHandshakeError {
|
||||
_, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}).Do(req)
|
||||
assert.ErrorContains(s.T(), err, "tls:", "test %q should fail the TLS handshake", test.desc)
|
||||
continue
|
||||
}
|
||||
|
||||
err = try.RequestWithTransport(req, 2*time.Second, &http.Transport{TLSClientConfig: tlsConfig}, try.StatusCodeIs(test.expectedStatusCode))
|
||||
assert.NoError(s.T(), err, "test %q failed with: %v", test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration.
|
||||
func (s *HTTPSSuite) TestWithInvalidTLSOption() {
|
||||
backend := startTestServer("9010", http.StatusOK, "server1")
|
||||
|
||||
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// All errors
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"]`))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router3 has an error because it uses an unknown entrypoint
|
||||
@@ -957,11 +957,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router4 is enabled, but in warning state because its tls options conf was messed up
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router5 is disabled because its middleware conf is broken
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router5@file", 1000*time.Millisecond, try.BodyContains())
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router5@file", 1000*time.Millisecond, try.BodyContains())
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "foo.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: /bar
|
||||
backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 80
|
||||
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
group: ""
|
||||
kind: Service
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tcp
|
||||
protocol: TCP
|
||||
port: 9000
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TCPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TCPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
metadata:
|
||||
name: tcp-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 9000
|
||||
- name: whoamitcp
|
||||
port: 9000
|
||||
weight: 1
|
||||
group: ""
|
||||
kind: Service
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: supersecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tls
|
||||
protocol: TLS
|
||||
port: 9000
|
||||
tls:
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: supersecret
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 9000
|
||||
- name: whoamitcp
|
||||
port: 9000
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
@@ -240,6 +240,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
|
||||
|
||||
@@ -8779,20 +8779,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
@@ -8842,20 +8844,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
@@ -8914,20 +8918,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
|
||||
@@ -221,6 +221,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
|
||||
|
||||
@@ -239,6 +239,17 @@ func (p *Provider) loadTLSService(ctx context.Context, listener gatewayListener,
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
|
||||
|
||||
@@ -415,11 +415,13 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)).
|
||||
Msg("Cannot resolve auth secret, skipping auth middleware")
|
||||
} else {
|
||||
loc.BasicAuth = basic
|
||||
loc.DigestAuth = digest
|
||||
Msg("Cannot resolve auth secret, skipping ingress")
|
||||
// Skipping the ingress entirely when auth secret resolution fails,
|
||||
// to match ingress-nginx behavior.
|
||||
continue
|
||||
}
|
||||
loc.BasicAuth = basic
|
||||
loc.DigestAuth = digest
|
||||
}
|
||||
|
||||
// Pre-resolve custom headers ConfigMap.
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-basicauth-secret-missing
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-type: "basic"
|
||||
nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
|
||||
nginx.ingress.kubernetes.io/auth-secret: "default/missing-basic-auth"
|
||||
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /basicauth
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-endpoint-conditions
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
clusterIP: 10.10.10.1
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: web
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: whoami-abc
|
||||
namespace: default
|
||||
labels:
|
||||
kubernetes.io/service-name: whoami
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: web
|
||||
port: 80
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
serving: true
|
||||
terminating: false
|
||||
- addresses:
|
||||
- 10.10.0.2
|
||||
conditions:
|
||||
ready: false
|
||||
serving: true
|
||||
terminating: true
|
||||
- addresses:
|
||||
- 10.10.0.3
|
||||
conditions:
|
||||
ready: false
|
||||
serving: false
|
||||
terminating: true
|
||||
@@ -1329,6 +1329,37 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Basic Auth with missing secret — ingress is skipped entirely",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-basicauth-secret-missing.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"unavailable-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Forward Auth",
|
||||
paths: []string{
|
||||
@@ -15656,6 +15687,102 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with endpoint conditions",
|
||||
paths: []string{
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-endpoint-conditions.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0": {
|
||||
EntryPoints: []string{"http"},
|
||||
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-with-endpoint-conditions-whoami-80",
|
||||
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-retry"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "ingress-with-endpoint-conditions",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls": {
|
||||
EntryPoints: []string{"https"},
|
||||
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-with-endpoint-conditions-whoami-80",
|
||||
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "ingress-with-endpoint-conditions",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{Attempts: 3},
|
||||
},
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry": {
|
||||
Retry: &dynamic.Retry{Attempts: 3},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-endpoint-conditions-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{URL: "http://10.10.0.1:80"},
|
||||
{URL: "http://10.10.0.2:80", Fenced: true},
|
||||
},
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
PassHostHeader: ptr.To(true),
|
||||
ServersTransport: "default-ingress-with-endpoint-conditions",
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
"unavailable-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-ingress-with-endpoint-conditions": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Auth TLS secret missing — ingress is skipped entirely",
|
||||
paths: []string{
|
||||
|
||||
@@ -294,7 +294,8 @@ func buildService(backend *backend, serversTransportName string) *dynamic.Servic
|
||||
svc := &dynamic.Service{LoadBalancer: lb}
|
||||
for _, ep := range backend.Endpoints {
|
||||
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
||||
URL: fmt.Sprintf("http://%s", ep.Address),
|
||||
URL: fmt.Sprintf("http://%s", ep.Address),
|
||||
Fenced: ep.Fenced,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -320,7 +321,8 @@ func buildServiceWithLocConfig(backend *backend, serversTransportName string, lo
|
||||
svc := &dynamic.Service{LoadBalancer: lb}
|
||||
for _, ep := range backend.Endpoints {
|
||||
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
||||
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
|
||||
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
|
||||
Fenced: ep.Fenced,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+101
-50
@@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
Str(logs.RouterName, routerName).
|
||||
Strs(logs.EntryPointName, defaultEntryPoints).
|
||||
Msg("No entryPoint defined for this router, using the default one(s) instead")
|
||||
router.EntryPoints = defaultEntryPoints
|
||||
router.EntryPoints = slices.Clone(defaultEntryPoints)
|
||||
}
|
||||
|
||||
// The `ruleSyntax` option is deprecated.
|
||||
@@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
log.Debug().
|
||||
Str(logs.RouterName, routerName).
|
||||
Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints)
|
||||
router.EntryPoints = defaultEntryPoints
|
||||
router.EntryPoints = slices.Clone(defaultEntryPoints)
|
||||
}
|
||||
conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router
|
||||
}
|
||||
@@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
return conf
|
||||
}
|
||||
|
||||
func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
|
||||
return cfg
|
||||
// resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per
|
||||
// entryPoint basis.
|
||||
//
|
||||
// TLS options conflicts (i.e. the same host served with different TLS options) can
|
||||
// only be detected and arbitrated within a single TLS listener, that is to say within
|
||||
// a single entryPoint. To honor that, routers are grouped per entryPoint and the
|
||||
// conflict detection is run independently for each entryPoint.
|
||||
//
|
||||
// A router keeps its original name, and its resolved TLS options, for the entryPoints
|
||||
// on which it does not conflict. For each entryPoint on which it conflicts, that
|
||||
// entryPoint is removed from the router and a dedicated copy is emitted, with its
|
||||
// TLSOptions reset to the default one, named following the "ep-conflicted-name@provider" pattern.
|
||||
func resolveHTTPTLSOptions(routers map[string]*dynamic.Router) map[string]*dynamic.Router {
|
||||
if len(routers) == 0 {
|
||||
return routers
|
||||
}
|
||||
|
||||
rts := make(map[string]*dynamic.Router)
|
||||
newRouters := make(map[string]*dynamic.Router)
|
||||
|
||||
// Keyed by domain, then by options reference.
|
||||
// The actual source of truth for what TLS options will actually be used for the connection.
|
||||
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
|
||||
// options that occur for a given host name, so that later on we can set relevant
|
||||
// errors and logging for all the routers concerned (i.e. wrongly configured).
|
||||
tlsOptionsForHostSNI := map[string]map[string][]string{}
|
||||
|
||||
for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
|
||||
rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
|
||||
|
||||
if routerHTTPConfig.TLS == nil {
|
||||
// Split every router per entryPoint.
|
||||
// Routers always have at least one entryPoint at this stage, as they are
|
||||
// defaulted in mergeConfiguration before applyModel and this resolution run.
|
||||
routersByEntryPoint := map[string]map[string]*dynamic.Router{}
|
||||
for name, router := range routers {
|
||||
if router.TLS == nil {
|
||||
newRouters[name] = router
|
||||
continue
|
||||
}
|
||||
|
||||
ctxRouter := provider.AddInContext(context.Background(), routerHTTPName)
|
||||
logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger()
|
||||
|
||||
tlsOptionsName := traefiktls.DefaultTLSConfigName
|
||||
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
|
||||
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
|
||||
router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName {
|
||||
router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.TLS.Options)
|
||||
}
|
||||
|
||||
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
|
||||
for _, ep := range router.EntryPoints {
|
||||
if routersByEntryPoint[ep] == nil {
|
||||
routersByEntryPoint[ep] = map[string]*dynamic.Router{}
|
||||
}
|
||||
|
||||
routersByEntryPoint[ep][name] = router
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the TLS options independently for each entryPoint.
|
||||
conflictingRouters := make(map[string][]string, len(routersByEntryPoint))
|
||||
for ep, epRouters := range routersByEntryPoint {
|
||||
conflictingRouters[ep] = findConflictingRouters(ep, epRouters)
|
||||
}
|
||||
|
||||
for name, router := range routers {
|
||||
router.EntryPoints = slices.DeleteFunc(router.EntryPoints, func(ep string) bool {
|
||||
deleted := slices.Contains(conflictingRouters[ep], name)
|
||||
if deleted {
|
||||
rt := router.DeepCopy()
|
||||
rt.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
rt.EntryPoints = []string{ep}
|
||||
// The new name is not collision free but has very small possibility to collide.
|
||||
// TODO: rework this naming whenever we'll introduce a resource reference mechanism not based on a string.
|
||||
newRouters[ep+"-conflicted-"+name] = rt
|
||||
}
|
||||
|
||||
return deleted
|
||||
})
|
||||
|
||||
if len(router.EntryPoints) > 0 {
|
||||
newRouters[name] = router
|
||||
}
|
||||
}
|
||||
|
||||
return newRouters
|
||||
}
|
||||
|
||||
// findConflictingRouters returns the names of the routers, among the given
|
||||
// single-entryPoint routers, that serve a host (SNI) also served by another router
|
||||
// with a different resolved TLS option. Such routers are arbitrated by falling back
|
||||
// to the default TLS options.
|
||||
func findConflictingRouters(ep string, routers map[string]*dynamic.Router) []string {
|
||||
var conflicting []string
|
||||
|
||||
// For each host (SNI, already lower-cased by the domain parsing), the routers
|
||||
// serving it grouped by their resolved TLS option. A host with more than one
|
||||
// group is served with conflicting TLS options.
|
||||
routersByHostAndOption := map[string]map[string][]string{}
|
||||
|
||||
for name, router := range routers {
|
||||
if router.TLS == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
domains, err := httpmuxer.ParseDomains(router.Rule)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
rts[routerHTTPName].TLS.ResolvedOptions = "default"
|
||||
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
|
||||
// The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI,
|
||||
// so if it is not the default one, it is a conflict.
|
||||
if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName {
|
||||
conflicting = append(conflicting, name)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
// domain is already in lower case thanks to the domain parsing
|
||||
if tlsOptionsForHostSNI[domain] == nil {
|
||||
tlsOptionsForHostSNI[domain] = make(map[string][]string)
|
||||
if routersByHostAndOption[domain] == nil {
|
||||
routersByHostAndOption[domain] = map[string][]string{}
|
||||
}
|
||||
tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
|
||||
option := router.TLS.ResolvedOptions
|
||||
routersByHostAndOption[domain][option] = append(routersByHostAndOption[domain][option], name)
|
||||
}
|
||||
}
|
||||
|
||||
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
|
||||
if len(tlsConfigs) == 1 {
|
||||
for optionsName, v := range tlsConfigs {
|
||||
log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
|
||||
for _, s := range v {
|
||||
rts[s].TLS.ResolvedOptions = optionsName
|
||||
}
|
||||
}
|
||||
for domain, routersByOption := range routersByHostAndOption {
|
||||
if len(routersByOption) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// multiple tlsConfigs
|
||||
routers := make([]string, 0, len(tlsConfigs))
|
||||
for _, v := range tlsConfigs {
|
||||
for _, s := range v {
|
||||
rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
routers = append(routers, s)
|
||||
}
|
||||
var routersInConflict []string
|
||||
for _, names := range routersByOption {
|
||||
conflicting = append(conflicting, names...)
|
||||
routersInConflict = append(routersInConflict, names...)
|
||||
}
|
||||
|
||||
log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
|
||||
log.Error().Msgf("On EntryPoint %q, Host %q is served by multiple routers with different TLS options, default TLSOptions will be applied for the following routers: %v", ep, domain, routersInConflict)
|
||||
}
|
||||
|
||||
cfg.HTTP.Routers = rts
|
||||
return cfg
|
||||
return conflicting
|
||||
}
|
||||
|
||||
func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
@@ -1230,3 +1231,113 @@ func Test_applyModel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_resolveHTTPTLSOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers map[string]*dynamic.Router
|
||||
expected map[string]string // router name -> ResolvedOptions
|
||||
unexpectedRouters []string
|
||||
}{
|
||||
{
|
||||
desc: "same host, different options, different entryPoints: no conflict",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "optsA@file",
|
||||
"router-b@file": "optsB@file",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "same host, different options, same entryPoint: conflict falls back to default",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-router-a@file": "default",
|
||||
"ep-a-conflicted-router-b@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"router-a@file", "router-b@file"},
|
||||
},
|
||||
{
|
||||
desc: "same host, same options, same entryPoint: keeps the configured options",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "optsA@file",
|
||||
"router-b@file": "optsA@file",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "router spanning two entryPoints, conflict on one only: router is duplicated",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}},
|
||||
"other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a
|
||||
"shared@file": "optsX@file", // alone on ep-b
|
||||
"ep-a-conflicted-other@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"other@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, non-default options: forced to default and renamed",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"router-a@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, implicit default options: not conflicting, keeps its name",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, explicit default options: not conflicting, keeps its name",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{
|
||||
Options: "default",
|
||||
}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := resolveHTTPTLSOptions(test.routers)
|
||||
|
||||
for name, want := range test.expected {
|
||||
rt, ok := got[name]
|
||||
|
||||
require.True(t, ok, "router %q is missing", name)
|
||||
require.NotNil(t, rt.TLS, "router %q has no TLS config", name)
|
||||
assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints)
|
||||
}
|
||||
|
||||
for _, name := range test.unexpectedRouters {
|
||||
_, ok := got[name]
|
||||
require.False(t, ok, "router %q is present", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
|
||||
|
||||
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
|
||||
conf = applyModel(conf)
|
||||
conf = resolveHTTPTLSOptions(conf)
|
||||
if conf.HTTP != nil {
|
||||
conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers)
|
||||
}
|
||||
|
||||
for _, listener := range c.configurationListeners {
|
||||
listener(conf)
|
||||
|
||||
@@ -173,8 +173,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||
}
|
||||
|
||||
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
|
||||
logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.")
|
||||
routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
|
||||
routerHTTPConfig.AddError(errors.New("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"), false)
|
||||
}
|
||||
|
||||
// Even though the error is seemingly ignored (aside from logging it),
|
||||
|
||||
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
|
||||
OutputType = "file"
|
||||
FileName = "traefik_changelog.md"
|
||||
|
||||
# example new bugfix v3.7.4
|
||||
# example new bugfix v3.7.5
|
||||
CurrentRef = "v3.7"
|
||||
PreviousRef = "v3.7.3"
|
||||
PreviousRef = "v3.7.4"
|
||||
BaseBranch = "v3.7"
|
||||
FutureCurrentRefName = "v3.7.4"
|
||||
FutureCurrentRefName = "v3.7.5"
|
||||
|
||||
ThresholdPreviousRef = 10000
|
||||
ThresholdCurrentRef = 10000
|
||||
|
||||
Reference in New Issue
Block a user