diff --git a/pkg/provider/kubernetes/gateway/fixtures/grpcroute/with_backend_tls_policy.yml b/pkg/provider/kubernetes/gateway/fixtures/grpcroute/with_backend_tls_policy.yml index 2e583101a..aa4bbbfe4 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/grpcroute/with_backend_tls_policy.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/grpcroute/with_backend_tls_policy.yml @@ -64,6 +64,12 @@ spec: - group: core kind: ConfigMap name: ca-file-2 + - group: "" + kind: Secret + name: ca-file + - group: core + kind: Secret + name: ca-file-2 --- apiVersion: v1 @@ -82,3 +88,21 @@ metadata: namespace: default data: ca.crt: "CA2" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: ca-file + namespace: default +data: + ca.crt: Q0ExLXNlY3JldA== + +--- +apiVersion: v1 +kind: Secret +metadata: + name: ca-file-2 + namespace: default +data: + ca.crt: Q0EyLXNlY3JldA== diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy.yml new file mode 100644 index 000000000..6bbafb9de --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy.yml @@ -0,0 +1,124 @@ +--- +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: 9001 + hostname: foo.com + tls: + mode: Terminate # Default mode + 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: + - name: whoami + port: 80 + weight: 1 + +--- +kind: BackendTLSPolicy +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: policy-1 + namespace: default +spec: + targetRefs: + - group: "" + kind: Service + name: whoami + validation: + hostname: whoami + caCertificateRefs: + - group: "" + kind: ConfigMap + name: ca-file + - group: core + kind: ConfigMap + name: ca-file-2 + - group: "" + kind: Secret + name: ca-file + - group: core + kind: Secret + name: ca-file-2 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ca-file + namespace: default +data: + ca.crt: "CA1" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ca-file-2 + namespace: default +data: + ca.crt: "CA2" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: ca-file + namespace: default +data: + ca.crt: Q0ExLXNlY3JldA== + +--- +apiVersion: v1 +kind: Secret +metadata: + name: ca-file-2 + namespace: default +data: + ca.crt: Q0EyLXNlY3JldA== diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy_system.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy_system.yml new file mode 100644 index 000000000..8d7368a2e --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_backend_tls_policy_system.yml @@ -0,0 +1,78 @@ +--- +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: 9001 + hostname: foo.com + tls: + mode: Terminate # Default mode + 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: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + +--- +kind: BackendTLSPolicy +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: policy-1 + namespace: default +spec: + targetRefs: + - group: core + kind: Service + name: whoami + validation: + hostname: whoami + wellKnownCACertificates: System diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 28a49a539..77cb35f08 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -606,7 +606,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend } for _, caCertRef := range policy.Spec.Validation.CACertificateRefs { - if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") { + if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) { return nil, metav1.Condition{ Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -619,7 +619,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend var caCRT string switch caCertRef.Kind { - case "ConfigMap": + case kindConfigMap: configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name)) if err != nil { return nil, metav1.Condition{ @@ -632,7 +632,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend } } caCRT = configmap.Data["ca.crt"] - case "Secret": + case kindSecret: secret, err := p.client.GetSecret(namespace, string(caCertRef.Name)) if err != nil { return nil, metav1.Condition{ diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 235fa4e9e..d48a376d8 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -49,6 +49,8 @@ const ( kindTCPRoute = "TCPRoute" kindTLSRoute = "TLSRoute" kindService = "Service" + kindConfigMap = "ConfigMap" + kindSecret = "Secret" appProtocolHTTP = "http" appProtocolHTTPS = "https" @@ -591,7 +593,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat var errCertConditions []metav1.Condition listenerTLSCerts := make(map[string]*tls.CertAndStores) for _, certificateRef := range listener.TLS.CertificateRefs { - if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) { + if certificateRef.Kind == nil || *certificateRef.Kind != kindSecret || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) { errCertConditions = append(errCertConditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -604,7 +606,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat } certificateNamespace := string(ptr.Deref(certificateRef.Namespace, gatev1.Namespace(gateway.Namespace))) - if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil { + if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, kindSecret, string(certificateRef.Name), certificateNamespace); err != nil { errCertConditions = append(errCertConditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 651628969..88535974f 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -3579,6 +3579,8 @@ func TestLoadGRPCRoutes(t *testing.T) { RootCAs: []types.FileOrContent{ "CA1", "CA2", + "CA1-secret", + "CA2-secret", }, }, }, @@ -6156,6 +6158,178 @@ func TestLoadTLSRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Simple TLSRoute and BackendTLSPolicy with CA certificate", + paths: []string{"services.yml", "tlsroute/with_backend_tls_policy.yml"}, + entryPoints: map[string]Entrypoint{"tls": { + Address: ":9001", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "deny-unknown-host": { + Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)", + Priority: 1, + Service: "deny-unknown-host", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": { + EntryPoints: []string{"tls"}, + Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr", + Rule: `HostSNI("foo.com")`, + Priority: 7, + RuleSyntax: "default", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "deny-unknown-host": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{}, + }, + "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": { + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{ + { + Name: "default-whoami-80", + Weight: ptr.To(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.1:80", + }, + { + Address: "10.10.0.2:80", + }, + }, + ServersTransport: "default-whoami-80", + }, + }, + }, + ServersTransports: map[string]*dynamic.TCPServersTransport{ + "default-whoami-80": { + TLS: &dynamic.TLSClientConfig{ + ServerName: "whoami", + RootCAs: []types.FileOrContent{ + "CA1", + "CA2", + "CA1-secret", + "CA2-secret", + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: types.FileOrContent(listenerCert), + KeyFile: types.FileOrContent(listenerKey), + }, + }, + }, + }, + }, + }, + { + desc: "Simple TLSRoute and BackendTLSPolicy with System CA", + paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_system.yml"}, + entryPoints: map[string]Entrypoint{"tls": { + Address: ":9001", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "deny-unknown-host": { + Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)", + Priority: 1, + Service: "deny-unknown-host", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": { + EntryPoints: []string{"tls"}, + Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr", + Rule: `HostSNI("foo.com")`, + Priority: 7, + RuleSyntax: "default", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "deny-unknown-host": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{}, + }, + "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": { + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{ + { + Name: "default-whoami-80", + Weight: ptr.To(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.1:80", + }, + { + Address: "10.10.0.2:80", + }, + }, + ServersTransport: "default-whoami-80", + }, + }, + }, + ServersTransports: map[string]*dynamic.TCPServersTransport{ + "default-whoami-80": { + TLS: &dynamic.TLSClientConfig{ + ServerName: "whoami", + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: types.FileOrContent(listenerCert), + KeyFile: types.FileOrContent(listenerKey), + }, + }, + }, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go index 3a9632a7d..071d64a63 100644 --- a/pkg/provider/kubernetes/gateway/tcproute.go +++ b/pkg/provider/kubernetes/gateway/tcproute.go @@ -347,4 +347,9 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) { to.TCP.Services = map[string]*dynamic.TCPService{} } maps.Copy(to.TCP.Services, from.TCP.Services) + + if to.TCP.ServersTransports == nil { + to.TCP.ServersTransports = map[string]*dynamic.TCPServersTransport{} + } + maps.Copy(to.TCP.ServersTransports, from.TCP.ServersTransports) } diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go index 9b660d449..c9d4f0040 100644 --- a/pkg/provider/kubernetes/gateway/tlsroute.go +++ b/pkg/provider/kubernetes/gateway/tlsroute.go @@ -5,12 +5,14 @@ import ( "fmt" "net" "regexp" + "slices" "strconv" "strings" "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/provider" + "github.com/traefik/traefik/v3/pkg/types" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ktypes "k8s.io/apimachinery/pkg/types" @@ -73,7 +75,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway } } - routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames) + routeConf, resolveRefCondition := p.loadTLSRoute(ctx, listener, route, hostnames) if accepted && listener.Attached { mergeTCPConfiguration(routeConf, conf) } @@ -110,7 +112,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway } } -func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) { +func (p *Provider) loadTLSRoute(ctx context.Context, listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) { conf := &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: make(map[string]*dynamic.TCPRouter), @@ -171,7 +173,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute } var serviceCondition *metav1.Condition - router.Service, serviceCondition = p.loadTLSWRRService(conf, routerName, routeRule.BackendRefs, route) + router.Service, serviceCondition = p.loadTLSWRRService(ctx, listener, conf, routerName, routeRule.BackendRefs, route) if serviceCondition != nil { condition = *serviceCondition } @@ -183,7 +185,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute } // loadTLSWRRService is generating a WRR service, even when there is only one target. -func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) { +func (p *Provider) loadTLSWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) { name := routeKey + "-wrr" if _, ok := conf.TCP.Services[name]; ok { return name, nil @@ -192,7 +194,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin var wrr dynamic.TCPWeightedRoundRobin var condition *metav1.Condition for _, backendRef := range backendRefs { - svcName, svc, errCondition := p.loadTLSService(route, backendRef) + svcName, svc, errCondition := p.loadTLSService(ctx, listener, conf, route, backendRef) weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) if errCondition != nil { @@ -226,7 +228,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin return name, condition } -func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) { +func (p *Provider) loadTLSService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) { kind := ptr.Deref(backendRef.Kind, kindService) group := groupCore @@ -283,18 +285,23 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back portStr := strconv.FormatInt(int64(port), 10) serviceName = provider.Normalize(serviceName + "-" + portStr) - lb, errCondition := p.loadTLSServers(namespace, route, backendRef) + lb, st, errCondition := p.loadTLSServers(ctx, namespace, route, backendRef, listener) if errCondition != nil { return serviceName, nil, errCondition } + if st != nil { + lb.ServersTransport = serviceName + conf.TCP.ServersTransports[serviceName] = st + } + return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil } -func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) { +func (p *Provider) loadTLSServers(ctx context.Context, namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, listener gatewayListener) (*dynamic.TCPServersLoadBalancer, *dynamic.TCPServersTransport, *metav1.Condition) { backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef) if err != nil { - return nil, &metav1.Condition{ + return nil, nil, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.GetGeneration(), @@ -304,8 +311,126 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back } } + backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name)) + if err != nil { + return nil, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err), + } + } + + // Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements. + slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int { + cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time) + if cmpTime == 0 { + return strings.Compare(a.Name, b.Name) + } + return cmpTime + }) + + var serversTransport *dynamic.TCPServersTransport + for _, policy := range backendTLSPolicies { + for _, targetRef := range policy.Spec.TargetRefs { + // Skip targetRefs that doesn't match the backendRef, + // since a BackendTLSPolicy can select multiple services. + if targetRef.Name != backendRef.Name { + continue + } + // Skip the targetRef if the sectionName doesn't match the backendRef port. + if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) { + continue + } + + policyAncestorStatus := gatev1.PolicyAncestorStatus{ + AncestorRef: gatev1.ParentReference{ + Group: ptr.To(gatev1.Group(groupGateway)), + Kind: ptr.To(gatev1.Kind(kindGateway)), + Namespace: ptr.To(gatev1.Namespace(namespace)), + Name: gatev1.ObjectName(listener.GWName), + SectionName: ptr.To(gatev1.SectionName(listener.Name)), + }, + ControllerName: controllerName, + } + + // Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict. + if serversTransport != nil { + policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, + metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs), + }, + metav1.Condition{ + Type: string(gatev1.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.PolicyReasonConflicted), + }, + ) + + status := gatev1.PolicyStatus{ + Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus}, + } + if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil { + log.Ctx(ctx).Warn().Err(err).Msg("Unable to update conflicting BackendTLSPolicy status") + } + + continue + } + + var resolvedRefCondition metav1.Condition + serversTransport, resolvedRefCondition = p.loadTCPServersTransport(namespace, policy) + + policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition) + if resolvedRefCondition.Status == metav1.ConditionFalse { + policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{ + Type: string(gatev1.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate), + }) + } else { + policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{ + Type: string(gatev1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.PolicyReasonAccepted), + }) + } + + status := gatev1.PolicyStatus{ + Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus}, + } + if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil { + log.Ctx(ctx).Warn().Err(err).Msg("Unable to update BackendTLSPolicy status") + } + + // When something went wrong during the loading of a ServersTransport, + // we stop here and return a route condition error. + if resolvedRefCondition.Status == metav1.ConditionFalse { + return nil, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message), + } + } + } + } + if svcPort.Protocol != corev1.ProtocolTCP { - return nil, &metav1.Condition{ + return nil, nil, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.GetGeneration(), @@ -323,7 +448,89 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back Address: net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port))), }) } - return lb, nil + return lb, serversTransport, nil +} + +func (p *Provider) loadTCPServersTransport(namespace string, policy *gatev1.BackendTLSPolicy) (*dynamic.TCPServersTransport, metav1.Condition) { + st := &dynamic.TCPServersTransport{ + TLS: &dynamic.TLSClientConfig{ + ServerName: string(policy.Spec.Validation.Hostname), + }, + } + + if policy.Spec.Validation.WellKnownCACertificates != nil { + return st, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs), + } + } + + for _, caCertRef := range policy.Spec.Validation.CACertificateRefs { + if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) { + return nil, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind), + Message: "Only ConfigMaps and Secrets are supported", + } + } + + var caCRT string + switch caCertRef.Kind { + case kindConfigMap: + configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name)) + if err != nil { + return nil, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef), + Message: fmt.Sprintf("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err), + } + } + caCRT = configmap.Data["ca.crt"] + case kindSecret: + secret, err := p.client.GetSecret(namespace, string(caCertRef.Name)) + if err != nil { + return nil, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef), + Message: fmt.Sprintf("getting secret %s/%s: %s", namespace, string(caCertRef.Name), err), + } + } + caCRT = string(secret.Data["ca.crt"]) + } + + if caCRT == "" { + return nil, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef), + Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)), + } + } + + st.TLS.RootCAs = append(st.TLS.RootCAs, types.FileOrContent(caCRT)) + } + + return st, metav1.Condition{ + Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: policy.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs), + } } func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {