Skip to content

Commit 0677eca

Browse files
committed
feat(translator): Security policies for TLS gateways
Signed-off-by: asif.hasnain@sap.com <asif.hasnain@sap.com>
1 parent 5178c08 commit 0677eca

18 files changed

Lines changed: 544 additions & 46 deletions

api/v1alpha1/securitypolicy_types.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,18 @@ type SecurityPolicy struct {
3737

3838
// SecurityPolicySpec defines the desired state of SecurityPolicy.
3939
//
40-
// NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, and TCPRoute.
41-
// When a SecurityPolicy targets a TCPRoute, only client-IP CIDR based authorization
40+
// NOTE: SecurityPolicy can target Gateway, HTTPRoute, GRPCRoute, TCPRoute, and TLSRoute.
41+
// When a SecurityPolicy targets a TCPRoute or TLSRoute, only client-IP CIDR based authorization
4242
// (Authorization rules that use Principal.ClientCIDRs) is applied. Other
4343
// authentication/authorization features such as JWT, API Key, Basic Auth,
4444
// OIDC, External Authorization, or GeoIP based authorization are not applicable
45-
// to TCPRoute targets.
45+
// to TCPRoute or TLSRoute targets.
4646
//
4747
// +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"
4848
// +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"
49-
// +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"
49+
// +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"
5050
// +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"
51-
// +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"
51+
// +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"
5252
// +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"
5353
type SecurityPolicySpec struct {
5454
PolicyTargetReferences `json:",inline"`

charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7479,15 +7479,15 @@ spec:
74797479
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
74807480
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
74817481
: true'
7482-
- message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute
7482+
- message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute
74837483
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
7484-
''GRPCRoute'', ''TCPRoute''] : true'
7484+
''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true'
74857485
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
74867486
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
74877487
''gateway.networking.k8s.io'') : true '
7488-
- message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute
7488+
- message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute
74897489
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
7490-
''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
7490+
''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true '
74917491
- message: if authorization.rules.principal.jwt is used, jwt must be defined
74927492
rule: '(has(self.authorization) && has(self.authorization.rules) &&
74937493
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)

charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7478,15 +7478,15 @@ spec:
74787478
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
74797479
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
74807480
: true'
7481-
- message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute
7481+
- message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute
74827482
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
7483-
''GRPCRoute'', ''TCPRoute''] : true'
7483+
''GRPCRoute'', ''TCPRoute'', ''TLSRoute''] : true'
74847484
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
74857485
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
74867486
''gateway.networking.k8s.io'') : true '
7487-
- message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute
7487+
- message: this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/TLSRoute
74887488
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
7489-
''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
7489+
''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''TLSRoute'']) : true '
74907490
- message: if authorization.rules.principal.jwt is used, jwt must be defined
74917491
rule: '(has(self.authorization) && has(self.authorization.rules) &&
74927492
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)

internal/gatewayapi/securitypolicy.go

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,9 @@ func (t *Translator) processSecurityPolicyForRoute(
374374
// then run it once to keep the flow linear and easier to read.
375375
validator := validateSecurityPolicy
376376
errMsg := "invalid SecurityPolicy"
377-
if currTarget.Kind == resource.KindTCPRoute {
378-
validator = validateSecurityPolicyForTCP
379-
errMsg = "invalid SecurityPolicy for TCP route"
377+
if currTarget.Kind == resource.KindTCPRoute || currTarget.Kind == resource.KindTLSRoute {
378+
validator = validateSecurityPolicyForTCPAndTLS
379+
errMsg = "invalid SecurityPolicy for TCP/TLS route"
380380
}
381381
if err := validator(policy); err != nil {
382382
status.SetTranslationErrorForPolicyAncestors(&policy.Status,
@@ -660,16 +660,16 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error {
660660
return nil
661661
}
662662

663-
// validateSecurityPolicyForTCP ensures SecurityPolicy usage on TCP is compatible.
663+
// validateSecurityPolicyForTCPAndTLS ensures SecurityPolicy usage on TCP/TLS is compatible.
664664
//
665-
// TCP supports Authorization with ClientCIDRs ONLY.
665+
// TCP and TLS support Authorization with ClientCIDRs ONLY.
666666
// - Principals.JWT => invalid (HTTP-only)
667667
// - Principals.Headers => invalid (HTTP-only)
668-
// - Empty/no Authorization is allowed and results in no-op on TCP.
668+
// - Empty/no Authorization is allowed and results in no-op on TCP/TLS.
669669
// Returns an error when any HTTP-only field is present or CIDRs are invalid.
670-
func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error {
670+
func validateSecurityPolicyForTCPAndTLS(p *egv1a1.SecurityPolicy) error {
671671
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 {
672-
return fmt.Errorf("only authorization is supported for TCP (routes/listeners)")
672+
return fmt.Errorf("only authorization is supported for TCP/TLS (routes/listeners)")
673673
}
674674
if p.Spec.Authorization == nil || len(p.Spec.Authorization.Rules) == 0 {
675675
return nil
@@ -683,13 +683,13 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error {
683683
continue
684684
}
685685
if rule.Principal.JWT != nil {
686-
return fmt.Errorf("rule %d: JWT not supported for TCP", i)
686+
return fmt.Errorf("rule %d: JWT not supported for TCP/TLS", i)
687687
}
688688
if len(rule.Principal.Headers) > 0 {
689-
return fmt.Errorf("rule %d: headers not supported for TCP", i)
689+
return fmt.Errorf("rule %d: headers not supported for TCP/TLS", i)
690690
}
691691
if len(rule.Principal.ClientIPGeoLocations) > 0 {
692-
return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP", i)
692+
return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP/TLS", i)
693693
}
694694
if err := validateCIDRs(rule.Principal.ClientCIDRs); err != nil {
695695
return fmt.Errorf("rule %d: %w", i, err)
@@ -991,16 +991,26 @@ func (t *Translator) translateSecurityPolicyForRoute(
991991

992992
irKey := t.getIRKey(gtwCtx.Gateway)
993993
switch route.GetRouteType() {
994-
case resource.KindTCPRoute:
994+
case resource.KindTCPRoute, resource.KindTLSRoute:
995995
for _, listener := range parentRefCtx.listeners {
996996
// If policyTargetListener is set, only apply to the specific listener
997997
if policyTargetListener != nil && *policyTargetListener != listener.Name {
998998
continue
999999
}
10001000
tl := xdsIR[irKey].GetTCPListener(irListenerName(listener))
1001+
10011002
for _, r := range tl.Routes {
1003+
// For TLSRoute: filter by route metadata to handle multiple routes per listener
1004+
if target.Kind == resource.KindTLSRoute {
1005+
if r.Metadata == nil ||
1006+
r.Metadata.Namespace != string(target.Namespace) ||
1007+
r.Metadata.Name != string(target.Name) {
1008+
continue
1009+
}
1010+
}
1011+
10021012
// If target.SectionName is specified it must match the route-rule section name
1003-
// in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP
1013+
// in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP/TLS
10041014
// routes the section name is currently stored on r.Destination.Metadata.SectionName.
10051015
if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName {
10061016
continue
@@ -1009,7 +1019,7 @@ func (t *Translator) translateSecurityPolicyForRoute(
10091019
if r.Authorization != nil {
10101020
continue
10111021
}
1012-
// Only authorization for TCP
1022+
// Only authorization for TCP/TLS
10131023
if authorization != nil {
10141024
authCopy := *authorization
10151025
r.Authorization = &authCopy
@@ -1278,7 +1288,8 @@ func (t *Translator) translateSecurityPolicyForGateway(
12781288
tcpAuthorization = &authCopy
12791289
}
12801290

1281-
// Apply to TCP listeners (Authorization only).
1291+
// Apply to TCP/TLS listeners (Authorization only).
1292+
// Both TCPRoute and TLSRoute translate to TCP listeners in the IR.
12821293
if tcpAuthorization != nil {
12831294
for _, tl := range x.TCP {
12841295
if tl == nil || len(tl.Routes) == 0 {

internal/gatewayapi/securitypolicy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ func Test_validateSecurityPolicyForTCP_Table(t *testing.T) {
13261326
for _, tc := range tests {
13271327
t.Run(tc.name, func(t *testing.T) {
13281328
p := &egv1a1.SecurityPolicy{Spec: tc.spec}
1329-
err := validateSecurityPolicyForTCP(p)
1329+
err := validateSecurityPolicyForTCPAndTLS(p)
13301330
if tc.wantErr {
13311331
require.Error(t, err)
13321332
} else {

internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ securityPolicies:
8585
sectionName: tcp
8686
conditions:
8787
- lastTransitionTime: null
88-
message: Only authorization is supported for TCP (routes/listeners).
88+
message: Only authorization is supported for TCP/TLS (routes/listeners).
8989
reason: Invalid
9090
status: "False"
9191
type: Accepted

internal/provider/kubernetes/controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2447,7 +2447,7 @@ func TestProcessPolicyTargetReferenceGrants(t *testing.T) {
24472447
refGrant("sp-tcp-route", routeNS, resource.KindSecurityPolicy, policyNS, gwapiv1.GroupName, resource.KindTCPRoute),
24482448
refGrant("sp-tls-route", routeNS, resource.KindSecurityPolicy, policyNS, gwapiv1.GroupName, resource.KindTLSRoute),
24492449
},
2450-
expectedNames: sets.New("sp-gateway", "sp-http-route", "sp-grpc-route", "sp-tcp-route"),
2450+
expectedNames: sets.New("sp-gateway", "sp-http-route", "sp-grpc-route", "sp-tcp-route", "sp-tls-route"),
24512451
},
24522452
{
24532453
name: "ignores non matching grants",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
tcp:
2+
- name: "tls-listener-authorization"
3+
address: "0.0.0.0"
4+
port: 8443
5+
routes:
6+
- name: "tls-route-blocked"
7+
tls:
8+
inspector:
9+
snis:
10+
- "blocked.example.com"
11+
authorization:
12+
defaultAction: Deny
13+
rules:
14+
- action: Allow
15+
name: allow-office
16+
principal:
17+
clientCIDRs:
18+
- cidr: 192.168.254.0/24
19+
distinct: false
20+
isIPv6: false
21+
maskLen: 24
22+
destination:
23+
name: "tls-backend-blocked"
24+
settings:
25+
- endpoints:
26+
- host: "10.0.1.1"
27+
port: 443
28+
name: "tls-backend-blocked/backend/0"
29+
- name: "tls-route-allowed"
30+
tls:
31+
inspector:
32+
snis:
33+
- "allowed.example.com"
34+
authorization:
35+
defaultAction: Deny
36+
rules:
37+
- action: Allow
38+
name: allow-all-ipv4
39+
principal:
40+
clientCIDRs:
41+
- cidr: 0.0.0.0/0
42+
distinct: false
43+
isIPv6: false
44+
maskLen: 0
45+
- action: Allow
46+
name: allow-all-ipv6
47+
principal:
48+
clientCIDRs:
49+
- cidr: ::/0
50+
distinct: false
51+
isIPv6: true
52+
maskLen: 0
53+
destination:
54+
name: "tls-backend-allowed"
55+
settings:
56+
- endpoints:
57+
- host: "10.0.1.2"
58+
port: 443
59+
name: "tls-backend-allowed/backend/0"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
- circuitBreakers:
2+
thresholds:
3+
- maxRetries: 1024
4+
commonLbConfig: {}
5+
connectTimeout: 10s
6+
dnsLookupFamily: V4_PREFERRED
7+
edsClusterConfig:
8+
edsConfig:
9+
ads: {}
10+
resourceApiVersion: V3
11+
serviceName: tls-backend-blocked
12+
ignoreHealthOnHostRemoval: true
13+
loadBalancingPolicy:
14+
policies:
15+
- typedExtensionConfig:
16+
name: envoy.load_balancing_policies.least_request
17+
typedConfig:
18+
'@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
19+
localityLbConfig:
20+
localityWeightedLbConfig: {}
21+
name: tls-backend-blocked
22+
perConnectionBufferLimitBytes: 32768
23+
type: EDS
24+
- circuitBreakers:
25+
thresholds:
26+
- maxRetries: 1024
27+
commonLbConfig: {}
28+
connectTimeout: 10s
29+
dnsLookupFamily: V4_PREFERRED
30+
edsClusterConfig:
31+
edsConfig:
32+
ads: {}
33+
resourceApiVersion: V3
34+
serviceName: tls-backend-allowed
35+
ignoreHealthOnHostRemoval: true
36+
loadBalancingPolicy:
37+
policies:
38+
- typedExtensionConfig:
39+
name: envoy.load_balancing_policies.least_request
40+
typedConfig:
41+
'@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
42+
localityLbConfig:
43+
localityWeightedLbConfig: {}
44+
name: tls-backend-allowed
45+
perConnectionBufferLimitBytes: 32768
46+
type: EDS
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
- clusterName: tls-backend-blocked
2+
endpoints:
3+
- lbEndpoints:
4+
- endpoint:
5+
address:
6+
socketAddress:
7+
address: 10.0.1.1
8+
portValue: 443
9+
loadBalancingWeight: 1
10+
loadBalancingWeight: 1
11+
locality:
12+
region: tls-backend-blocked/backend/0
13+
- clusterName: tls-backend-allowed
14+
endpoints:
15+
- lbEndpoints:
16+
- endpoint:
17+
address:
18+
socketAddress:
19+
address: 10.0.1.2
20+
portValue: 443
21+
loadBalancingWeight: 1
22+
loadBalancingWeight: 1
23+
locality:
24+
region: tls-backend-allowed/backend/0

0 commit comments

Comments
 (0)