From 0677eca60efaf38fc0acfcd2a213e4bc03d78af3 Mon Sep 17 00:00:00 2001 From: "asif.hasnain@sap.com" Date: Thu, 9 Jul 2026 20:21:28 +0200 Subject: [PATCH 1/3] feat(translator): Security policies for TLS gateways Signed-off-by: asif.hasnain@sap.com --- api/v1alpha1/securitypolicy_types.go | 10 +- ...ateway.envoyproxy.io_securitypolicies.yaml | 8 +- ...ateway.envoyproxy.io_securitypolicies.yaml | 8 +- internal/gatewayapi/securitypolicy.go | 41 +++-- internal/gatewayapi/securitypolicy_test.go | 2 +- ...ritypolicy-with-merge-tcp-invalid.out.yaml | 2 +- .../provider/kubernetes/controller_test.go | 2 +- .../in/xds-ir/tls-route-authorization.yaml | 59 +++++++ .../tls-route-authorization.clusters.yaml | 46 ++++++ .../tls-route-authorization.endpoints.yaml | 24 +++ .../tls-route-authorization.listeners.yaml | 120 ++++++++++++++ .../tls-route-authorization.routes.yaml | 1 + site/content/en/latest/api/extension_types.md | 6 +- .../tlsroute-authorization-client-ip.yaml | 85 ++++++++++ .../tests/tlsroute_authorization_client_ip.go | 152 ++++++++++++++++++ test/helm/gateway-crds-helm/all.out.yaml | 8 +- test/helm/gateway-crds-helm/e2e.out.yaml | 8 +- .../envoy-gateway-crds.out.yaml | 8 +- 18 files changed, 544 insertions(+), 46 deletions(-) create mode 100644 internal/xds/translator/testdata/in/xds-ir/tls-route-authorization.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.routes.yaml create mode 100644 test/e2e/testdata/tlsroute-authorization-client-ip.yaml create mode 100644 test/e2e/tests/tlsroute_authorization_client_ip.go diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index 033291063a..f314e8cf90 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -37,18 +37,18 @@ type SecurityPolicy struct { // SecurityPolicySpec defines the desired state of SecurityPolicy. // -// NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, and TCPRoute. -// When a SecurityPolicy targets a TCPRoute, only client-IP CIDR based authorization +// NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, TCPRoute, and TLSRoute. +// When a SecurityPolicy targets a TCPRoute or TLSRoute, only client-IP CIDR based authorization // (Authorization rules that use Principal.ClientCIDRs) is applied. Other // authentication/authorization features such as JWT, API Key, Basic Auth, // OIDC, External Authorization, or GeoIP based authorization are not applicable -// to TCPRoute targets. +// to TCPRoute or TLSRoute targets. // // +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used" // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true", message="this policy can only have a targetRef.group of gateway.networking.k8s.io" -// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute" +// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute" // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == 'gateway.networking.k8s.io') : true ", message="this policy can only have a targetRefs[*].group of gateway.networking.k8s.io" -// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute" +// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute" // +kubebuilder:validation:XValidation:rule="(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) : false)) ? has(self.jwt) : true", message="if authorization.rules.principal.jwt is used, jwt must be defined" type SecurityPolicySpec struct { PolicyTargetReferences `json:",inline"` diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml index 6eba7e7eb4..5466282e5f 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -7479,15 +7479,15 @@ spec: - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' - - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', - ''GRPCRoute'', ''TCPRoute''] : true' + ''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true' - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == ''gateway.networking.k8s.io'') : true ' - - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', - ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true ' + ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true ' - message: if authorization.rules.principal.jwt is used, jwt must be defined rule: '(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 3f1ec675ff..c6afe07cce 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -7478,15 +7478,15 @@ spec: - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' - - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', - ''GRPCRoute'', ''TCPRoute''] : true' + ''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true' - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == ''gateway.networking.k8s.io'') : true ' - - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', - ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true ' + ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true ' - message: if authorization.rules.principal.jwt is used, jwt must be defined rule: '(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 6e796e21ae..a79bcf80e4 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -374,9 +374,9 @@ func (t *Translator) processSecurityPolicyForRoute( // then run it once to keep the flow linear and easier to read. validator := validateSecurityPolicy errMsg := "invalid SecurityPolicy" - if currTarget.Kind == resource.KindTCPRoute { - validator = validateSecurityPolicyForTCP - errMsg = "invalid SecurityPolicy for TCP route" + if currTarget.Kind == resource.KindTCPRoute || currTarget.Kind == resource.KindTLSRoute { + validator = validateSecurityPolicyForTCPAndTLS + errMsg = "invalid SecurityPolicy for TCP/TLS route" } if err := validator(policy); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, @@ -660,16 +660,16 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error { return nil } -// validateSecurityPolicyForTCP ensures SecurityPolicy usage on TCP is compatible. +// validateSecurityPolicyForTCPAndTLS ensures SecurityPolicy usage on TCP/TLS is compatible. // -// TCP supports Authorization with ClientCIDRs ONLY. +// TCP and TLS support Authorization with ClientCIDRs ONLY. // - Principals.JWT => invalid (HTTP-only) // - Principals.Headers => invalid (HTTP-only) -// - Empty/no Authorization is allowed and results in no-op on TCP. +// - Empty/no Authorization is allowed and results in no-op on TCP/TLS. // Returns an error when any HTTP-only field is present or CIDRs are invalid. -func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error { +func validateSecurityPolicyForTCPAndTLS(p *egv1a1.SecurityPolicy) error { if p.Spec.CORS != nil || p.Spec.JWT != nil || p.Spec.OIDC != nil || p.Spec.APIKeyAuth != nil || p.Spec.BasicAuth != nil || p.Spec.ExtAuth != nil { - return fmt.Errorf("only authorization is supported for TCP (routes/listeners)") + return fmt.Errorf("only authorization is supported for TCP/TLS (routes/listeners)") } if p.Spec.Authorization == nil || len(p.Spec.Authorization.Rules) == 0 { return nil @@ -683,13 +683,13 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error { continue } if rule.Principal.JWT != nil { - return fmt.Errorf("rule %d: JWT not supported for TCP", i) + return fmt.Errorf("rule %d: JWT not supported for TCP/TLS", i) } if len(rule.Principal.Headers) > 0 { - return fmt.Errorf("rule %d: headers not supported for TCP", i) + return fmt.Errorf("rule %d: headers not supported for TCP/TLS", i) } if len(rule.Principal.ClientIPGeoLocations) > 0 { - return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP", i) + return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP/TLS", i) } if err := validateCIDRs(rule.Principal.ClientCIDRs); err != nil { return fmt.Errorf("rule %d: %w", i, err) @@ -991,16 +991,26 @@ func (t *Translator) translateSecurityPolicyForRoute( irKey := t.getIRKey(gtwCtx.Gateway) switch route.GetRouteType() { - case resource.KindTCPRoute: + case resource.KindTCPRoute, resource.KindTLSRoute: for _, listener := range parentRefCtx.listeners { // If policyTargetListener is set, only apply to the specific listener if policyTargetListener != nil && *policyTargetListener != listener.Name { continue } tl := xdsIR[irKey].GetTCPListener(irListenerName(listener)) + for _, r := range tl.Routes { + // For TLSRoute: filter by route metadata to handle multiple routes per listener + if target.Kind == resource.KindTLSRoute { + if r.Metadata == nil || + r.Metadata.Namespace != string(target.Namespace) || + r.Metadata.Name != string(target.Name) { + continue + } + } + // If target.SectionName is specified it must match the route-rule section name - // in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP + // in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP/TLS // routes the section name is currently stored on r.Destination.Metadata.SectionName. if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName { continue @@ -1009,7 +1019,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if r.Authorization != nil { continue } - // Only authorization for TCP + // Only authorization for TCP/TLS if authorization != nil { authCopy := *authorization r.Authorization = &authCopy @@ -1278,7 +1288,8 @@ func (t *Translator) translateSecurityPolicyForGateway( tcpAuthorization = &authCopy } - // Apply to TCP listeners (Authorization only). + // Apply to TCP/TLS listeners (Authorization only). + // Both TCPRoute and TLSRoute translate to TCP listeners in the IR. if tcpAuthorization != nil { for _, tl := range x.TCP { if tl == nil || len(tl.Routes) == 0 { diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 223e0dea4e..fe8a96e140 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1326,7 +1326,7 @@ func Test_validateSecurityPolicyForTCP_Table(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { p := &egv1a1.SecurityPolicy{Spec: tc.spec} - err := validateSecurityPolicyForTCP(p) + err := validateSecurityPolicyForTCPAndTLS(p) if tc.wantErr { require.Error(t, err) } else { diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml index 1934ce02ea..f0baab72e8 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml @@ -85,7 +85,7 @@ securityPolicies: sectionName: tcp conditions: - lastTransitionTime: null - message: Only authorization is supported for TCP (routes/listeners). + message: Only authorization is supported for TCP/TLS (routes/listeners). reason: Invalid status: "False" type: Accepted diff --git a/internal/provider/kubernetes/controller_test.go b/internal/provider/kubernetes/controller_test.go index 7ad0c2c5d5..9fdb9c2de4 100644 --- a/internal/provider/kubernetes/controller_test.go +++ b/internal/provider/kubernetes/controller_test.go @@ -2447,7 +2447,7 @@ func TestProcessPolicyTargetReferenceGrants(t *testing.T) { refGrant("sp-tcp-route", routeNS, resource.KindSecurityPolicy, policyNS, gwapiv1.GroupName, resource.KindTCPRoute), refGrant("sp-tls-route", routeNS, resource.KindSecurityPolicy, policyNS, gwapiv1.GroupName, resource.KindTLSRoute), }, - expectedNames: sets.New("sp-gateway", "sp-http-route", "sp-grpc-route", "sp-tcp-route"), + expectedNames: sets.New("sp-gateway", "sp-http-route", "sp-grpc-route", "sp-tcp-route", "sp-tls-route"), }, { name: "ignores non matching grants", diff --git a/internal/xds/translator/testdata/in/xds-ir/tls-route-authorization.yaml b/internal/xds/translator/testdata/in/xds-ir/tls-route-authorization.yaml new file mode 100644 index 0000000000..7482b8612d --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/tls-route-authorization.yaml @@ -0,0 +1,59 @@ +tcp: +- name: "tls-listener-authorization" + address: "0.0.0.0" + port: 8443 + routes: + - name: "tls-route-blocked" + tls: + inspector: + snis: + - "blocked.example.com" + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-office + principal: + clientCIDRs: + - cidr: 192.168.254.0/24 + distinct: false + isIPv6: false + maskLen: 24 + destination: + name: "tls-backend-blocked" + settings: + - endpoints: + - host: "10.0.1.1" + port: 443 + name: "tls-backend-blocked/backend/0" + - name: "tls-route-allowed" + tls: + inspector: + snis: + - "allowed.example.com" + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-all-ipv4 + principal: + clientCIDRs: + - cidr: 0.0.0.0/0 + distinct: false + isIPv6: false + maskLen: 0 + - action: Allow + name: allow-all-ipv6 + principal: + clientCIDRs: + - cidr: ::/0 + distinct: false + isIPv6: true + maskLen: 0 + destination: + name: "tls-backend-allowed" + settings: + - endpoints: + - host: "10.0.1.2" + port: 443 + name: "tls-backend-allowed/backend/0" diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.clusters.yaml new file mode 100644 index 0000000000..659a279ddd --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.clusters.yaml @@ -0,0 +1,46 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: tls-backend-blocked + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: tls-backend-blocked + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: tls-backend-allowed + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: tls-backend-allowed + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.endpoints.yaml new file mode 100644 index 0000000000..9a89dfcc0d --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: tls-backend-blocked + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.0.1.1 + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: tls-backend-blocked/backend/0 +- clusterName: tls-backend-allowed + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.0.1.2 + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: tls-backend-allowed/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.listeners.yaml new file mode 100644 index 0000000000..4391379009 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.listeners.yaml @@ -0,0 +1,120 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 8443 + filterChains: + - filterChainMatch: + serverNames: + - blocked.example.com + filters: + - name: envoy.filters.network.rbac + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC + matcher: + matcherList: + matchers: + - onMatch: + action: + name: allow-office + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + name: ALLOW + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 192.168.254.0 + prefixLen: 24 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + onNoMatch: + action: + name: default + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY + statPrefix: tls-passthrough-8443 + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: tls-backend-blocked + statPrefix: tls-passthrough-8443 + name: tls-route-blocked + - filterChainMatch: + serverNames: + - allowed.example.com + filters: + - name: envoy.filters.network.rbac + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC + matcher: + matcherList: + matchers: + - onMatch: + action: + name: allow-all-ipv4 + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + name: ALLOW + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 0.0.0.0 + prefixLen: 0 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - onMatch: + action: + name: allow-all-ipv6 + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + name: ALLOW + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: '::' + prefixLen: 0 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + onNoMatch: + action: + name: default + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY + statPrefix: tls-passthrough-8443 + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: tls-backend-allowed + statPrefix: tls-passthrough-8443 + name: tls-route-allowed + listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + maxConnectionsToAcceptPerSocketEvent: 1 + name: tls-listener-authorization + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.routes.yaml new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-authorization.routes.yaml @@ -0,0 +1 @@ +[] diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 8c78804007..6e7f972d3a 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -5781,12 +5781,12 @@ Gateway. SecurityPolicySpec defines the desired state of SecurityPolicy. -NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, and TCPRoute. -When a SecurityPolicy targets a TCPRoute, only client-IP CIDR based authorization +NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, TCPRoute, and TLSRoute. +When a SecurityPolicy targets a TCPRoute or TLSRoute, only client-IP CIDR based authorization (Authorization rules that use Principal.ClientCIDRs) is applied. Other authentication/authorization features such as JWT, API Key, Basic Auth, OIDC, External Authorization, or GeoIP based authorization are not applicable -to TCPRoute targets. +to TCPRoute or TLSRoute targets. _Appears in:_ - [SecurityPolicy](#securitypolicy) diff --git a/test/e2e/testdata/tlsroute-authorization-client-ip.yaml b/test/e2e/testdata/tlsroute-authorization-client-ip.yaml new file mode 100644 index 0000000000..8c80f08c8a --- /dev/null +++ b/test/e2e/testdata/tlsroute-authorization-client-ip.yaml @@ -0,0 +1,85 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: tls-authorization-backend + namespace: gateway-conformance-infra +spec: + gatewayClassName: "envoy-gateway" + listeners: + - name: tls-passthrough + protocol: TLS + port: 8443 + tls: + mode: Passthrough + allowedRoutes: + kinds: + - kind: TLSRoute +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-backend-authorization-blocked + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: tls-authorization-backend + sectionName: tls-passthrough + hostnames: + - "blocked.example.com" + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-backend-authorization-allowed + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: tls-authorization-backend + sectionName: tls-passthrough + hostnames: + - "allowed.example.com" + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: tls-backend-authorization-blocked-security-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: TLSRoute + name: tls-backend-authorization-blocked + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 192.168.254.0/24 # This CIDR won't match test client +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: tls-backend-authorization-allowed-security-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: TLSRoute + name: tls-backend-authorization-allowed + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 0.0.0.0/0 # Allow all IPv4 + - ::/0 # Allow all IPv6 diff --git a/test/e2e/tests/tlsroute_authorization_client_ip.go b/test/e2e/tests/tlsroute_authorization_client_ip.go new file mode 100644 index 0000000000..63c32cfb91 --- /dev/null +++ b/test/e2e/tests/tlsroute_authorization_client_ip.go @@ -0,0 +1,152 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "crypto/tls" + "errors" + "io" + "net" + "testing" + "time" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, TLSRouteAuthzWithClientIP) +} + +var TLSRouteAuthzWithClientIP = suite.ConformanceTest{ + ShortName: "TLSRouteAuthzWithClientIP", + Description: "Authorization with client IP Allow/Deny list for TLS routes (passthrough)", + Manifests: []string{"testdata/tlsroute-authorization-client-ip.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + tlsRouteNNBlocked := types.NamespacedName{Name: "tls-backend-authorization-blocked", Namespace: ns} + tlsRouteNNAllowed := types.NamespacedName{Name: "tls-backend-authorization-allowed", Namespace: ns} + gwNN := types.NamespacedName{Name: "tls-authorization-backend", Namespace: ns} + gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN, "tls-passthrough"), tlsRouteNNBlocked, tlsRouteNNAllowed) + + // SecurityPolicy status.ancestors references the Gateway listener, not the route itself + // This matches how TCPRoute authorization tests work + tlsPassthroughSection := gwapiv1.SectionName("tls-passthrough") + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + SectionName: &tlsPassthroughSection, + } + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "tls-backend-authorization-blocked-security-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "tls-backend-authorization-allowed-security-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("blocked client IP cannot connect to blocked.example.com", func(t *testing.T) { + testTLSRouteConnectionBlocked(t, gwAddr, "blocked.example.com") + }) + + t.Run("allowed client IP can connect to allowed.example.com", func(t *testing.T) { + testTLSRouteConnectionAllowed(t, gwAddr, "allowed.example.com") + }) + }, +} + +func testTLSRouteConnectionBlocked(t *testing.T, gwAddr, hostname string) { + // Try to establish a TLS connection with SNI + tlsConfig := &tls.Config{ + ServerName: hostname, + InsecureSkipVerify: true, //nolint:gosec + } + + conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", gwAddr, tlsConfig) + if err != nil { + t.Logf("Connection blocked as expected: %v", err) + return + } + defer conn.Close() + + // If connection was established, try sending data + req := "GET / HTTP/1.1\r\nHost: " + hostname + "\r\nUser-Agent: test-client\r\nAccept: */*\r\n\r\n" + _, err = conn.Write([]byte(req)) + if err != nil { + t.Logf("Connection blocked during write as expected: %v", err) + return + } + + // Try to read response with a short timeout + if err := conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil { + t.Logf("Failed to set read deadline: %v", err) + return + } + buf := make([]byte, 1024) + n, err := conn.Read(buf) + + if errors.Is(err, io.EOF) || n == 0 { + t.Log("Got empty reply from server as expected (connection blocked)") + return + } + + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Log("Connection timed out as expected (connection blocked)") + return + } + if err != nil { + t.Logf("Connection blocked with error as expected: %v", err) + return + } + + // If we got here, we received some data, which means the connection was NOT blocked + response := string(buf[:n]) + t.Fatalf("Expected connection to be blocked, but got response: %s", response) +} + +func testTLSRouteConnectionAllowed(t *testing.T, gwAddr, hostname string) { + // Establish a TLS connection with SNI + tlsConfig := &tls.Config{ + ServerName: hostname, + InsecureSkipVerify: true, //nolint:gosec + } + + conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", gwAddr, tlsConfig) + if err != nil { + t.Fatalf("Failed to establish TLS connection (should be allowed): %v", err) + } + defer conn.Close() + + // Send a simple HTTP request + req := "GET / HTTP/1.1\r\nHost: " + hostname + "\r\nUser-Agent: test-client\r\nAccept: */*\r\n\r\n" + _, err = conn.Write([]byte(req)) + if err != nil { + t.Fatalf("Failed to write data (should be allowed): %v", err) + } + + // Try to read response + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("Failed to set read deadline: %v", err) + } + buf := make([]byte, 1024) + n, err := conn.Read(buf) + + if err != nil && !errors.Is(err, io.EOF) { + t.Fatalf("Failed to read response (should be allowed): %v", err) + } + + if n > 0 { + t.Logf("Successfully received response: %s", string(buf[:n])) + } else { + t.Log("Connection allowed and completed successfully") + } +} diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 84c0c0a573..3633892fcb 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -59876,15 +59876,15 @@ spec: - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' - - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', - ''GRPCRoute'', ''TCPRoute''] : true' + ''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true' - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == ''gateway.networking.k8s.io'') : true ' - - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', - ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true ' + ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true ' - message: if authorization.rules.principal.jwt is used, jwt must be defined rule: '(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 942eab31a4..732adb43e5 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -35814,15 +35814,15 @@ spec: - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' - - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', - ''GRPCRoute'', ''TCPRoute''] : true' + ''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true' - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == ''gateway.networking.k8s.io'') : true ' - - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', - ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true ' + ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true ' - message: if authorization.rules.principal.jwt is used, jwt must be defined rule: '(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 276e43f622..d2a4d06107 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -35814,15 +35814,15 @@ spec: - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' - - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', - ''GRPCRoute'', ''TCPRoute''] : true' + ''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true' - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == ''gateway.networking.k8s.io'') : true ' - - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute + - message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', - ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true ' + ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true ' - message: if authorization.rules.principal.jwt is used, jwt must be defined rule: '(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) From 0c9e46475343747451b7bdd3f0b8c011995bf9dd Mon Sep 17 00:00:00 2001 From: "asif.hasnain@sap.com" Date: Tue, 14 Jul 2026 14:22:24 +0200 Subject: [PATCH 2/3] adding tests and a release note Signed-off-by: asif.hasnain@sap.com --- ...icy-with-authorization-client-cidr.in.yaml | 87 +++++ ...cy-with-authorization-client-cidr.out.yaml | 327 ++++++++++++++++++ ...securitypolicy-with-multiple-rules.in.yaml | 59 ++++ ...ecuritypolicy-with-multiple-rules.out.yaml | 198 +++++++++++ .../9464-tlsroute-securitypolicy-support.md | 1 + .../tests/tlsroute_authorization_client_ip.go | 48 +++ 6 files changed, 720 insertions(+) create mode 100644 internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.in.yaml create mode 100644 internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.out.yaml create mode 100644 internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.in.yaml create mode 100644 internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.out.yaml create mode 100644 release-notes/current/new_features/9464-tlsroute-securitypolicy-support.md diff --git a/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.in.yaml b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.in.yaml new file mode 100644 index 0000000000..623077f1e6 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.in.yaml @@ -0,0 +1,87 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-tls + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 8443 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-a + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-tls + sectionName: tls-passthrough + hostnames: + - "a.example.com" + rules: + - backendRefs: + - name: service-1 + port: 8443 +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-b + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-tls + sectionName: tls-passthrough + hostnames: + - "b.example.com" + rules: + - backendRefs: + - name: service-2 + port: 8443 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-tlsroute-a + spec: + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-a + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-a + principal: + clientCIDRs: + - 192.168.0.0/16 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-tlsroute-b + spec: + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-b + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-b + principal: + clientCIDRs: + - 10.0.0.0/8 diff --git a/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.out.yaml b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.out.yaml new file mode 100644 index 0000000000..5c984f4726 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-authorization-client-cidr.out.yaml @@ -0,0 +1,327 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-tls + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: tls-passthrough + port: 8443 + protocol: TLS + tls: + mode: Passthrough + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tls-passthrough + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute +infraIR: + envoy-gateway/gateway-tls: + proxy: + listeners: + - name: envoy-gateway/gateway-tls/tls-passthrough + ports: + - containerPort: 8443 + name: tls-8443 + protocol: TLS + servicePort: 8443 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-tls + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-tls + namespace: envoy-gateway-system +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-tlsroute-a + namespace: default + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-a + principal: + clientCIDRs: + - 192.168.0.0/16 + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-a + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-tlsroute-b + namespace: default + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-b + principal: + clientCIDRs: + - 10.0.0.0/8 + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-b + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + name: tlsroute-a + namespace: default + spec: + hostnames: + - a.example.com + parentRefs: + - name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + rules: + - backendRefs: + - name: service-1 + port: 8443 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + name: tlsroute-b + namespace: default + spec: + hostnames: + - b.example.com + parentRefs: + - name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + rules: + - backendRefs: + - name: service-2 + port: 8443 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough +xdsIR: + envoy-gateway/gateway-tls: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-tls-e5408cf3 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-tls + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-tls-e5408cf3 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-tls + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + tcp: + - address: 0.0.0.0 + externalPort: 8443 + metadata: + kind: Gateway + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + name: envoy-gateway/gateway-tls/tls-passthrough + port: 8443 + routes: + - authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-a + principal: + clientCIDRs: + - cidr: 192.168.0.0/16 + distinct: false + invert: false + isIPv6: false + maskLen: 16 + destination: + metadata: + kind: TLSRoute + name: tlsroute-a + namespace: default + name: tlsroute/default/tlsroute-a/rule/-1 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8443 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8443" + name: tlsroute/default/tlsroute-a/rule/-1/backend/0 + protocol: TCP + weight: 1 + metadata: + kind: TLSRoute + name: tlsroute-a + namespace: default + name: tlsroute/default/tlsroute-a + tls: + inspector: + snis: + - a.example.com + - authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-b + principal: + clientCIDRs: + - cidr: 10.0.0.0/8 + distinct: false + invert: false + isIPv6: false + maskLen: 8 + destination: + metadata: + kind: TLSRoute + name: tlsroute-b + namespace: default + name: tlsroute/default/tlsroute-b/rule/-1 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8443 + metadata: + kind: Service + name: service-2 + namespace: default + sectionName: "8443" + name: tlsroute/default/tlsroute-b/rule/-1/backend/0 + protocol: TCP + weight: 1 + metadata: + kind: TLSRoute + name: tlsroute-b + namespace: default + name: tlsroute/default/tlsroute-b + tls: + inspector: + snis: + - b.example.com diff --git a/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.in.yaml b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.in.yaml new file mode 100644 index 0000000000..94fad52448 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.in.yaml @@ -0,0 +1,59 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-tls + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 8443 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-multiple-rules + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-tls + sectionName: tls-passthrough + hostnames: + - "*.example.com" + rules: + - name: db-traffic + backendRefs: + - name: database-service + port: 3306 + - name: api-traffic + backendRefs: + - name: api-service + port: 8443 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-db-traffic-only + spec: + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-multiple-rules + sectionName: db-traffic + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-db-subnet + principal: + clientCIDRs: + - 10.0.1.0/24 diff --git a/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.out.yaml b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.out.yaml new file mode 100644 index 0000000000..b57d7c29ee --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-securitypolicy-with-multiple-rules.out.yaml @@ -0,0 +1,198 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-tls + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: tls-passthrough + port: 8443 + protocol: TLS + tls: + mode: Passthrough + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tls-passthrough + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute +infraIR: + envoy-gateway/gateway-tls: + proxy: + listeners: + - name: envoy-gateway/gateway-tls/tls-passthrough + ports: + - containerPort: 8443 + name: tls-8443 + protocol: TLS + servicePort: 8443 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-tls + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-tls + namespace: envoy-gateway-system +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-db-traffic-only + namespace: default + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-db-subnet + principal: + clientCIDRs: + - 10.0.1.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: TLSRoute + name: tlsroute-multiple-rules + sectionName: db-traffic + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + name: tlsroute-multiple-rules + namespace: default + spec: + hostnames: + - '*.example.com' + parentRefs: + - name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + rules: + - backendRefs: + - name: database-service + port: 3306 + name: db-traffic + - backendRefs: + - name: api-service + port: 8443 + name: api-traffic + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: |- + Service default/database-service not found. + Service default/api-service not found. + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough +xdsIR: + envoy-gateway/gateway-tls: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-tls-e5408cf3 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-tls + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-tls-e5408cf3 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-tls + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + tcp: + - address: 0.0.0.0 + externalPort: 8443 + metadata: + kind: Gateway + name: gateway-tls + namespace: envoy-gateway + sectionName: tls-passthrough + name: envoy-gateway/gateway-tls/tls-passthrough + port: 8443 + routes: + - destination: + metadata: + kind: TLSRoute + name: tlsroute-multiple-rules + namespace: default + name: tlsroute/default/tlsroute-multiple-rules/rule/-1 + metadata: + kind: TLSRoute + name: tlsroute-multiple-rules + namespace: default + name: tlsroute/default/tlsroute-multiple-rules + tls: + inspector: + snis: + - '*.example.com' diff --git a/release-notes/current/new_features/9464-tlsroute-securitypolicy-support.md b/release-notes/current/new_features/9464-tlsroute-securitypolicy-support.md new file mode 100644 index 0000000000..304b1b363b --- /dev/null +++ b/release-notes/current/new_features/9464-tlsroute-securitypolicy-support.md @@ -0,0 +1 @@ +Added support for `TLSRoute` as a `targetRef` kind in `SecurityPolicy`. diff --git a/test/e2e/tests/tlsroute_authorization_client_ip.go b/test/e2e/tests/tlsroute_authorization_client_ip.go index 63c32cfb91..b92e85979c 100644 --- a/test/e2e/tests/tlsroute_authorization_client_ip.go +++ b/test/e2e/tests/tlsroute_authorization_client_ip.go @@ -8,6 +8,7 @@ package tests import ( + "context" "crypto/tls" "errors" "io" @@ -15,7 +16,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" @@ -23,6 +26,7 @@ import ( "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/gatewayapi/resource" + "github.com/envoyproxy/gateway/test/utils/prometheus" ) func init() { @@ -40,6 +44,12 @@ var TLSRouteAuthzWithClientIP = suite.ConformanceTest{ gwNN := types.NamespacedName{Name: "tls-authorization-backend", Namespace: ns} gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN, "tls-passthrough"), tlsRouteNNBlocked, tlsRouteNNAllowed) + // Create Prometheus client + promClient, err := prometheus.NewClient(suite.Client, + types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}, + ) + require.NoError(t, err) + // SecurityPolicy status.ancestors references the Gateway listener, not the route itself // This matches how TCPRoute authorization tests work tlsPassthroughSection := gwapiv1.SectionName("tls-passthrough") @@ -55,10 +65,12 @@ var TLSRouteAuthzWithClientIP = suite.ConformanceTest{ t.Run("blocked client IP cannot connect to blocked.example.com", func(t *testing.T) { testTLSRouteConnectionBlocked(t, gwAddr, "blocked.example.com") + verifyRBACStats(t, promClient, false) }) t.Run("allowed client IP can connect to allowed.example.com", func(t *testing.T) { testTLSRouteConnectionAllowed(t, gwAddr, "allowed.example.com") + verifyRBACStats(t, promClient, true) }) }, } @@ -150,3 +162,39 @@ func testTLSRouteConnectionAllowed(t *testing.T, gwAddr, hostname string) { t.Log("Connection allowed and completed successfully") } } + +func verifyRBACStats(t *testing.T, promClient *prometheus.Client, expectAllowed bool) { + t.Helper() + + metric := "envoy_rbac_denied" + if expectAllowed { + metric = "envoy_rbac_allowed" + } + + // Query RBAC metrics for the specific TLS listener (tls-passthrough-8443) + // This ensures we're checking stats for THIS test's traffic, not other tests + query := metric + `{namespace="envoy-gateway-system",envoy_rbac_prefix="tls-passthrough-8443"}` + + // Poll for RBAC stats with a short timeout + err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, + func(ctx context.Context) (bool, error) { + val, err := promClient.QuerySum(ctx, query) + if err != nil { + t.Logf("Prometheus query failed (will retry): %v", err) + return false, nil + } + + if val > 0 { + if expectAllowed { + t.Logf("RBAC filter allowed %v connections (confirmed via Prometheus)", val) + } else { + t.Logf("RBAC filter denied %v connections (confirmed via Prometheus)", val) + } + return true, nil + } + + return false, nil + }) + + require.NoError(t, err, "Failed to verify RBAC stats via Prometheus") +} From 57e3ab0a813e5c8c04a5c92848b34f9411ffa029 Mon Sep 17 00:00:00 2001 From: "asif.hasnain@sap.com" Date: Thu, 16 Jul 2026 20:45:11 +0200 Subject: [PATCH 3/3] address bot review Signed-off-by: asif.hasnain@sap.com --- internal/gatewayapi/securitypolicy.go | 2 +- internal/provider/kubernetes/controller.go | 1 + test/cel-validation/securitypolicy_test.go | 51 +++++++++++++++++-- .../tests/tlsroute_authorization_client_ip.go | 4 +- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 925ca09af4..d2be827adf 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -955,7 +955,7 @@ func validateSecurityPolicyForTCPAndTLS(p *egv1a1.SecurityPolicy) error { for i := range p.Spec.Authorization.Rules { rule := &p.Spec.Authorization.Rules[i] if rule.CEL != nil { - return fmt.Errorf("rule %d: CEL not supported for TCP", i) + return fmt.Errorf("rule %d: CEL not supported for TCP/TLS", i) } if rule.Principal == nil { continue diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index d2b044a5d3..090497fd73 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -1683,6 +1683,7 @@ func (r *gatewayAPIReconciler) processPolicyTargetReferenceGrants( resource.KindHTTPRoute, resource.KindGRPCRoute, resource.KindTCPRoute, + resource.KindTLSRoute, ), } diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go index b5f111bd81..b57e398f28 100644 --- a/test/cel-validation/securitypolicy_test.go +++ b/test/cel-validation/securitypolicy_test.go @@ -111,6 +111,51 @@ func TestSecurityPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "valid TLSRoute targetRef", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("TLSRoute"), + Name: gwapiv1.ObjectName("tls-backend"), + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "valid TLSRoute targetRef with authorization", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("TLSRoute"), + Name: gwapiv1.ObjectName("tls-backend"), + }, + }, + }, + Authorization: &egv1a1.Authorization{ + DefaultAction: new(egv1a1.AuthorizationActionDeny), + Rules: []egv1a1.AuthorizationRule{ + { + Action: egv1a1.AuthorizationActionAllow, + Principal: &egv1a1.Principal{ + ClientCIDRs: []egv1a1.CIDR{"10.0.0.0/8"}, + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, { desc: "valid mergeType with xRoute targetSelector", mutate: func(sp *egv1a1.SecurityPolicy) { @@ -210,7 +255,7 @@ func TestSecurityPolicyTarget(t *testing.T) { }, wantErrors: []string{ "spec: Invalid value:", - ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute", + ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute", }, }, { @@ -251,7 +296,7 @@ func TestSecurityPolicyTarget(t *testing.T) { wantErrors: []string{ "spec: Invalid value:", ": this policy can only have a targetRef.group of gateway.networking.k8s.io", - ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute", + ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute", }, }, { @@ -274,7 +319,7 @@ func TestSecurityPolicyTarget(t *testing.T) { wantErrors: []string{ "spec: Invalid value:", ": this policy can only have a targetRefs[*].group of gateway.networking.k8s.io", - ": this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute", + ": this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute", }, }, diff --git a/test/e2e/tests/tlsroute_authorization_client_ip.go b/test/e2e/tests/tlsroute_authorization_client_ip.go index b92e85979c..921b6ce9e7 100644 --- a/test/e2e/tests/tlsroute_authorization_client_ip.go +++ b/test/e2e/tests/tlsroute_authorization_client_ip.go @@ -158,9 +158,9 @@ func testTLSRouteConnectionAllowed(t *testing.T, gwAddr, hostname string) { if n > 0 { t.Logf("Successfully received response: %s", string(buf[:n])) - } else { - t.Log("Connection allowed and completed successfully") + return } + t.Fatalf("Connection was established but got an empty response; expected the backend to reply") } func verifyRBACStats(t *testing.T, promClient *prometheus.Client, expectAllowed bool) {