diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index 6b80655d4e..574eab08e5 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -37,10 +37,10 @@ type SecurityPolicy struct { // // +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'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute" -// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? !has(self.targetRef.sectionName) : true",message="this policy does not yet support the sectionName field" +// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind == 'Gateway' || !has(self.targetRef.sectionName) : true",message="this policy supports the sectionName field only for kind Gateway" // +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']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute" -// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) : true",message="this policy does not yet support the sectionName field" +// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind == 'Gateway' || !has(ref.sectionName)) : true",message="this policy supports the sectionName field only for kind Gateway" // +kubebuilder:validation:XValidation:rule="(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal.jwt))) ? has(self.jwt) : true", message="if authorization.rules.principal.jwt is used, jwt must be defined" // // SecurityPolicySpec defines the desired state of SecurityPolicy. 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 1f3189dc98..fdf41f34c3 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 @@ -4870,17 +4870,18 @@ spec: - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''] : true' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRef) ? self.targetRef.kind == ''Gateway'' || !has(self.targetRef.sectionName) + : 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 rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) : true ' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) - : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind == ''Gateway'' + || !has(ref.sectionName)) : 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.jwt))) ? has(self.jwt) diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index c2bc80a65f..cd4f50f04d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -4869,17 +4869,18 @@ spec: - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''] : true' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRef) ? self.targetRef.kind == ''Gateway'' || !has(self.targetRef.sectionName) + : 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 rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) : true ' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) - : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind == ''Gateway'' + || !has(ref.sectionName)) : 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.jwt))) ? has(self.jwt) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 81cf59f3fb..44c177b73f 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -490,7 +490,8 @@ type policyRouteTargetContext struct { type policyGatewayTargetContext struct { *GatewayContext - attached bool + attached bool + attachedToListeners sets.Set[string] } // listenersWithSameHTTPPort returns a list of the names of all other HTTP listeners diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index f9fd860037..b40257d963 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -78,17 +78,19 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security gatewayMap := map[types.NamespacedName]*policyGatewayTargetContext{} for _, gw := range gateways { key := utils.NamespacedName(gw) - gatewayMap[key] = &policyGatewayTargetContext{GatewayContext: gw} + gatewayMap[key] = &policyGatewayTargetContext{GatewayContext: gw, attachedToListeners: make(sets.Set[string])} } - // Map of Gateway to the routes attached to it - gatewayRouteMap := make(map[string]sets.Set[string]) + // Map of Gateway to the routes attached to it. + // The routes are grouped by sectionNames of their targetRefs + gatewayRouteMap := make(map[string]map[string]sets.Set[string]) handledPolicies := make(map[types.NamespacedName]*egv1a1.SecurityPolicy) // Translate // 1. First translate Policies targeting xRoutes - // 2. Finally, the policies targeting Gateways + // 2. Then translate Policies targeting Listeners + // 3. Finally, the policies targeting Gateways // Process the policies targeting xRoutes for _, currPolicy := range securityPolicies { @@ -134,9 +136,17 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security key := gwNN.String() if _, ok := gatewayRouteMap[key]; !ok { - gatewayRouteMap[key] = make(sets.Set[string]) + gatewayRouteMap[key] = make(map[string]sets.Set[string]) } - gatewayRouteMap[key].Insert(utils.NamespacedName(targetedRoute).String()) + listenerRouteMap := gatewayRouteMap[key] + sectionName := "" + if p.SectionName != nil { + sectionName = string(*p.SectionName) + } + if _, ok := listenerRouteMap[sectionName]; !ok { + listenerRouteMap[sectionName] = make(sets.Set[string]) + } + listenerRouteMap[sectionName].Insert(utils.NamespacedName(targetedRoute).String()) parentGateways = append(parentGateways, getAncestorRefForPolicy(gwNN, p.SectionName)) } } @@ -179,17 +189,12 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security } } - // Process the policies targeting Gateways + // Process the policies targeting Listeners for _, currPolicy := range securityPolicies { policyName := utils.NamespacedName(currPolicy) targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways) for _, currTarget := range targetRefs { - if currTarget.Kind == resource.KindGateway { - var ( - targetedGateway *GatewayContext - resolveErr *status.PolicyResolveError - ) - + if currTarget.Kind == resource.KindGateway && currTarget.SectionName != nil { policy, found := handledPolicies[policyName] if !found { policy = currPolicy.DeepCopy() @@ -197,69 +202,135 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security res = append(res, policy) } - targetedGateway, resolveErr = resolveSecurityPolicyGatewayTargetRef(policy, currTarget, gatewayMap) - // Skip if the gateway is not found - // It's not necessarily an error because the SecurityPolicy may be - // reconciled by multiple controllers. And the other controller may - // have the target gateway. - if targetedGateway == nil { - continue + t.processSecurityPolicyForGateway(resources, xdsIR, + gatewayMap, gatewayRouteMap, policy, currTarget) + } + } + } + // Process the policies targeting Gateways + for _, currPolicy := range securityPolicies { + policyName := utils.NamespacedName(currPolicy) + targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways) + for _, currTarget := range targetRefs { + if currTarget.Kind == resource.KindGateway && currTarget.SectionName == nil { + policy, found := handledPolicies[policyName] + if !found { + policy = currPolicy.DeepCopy() + handledPolicies[policyName] = policy + res = append(res, policy) } - // Find its ancestor reference by resolved gateway, even with resolve error - gatewayNN := utils.NamespacedName(targetedGateway) - parentGateways := []gwapiv1a2.ParentReference{ - getAncestorRefForPolicy(gatewayNN, nil), - } + t.processSecurityPolicyForGateway(resources, xdsIR, + gatewayMap, gatewayRouteMap, policy, currTarget) + } + } + } - // Set conditions for resolve error, then skip current gateway - if resolveErr != nil { - status.SetResolveErrorForPolicyAncestors(&policy.Status, - parentGateways, - t.GatewayControllerName, - policy.Generation, - resolveErr, - ) + return res +} - continue - } +func (t *Translator) processSecurityPolicyForGateway( + resources *resource.Resources, + xdsIR resource.XdsIRMap, + gatewayMap map[types.NamespacedName]*policyGatewayTargetContext, + gatewayRouteMap map[string]map[string]sets.Set[string], + policy *egv1a1.SecurityPolicy, + currTarget gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, +) { + var ( + targetedGateway *GatewayContext + resolveErr *status.PolicyResolveError + ) - if err := t.translateSecurityPolicyForGateway(policy, targetedGateway, currTarget, resources, xdsIR); err != nil { - status.SetTranslationErrorForPolicyAncestors(&policy.Status, - parentGateways, - t.GatewayControllerName, - policy.Generation, - status.Error2ConditionMsg(err), - ) - } + targetedGateway, resolveErr = resolveSecurityPolicyGatewayTargetRef(policy, currTarget, gatewayMap) + // Skip if the gateway is not found + // It's not necessarily an error because the SecurityPolicy may be + // reconciled by multiple controllers. And the other controller may + // have the target gateway. + if targetedGateway == nil { + return + } - // Set Accepted condition if it is unset - status.SetAcceptedForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName) + // Find its ancestor reference by resolved gateway, even with resolve error + gatewayNN := utils.NamespacedName(targetedGateway) + parentGateways := []gwapiv1a2.ParentReference{ + getAncestorRefForPolicy(gatewayNN, currTarget.SectionName), + } - // Check if this policy is overridden by other policies targeting - // at route level - if r, ok := gatewayRouteMap[gatewayNN.String()]; ok { - // Maintain order here to ensure status/string does not change with the same data - routes := r.UnsortedList() - sort.Strings(routes) - message := fmt.Sprintf( - "This policy is being overridden by other securityPolicies for these routes: %v", - routes) - status.SetConditionForPolicyAncestors(&policy.Status, - parentGateways, - t.GatewayControllerName, - egv1a1.PolicyConditionOverridden, - metav1.ConditionTrue, - egv1a1.PolicyReasonOverridden, - message, - policy.Generation, - ) - } - } - } + // Set conditions for resolve error, then skip current gateway + if resolveErr != nil { + status.SetResolveErrorForPolicyAncestors(&policy.Status, + parentGateways, + t.GatewayControllerName, + policy.Generation, + resolveErr, + ) + + return } - return res + if err := t.translateSecurityPolicyForGateway(policy, targetedGateway, currTarget, resources, xdsIR); err != nil { + status.SetTranslationErrorForPolicyAncestors(&policy.Status, + parentGateways, + t.GatewayControllerName, + policy.Generation, + status.Error2ConditionMsg(err), + ) + } + + // Set Accepted condition if it is unset + status.SetAcceptedForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName) + + // Check if this policy is overridden by other policies targeting at route and listener levels + overriddenTargetsMessage := getOverriddenTargetsMessage( + gatewayMap[gatewayNN], gatewayRouteMap[gatewayNN.String()], currTarget.SectionName) + if overriddenTargetsMessage != "" { + status.SetConditionForPolicyAncestors(&policy.Status, + parentGateways, + t.GatewayControllerName, + egv1a1.PolicyConditionOverridden, + metav1.ConditionTrue, + egv1a1.PolicyReasonOverridden, + "This policy is being overridden by other securityPolicies for "+overriddenTargetsMessage, + policy.Generation, + ) + } +} + +func getOverriddenTargetsMessage( + targetContext *policyGatewayTargetContext, + listenerRouteMap map[string]sets.Set[string], + sectionName *gwapiv1.SectionName, +) string { + var listeners, routes []string + if sectionName == nil { + if targetContext != nil { + listeners = targetContext.attachedToListeners.UnsortedList() + } + for _, routeSet := range listenerRouteMap { + routes = append(routes, routeSet.UnsortedList()...) + } + } else if listenerRouteMap != nil { + if routeSet, ok := listenerRouteMap[string(*sectionName)]; ok { + routes = routeSet.UnsortedList() + } + if routeSet, ok := listenerRouteMap[""]; ok { + routes = append(routes, routeSet.UnsortedList()...) + } + } + if len(listeners) > 0 { + sort.Strings(listeners) + if len(routes) > 0 { + sort.Strings(routes) + return fmt.Sprintf("these listeners: %v and these routes: %v", listeners, routes) + } else { + return fmt.Sprintf("these listeners: %v", listeners) + } + } else if len(routes) > 0 { + sort.Strings(routes) + return fmt.Sprintf("these routes: %v", routes) + } + return "" } // validateSecurityPolicy validates the SecurityPolicy. @@ -327,19 +398,32 @@ func resolveSecurityPolicyGatewayTargetRef( return nil, nil } - // Check if another policy targeting the same Gateway exists - if gateway.attached { - message := fmt.Sprintf("Unable to target Gateway %s, another SecurityPolicy has already attached to it", - string(target.Name)) + if target.SectionName == nil { + // Check if another policy targeting the same Gateway exists + if gateway.attached { + message := fmt.Sprintf("Unable to target Gateway %s, another SecurityPolicy has already attached to it", + string(target.Name)) - return gateway.GatewayContext, &status.PolicyResolveError{ - Reason: gwapiv1a2.PolicyReasonConflicted, - Message: message, + return gateway.GatewayContext, &status.PolicyResolveError{ + Reason: gwapiv1a2.PolicyReasonConflicted, + Message: message, + } + } + gateway.attached = true + } else { + listenerName := string(*target.SectionName) + if gateway.attachedToListeners.Has(listenerName) { + message := fmt.Sprintf("Unable to target Listener %s/%s, another SecurityPolicy has already attached to it", + string(target.Name), listenerName) + + return gateway.GatewayContext, &status.PolicyResolveError{ + Reason: gwapiv1a2.PolicyReasonConflicted, + Message: message, + } } + gateway.attachedToListeners.Insert(listenerName) } - // Set context and save - gateway.attached = true gateways[key] = gateway return gateway.GatewayContext, nil @@ -586,10 +670,16 @@ func (t *Translator) translateSecurityPolicyForGateway( policyTarget := irStringKey(policy.Namespace, string(target.Name)) for _, h := range x.HTTP { - gatewayName := h.Name[0:strings.LastIndex(h.Name, "/")] + // A HTTPListener name has the format namespace/gatewayName/listenerName + gatewayNameEnd := strings.LastIndex(h.Name, "/") + gatewayName := h.Name[0:gatewayNameEnd] if t.MergeGateways && gatewayName != policyTarget { continue } + // If specified the sectionName must match the listenerName part of the HTTPListener name + if target.SectionName != nil && string(*target.SectionName) != h.Name[gatewayNameEnd+1:] { + continue + } // A Policy targeting the most specific scope(xRoute) wins over a policy // targeting a lesser specific scope(Gateway). for _, r := range h.Routes { diff --git a/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml b/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml index f34bff003a..cf92c4c9eb 100644 --- a/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml @@ -7,9 +7,17 @@ gateways: spec: gatewayClassName: envoy-gateway-class listeners: - - name: http + - name: listener-1 protocol: HTTP port: 80 + hostname: listener-1.gateway-1.envoyproxy.io + allowedRoutes: + namespaces: + from: All + - name: listener-2 + protocol: HTTP + port: 80 + hostname: listener-2.gateway-1.envoyproxy.io allowedRoutes: namespaces: from: All @@ -21,11 +29,11 @@ httpRoutes: name: httproute-1 spec: hostnames: - - gateway.envoyproxy.io + - listener-1.gateway-1.envoyproxy.io parentRefs: - namespace: envoy-gateway name: gateway-1 - sectionName: http + sectionName: listener-1 rules: - matches: - path: @@ -40,11 +48,11 @@ httpRoutes: name: httproute-2 spec: hostnames: - - gateway.envoyproxy.io + - listener-1.gateway-1.envoyproxy.io parentRefs: - namespace: envoy-gateway name: gateway-1 - sectionName: http + sectionName: listener-1 rules: - matches: - path: @@ -59,11 +67,11 @@ httpRoutes: name: httproute-3 spec: hostnames: - - gateway.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io parentRefs: - namespace: envoy-gateway name: gateway-1 - sectionName: http + sectionName: listener-2 rules: - matches: - path: @@ -71,6 +79,63 @@ httpRoutes: backendRefs: - name: service-1 port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-4 + spec: + hostnames: + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: listener-2 + rules: + - matches: + - path: + value: "/httproute-4" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-5 + spec: + hostnames: + - listener-1.gateway-1.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/httproute-5" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-6 + spec: + hostnames: + - listener-1.gateway-1.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/httproute-6" + backendRefs: + - name: service-1 + port: 8080 securityPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy @@ -108,6 +173,20 @@ securityPolicies: claimToHeaders: - header: one-route-example-key claim: claim1 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: policy-for-listener-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: listener-2 + cors: + allowHeaders: + - "x-listener-2" - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: @@ -146,3 +225,16 @@ securityPolicies: allowOrigins: - "http://*.example.com" maxAge: 1000s +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route-5 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-5 + cors: + allowHeaders: + - "x-httproute-5" diff --git a/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml b/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml index 890fe5272d..b256fcd587 100644 --- a/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml @@ -11,12 +11,43 @@ gateways: - allowedRoutes: namespaces: from: All - name: http + hostname: listener-1.gateway-1.envoyproxy.io + name: listener-1 + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: listener-2.gateway-1.envoyproxy.io + name: listener-2 port: 80 protocol: HTTP status: listeners: - - attachedRoutes: 3 + - attachedRoutes: 4 + 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: listener-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 4 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -33,7 +64,7 @@ gateways: reason: ResolvedRefs status: "True" type: ResolvedRefs - name: http + name: listener-2 supportedKinds: - group: gateway.networking.k8s.io kind: HTTPRoute @@ -48,11 +79,11 @@ httpRoutes: namespace: default spec: hostnames: - - gateway.envoyproxy.io + - listener-1.gateway-1.envoyproxy.io parentRefs: - name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-1 rules: - backendRefs: - name: service-1 @@ -77,7 +108,7 @@ httpRoutes: parentRef: name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-1 - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -86,11 +117,11 @@ httpRoutes: namespace: default spec: hostnames: - - gateway.envoyproxy.io + - listener-1.gateway-1.envoyproxy.io parentRefs: - name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-1 rules: - backendRefs: - name: service-1 @@ -115,7 +146,7 @@ httpRoutes: parentRef: name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-1 - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -124,11 +155,11 @@ httpRoutes: namespace: default spec: hostnames: - - gateway.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io parentRefs: - name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-2 rules: - backendRefs: - name: service-1 @@ -153,13 +184,125 @@ httpRoutes: parentRef: name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-2 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-4 + namespace: default + spec: + hostnames: + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: listener-2 + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /httproute-4 + 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-1 + namespace: envoy-gateway + sectionName: listener-2 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-5 + namespace: default + spec: + hostnames: + - listener-1.gateway-1.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /httproute-5 + 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-1 + namespace: envoy-gateway +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-6 + namespace: default + spec: + hostnames: + - listener-1.gateway-1.envoyproxy.io + - listener-2.gateway-1.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /httproute-6 + 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-1 + namespace: envoy-gateway infraIR: envoy-gateway/gateway-1: proxy: listeners: - address: null - name: envoy-gateway/gateway-1/http + name: envoy-gateway/gateway-1/listener-1 ports: - containerPort: 10080 name: http-80 @@ -207,7 +350,7 @@ securityPolicies: kind: Gateway name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-1 conditions: - lastTransitionTime: null message: Policy has been accepted. @@ -237,13 +380,77 @@ securityPolicies: kind: Gateway name: gateway-1 namespace: envoy-gateway - sectionName: http + sectionName: listener-2 + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route-5 + namespace: default + spec: + cors: + allowHeaders: + - x-httproute-5 + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-5 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-listener-2 + namespace: envoy-gateway + spec: + cors: + allowHeaders: + - x-listener-2 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: listener-2 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-2 conditions: - lastTransitionTime: null message: Policy has been accepted. reason: Accepted status: "True" type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other securityPolicies for these + routes: [default/httproute-3 default/httproute-5]' + reason: Overridden + status: "True" + type: Overridden controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy @@ -297,7 +504,8 @@ securityPolicies: type: Accepted - lastTransitionTime: null message: 'This policy is being overridden by other securityPolicies for these - routes: [default/httproute-1 default/httproute-3]' + listeners: [listener-2] and these routes: [default/httproute-1 default/httproute-3 + default/httproute-5]' reason: Overridden status: "True" type: Overridden @@ -310,19 +518,114 @@ xdsIR: http: - address: 0.0.0.0 hostnames: - - '*' + - listener-1.gateway-1.envoyproxy.io isHTTP2: false metadata: kind: Gateway name: gateway-1 namespace: envoy-gateway - sectionName: http - name: envoy-gateway/gateway-1/http + sectionName: listener-1 + name: envoy-gateway/gateway-1/listener-1 path: escapedSlashesAction: UnescapeAndRedirect mergeSlashes: true port: 10080 routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-5 + namespace: default + name: httproute/default/httproute-5/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-5/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: listener-1.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-5 + namespace: default + name: httproute/default/httproute-5/rule/0/match/0/listener-1_gateway-1_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /httproute-5 + security: + cors: + allowHeaders: + - x-httproute-5 + - destination: + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-6/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: listener-1.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0/match/0/listener-1_gateway-1_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /httproute-6 + security: + cors: + allowHeaders: + - x-header-1 + - x-header-2 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + safeRegex: http://.*\.example\.com + - distinct: false + exact: http://foo.bar.com + name: "" + - distinct: false + name: "" + safeRegex: https://.* + exposeHeaders: + - x-header-3 + - x-header-4 + maxAge: 16m40s + jwt: + providers: + - audiences: + - one.foo.com + claimToHeaders: + - claim: claim1 + header: one-route-example-key + issuer: https://one.example.com + name: example1 + remoteJWKS: + uri: https://one.example.com/jwt/public-key/jwks.json - destination: metadata: kind: HTTPRoute @@ -341,13 +644,13 @@ xdsIR: name: httproute/default/httproute-1/rule/0/backend/0 protocol: HTTP weight: 1 - hostname: gateway.envoyproxy.io + hostname: listener-1.gateway-1.envoyproxy.io isHTTP2: false metadata: kind: HTTPRoute name: httproute-1 namespace: default - name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + name: httproute/default/httproute-1/rule/0/match/0/listener-1_gateway-1_envoyproxy_io pathMatch: distinct: false name: "" @@ -389,13 +692,13 @@ xdsIR: name: httproute/default/httproute-2/rule/0/backend/0 protocol: HTTP weight: 1 - hostname: gateway.envoyproxy.io + hostname: listener-1.gateway-1.envoyproxy.io isHTTP2: false metadata: kind: HTTPRoute name: httproute-2 namespace: default - name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io + name: httproute/default/httproute-2/rule/0/match/0/listener-1_gateway-1_envoyproxy_io pathMatch: distinct: false name: "" @@ -433,6 +736,120 @@ xdsIR: name: example1 remoteJWKS: uri: https://one.example.com/jwt/public-key/jwks.json + - address: 0.0.0.0 + hostnames: + - listener-2.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-2 + name: envoy-gateway/gateway-1/listener-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-4 + namespace: default + name: httproute/default/httproute-4/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-4/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: listener-2.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-4 + namespace: default + name: httproute/default/httproute-4/rule/0/match/0/listener-2_gateway-1_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /httproute-4 + security: + cors: + allowHeaders: + - x-listener-2 + - destination: + metadata: + kind: HTTPRoute + name: httproute-5 + namespace: default + name: httproute/default/httproute-5/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-5/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: listener-2.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-5 + namespace: default + name: httproute/default/httproute-5/rule/0/match/0/listener-2_gateway-1_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /httproute-5 + security: + cors: + allowHeaders: + - x-httproute-5 + - destination: + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-6/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: listener-2.gateway-1.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0/match/0/listener-2_gateway-1_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /httproute-6 + security: + cors: + allowHeaders: + - x-listener-2 - destination: metadata: kind: HTTPRoute @@ -451,13 +868,13 @@ xdsIR: name: httproute/default/httproute-3/rule/0/backend/0 protocol: HTTP weight: 1 - hostname: gateway.envoyproxy.io + hostname: listener-2.gateway-1.envoyproxy.io isHTTP2: false metadata: kind: HTTPRoute name: httproute-3 namespace: default - name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io + name: httproute/default/httproute-3/rule/0/match/0/listener-2_gateway-1_envoyproxy_io pathMatch: distinct: false name: "" diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go index 40f028d4a8..ebaa5522ed 100644 --- a/test/cel-validation/securitypolicy_test.go +++ b/test/cel-validation/securitypolicy_test.go @@ -149,7 +149,7 @@ func TestSecurityPolicyTarget(t *testing.T) { }, { - desc: "sectionName disabled until supported - targetRef", + desc: "sectionName supported for kind Gateway - targetRef", mutate: func(sp *egv1a1.SecurityPolicy) { sp.Spec = egv1a1.SecurityPolicySpec{ PolicyTargetReferences: egv1a1.PolicyTargetReferences{ @@ -164,12 +164,10 @@ func TestSecurityPolicyTarget(t *testing.T) { }, } }, - wantErrors: []string{ - "spec: Invalid value: \"object\": this policy does not yet support the sectionName field", - }, + wantErrors: []string{}, }, { - desc: "sectionName disabled until supported - targetRefs", + desc: "sectionName supported for kind Gateway - targetRefs", mutate: func(sp *egv1a1.SecurityPolicy) { sp.Spec = egv1a1.SecurityPolicySpec{ PolicyTargetReferences: egv1a1.PolicyTargetReferences{ @@ -186,8 +184,48 @@ func TestSecurityPolicyTarget(t *testing.T) { }, } }, + wantErrors: []string{}, + }, + { + desc: "sectionName disabled until supported for kind xRoute - targetRef", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("HTTPRoute"), + Name: gwapiv1a2.ObjectName("backend"), + }, + SectionName: §ionName, + }, + }, + } + }, + wantErrors: []string{ + "spec: Invalid value: \"object\": this policy supports the sectionName field only for kind Gateway", + }, + }, + { + desc: "sectionName disabled until supported for kind xRoute - targetRefs", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + { + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("HTTPRoute"), + Name: gwapiv1a2.ObjectName("backend"), + }, + SectionName: §ionName, + }, + }, + }, + } + }, wantErrors: []string{ - "spec: Invalid value: \"object\": this policy does not yet support the sectionName field", + "spec: Invalid value: \"object\": this policy supports the sectionName field only for kind Gateway", }, }, diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 34756b1c6d..85d865a3a3 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -43514,17 +43514,18 @@ spec: - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''] : true' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRef) ? self.targetRef.kind == ''Gateway'' || !has(self.targetRef.sectionName) + : 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 rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) : true ' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) - : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind == ''Gateway'' + || !has(ref.sectionName)) : 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.jwt))) ? has(self.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 b2ae01533d..837a258bf2 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -26202,17 +26202,18 @@ spec: - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''] : true' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRef) ? self.targetRef.kind == ''Gateway'' || !has(self.targetRef.sectionName) + : 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 rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute'']) : true ' - - message: this policy does not yet support the sectionName field - rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) - : true' + - message: this policy supports the sectionName field only for kind Gateway + rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind == ''Gateway'' + || !has(ref.sectionName)) : 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.jwt))) ? has(self.jwt)