diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go
index 08cb1e63c6..bd866d60a8 100644
--- a/api/v1alpha1/securitypolicy_types.go
+++ b/api/v1alpha1/securitypolicy_types.go
@@ -52,6 +52,15 @@ type SecurityPolicy struct {
type SecurityPolicySpec struct {
PolicyTargetReferences `json:",inline"`
+ // MergeType determines how this configuration is merged with existing SecurityPolicy
+ // configurations targeting a parent resource. When set, this configuration will be merged
+ // into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
+ // This field cannot be set when targeting a parent resource (Gateway).
+ // If unset, no merging occurs, and only the most specific configuration takes effect.
+ //
+ // +optional
+ MergeType *MergeType `json:"mergeType,omitempty"`
+
// APIKeyAuth defines the configuration for the API Key Authentication.
//
// +optional
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 8577b27ce1..2dbc8eaffd 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -6932,6 +6932,11 @@ func (in *SecurityPolicyList) DeepCopyObject() runtime.Object {
func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) {
*out = *in
in.PolicyTargetReferences.DeepCopyInto(&out.PolicyTargetReferences)
+ if in.MergeType != nil {
+ in, out := &in.MergeType, &out.MergeType
+ *out = new(MergeType)
+ **out = **in
+ }
if in.APIKeyAuth != nil {
in, out := &in.APIKeyAuth, &out.APIKeyAuth
*out = new(APIKeyAuth)
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 3bfde648f7..0e91ef0681 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
@@ -4368,6 +4368,14 @@ spec:
required:
- providers
type: object
+ mergeType:
+ description: |-
+ MergeType determines how this configuration is merged with existing SecurityPolicy
+ configurations targeting a parent resource. When set, this configuration will be merged
+ into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
+ This field cannot be set when targeting a parent resource (Gateway).
+ If unset, no merging occurs, and only the most specific configuration takes effect.
+ type: string
oidc:
description: OIDC defines the configuration for the OpenID Connect
(OIDC) authentication.
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 8303f1b640..17d5b973c9 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -4367,6 +4367,14 @@ spec:
required:
- providers
type: object
+ mergeType:
+ description: |-
+ MergeType determines how this configuration is merged with existing SecurityPolicy
+ configurations targeting a parent resource. When set, this configuration will be merged
+ into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
+ This field cannot be set when targeting a parent resource (Gateway).
+ If unset, no merging occurs, and only the most specific configuration takes effect.
+ type: string
oidc:
description: OIDC defines the configuration for the OpenID Connect
(OIDC) authentication.
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index f8f1181833..87455c5f44 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -111,16 +111,31 @@ func (t *Translator) ProcessSecurityPolicies(
// 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], gatewayMapSize)
+ gatewayRouteMap := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize),
+ }
handledPolicies := make(map[types.NamespacedName]*egv1a1.SecurityPolicy, policyMapSize)
+ // Map of attached Policy to Gateway. Used for policy merge process.
+ gatewayPolicyMap := make(map[NamespacedNameWithSection]*egv1a1.SecurityPolicy, gatewayMapSize)
+
+ // Map of Gateway to the routes merged to it.
+ gatewayPolicyMerged := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize),
+ }
+
// Translate
// 1. First translate Policies targeting RouteRules
// 2. Next translate Policies targeting xRoutes
// 3. Then translate Policies targeting Listeners
// 4. Finally, the policies targeting Gateways
+ // Build gateway policy maps, which are needed when processing the policies targeting xRoutes.
+ t.buildGatewayPolicyMapForSecurity(securityPolicies, gateways, gatewayMap, gatewayPolicyMap)
+
// Process the policies targeting RouteRules (HTTP + TCP)
for _, currPolicy := range securityPolicies {
policyName := utils.NamespacedName(currPolicy)
@@ -135,8 +150,7 @@ func (t *Translator) ProcessSecurityPolicies(
res = append(res, policy)
}
- t.processSecurityPolicyForRoute(resources, xdsIR,
- routeMap, gatewayRouteMap, policy, currTarget)
+ t.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget)
}
}
}
@@ -154,8 +168,7 @@ func (t *Translator) ProcessSecurityPolicies(
res = append(res, policy)
}
- t.processSecurityPolicyForRoute(resources, xdsIR,
- routeMap, gatewayRouteMap, policy, currTarget)
+ t.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget)
}
}
}
@@ -173,8 +186,7 @@ func (t *Translator) ProcessSecurityPolicies(
res = append(res, policy)
}
- t.processSecurityPolicyForGateway(resources, xdsIR,
- gatewayMap, gatewayRouteMap, policy, currTarget)
+ t.processSecurityPolicyForGateway(resources, xdsIR, gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget)
}
}
}
@@ -192,8 +204,7 @@ func (t *Translator) ProcessSecurityPolicies(
res = append(res, policy)
}
- t.processSecurityPolicyForGateway(resources, xdsIR,
- gatewayMap, gatewayRouteMap, policy, currTarget)
+ t.processSecurityPolicyForGateway(resources, xdsIR, gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget)
}
}
}
@@ -207,18 +218,65 @@ func (t *Translator) ProcessSecurityPolicies(
return res
}
+func (t *Translator) buildGatewayPolicyMapForSecurity(
+ securityPolicies []*egv1a1.SecurityPolicy,
+ gateways []*GatewayContext,
+ gatewayMap map[types.NamespacedName]*policyGatewayTargetContext,
+ gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.SecurityPolicy,
+) {
+ for _, currPolicy := range securityPolicies {
+ targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
+ for _, currTarget := range targetRefs {
+ if currTarget.Kind == resource.KindGateway {
+ // Check if the gateway exists
+ key := types.NamespacedName{
+ Name: string(currTarget.Name),
+ Namespace: currPolicy.Namespace,
+ }
+ gateway, ok := gatewayMap[key]
+ if !ok {
+ continue
+ }
+
+ // Check if the specified listener exists when sectionName is set
+ if currTarget.SectionName != nil {
+ if err := validateGatewayListenerSectionName(
+ *currTarget.SectionName,
+ key,
+ gateway.listeners,
+ ); err != nil {
+ continue
+ }
+ }
+
+ mapKey := NamespacedNameWithSection{
+ NamespacedName: key,
+ SectionName: ptr.Deref(currTarget.SectionName, ""),
+ }
+
+ // Only store the first policy for this Gateway/Listener - conflicts are handled elsewhere
+ if _, ok := gatewayPolicyMap[mapKey]; ok {
+ continue
+ }
+ gatewayPolicyMap[mapKey] = currPolicy
+ }
+ }
+ }
+}
+
func (t *Translator) processSecurityPolicyForRoute(
resources *resource.Resources,
xdsIR resource.XdsIRMap,
routeMap map[policyTargetRouteKey]*policyRouteTargetContext,
- gatewayRouteMap map[string]map[string]sets.Set[string],
+ gatewayRouteMap *GatewayPolicyRouteMap,
+ gatewayPolicyMerged *GatewayPolicyRouteMap,
+ gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.SecurityPolicy,
policy *egv1a1.SecurityPolicy,
currTarget gwapiv1.LocalPolicyTargetReferenceWithSectionName,
) {
var (
- targetedRoute RouteContext
- parentGateways []*gwapiv1.ParentReference
- resolveErr *status.PolicyResolveError
+ targetedRoute RouteContext
+ resolveErr *status.PolicyResolveError
)
targetedRoute, resolveErr = resolveSecurityPolicyRouteTargetRef(policy, currTarget, routeMap)
@@ -234,39 +292,49 @@ func (t *Translator) processSecurityPolicyForRoute(
// gatewayRouteMap, which will be used to check policy override.
// The parent gateways are also used to set the status of the policy.
parentRefs := GetParentReferences(targetedRoute)
+ ancestorRefs := make([]*gwapiv1.ParentReference, 0, len(parentRefs))
+ parentRefCtxs := make([]*RouteParentContext, 0, len(parentRefs))
for _, p := range parentRefs {
if p.Kind == nil || *p.Kind == resource.KindGateway {
namespace := targetedRoute.GetNamespace()
if p.Namespace != nil {
namespace = string(*p.Namespace)
}
- gwNN := types.NamespacedName{
- Namespace: namespace,
- Name: string(p.Name),
+ mapKey := NamespacedNameWithSection{
+ NamespacedName: types.NamespacedName{
+ Namespace: namespace,
+ Name: string(p.Name),
+ },
+ SectionName: ptr.Deref(p.SectionName, ""),
}
- key := gwNN.String()
- if _, ok := gatewayRouteMap[key]; !ok {
- gatewayRouteMap[key] = make(map[string]sets.Set[string])
+ if _, ok := gatewayRouteMap.Routes[mapKey]; !ok {
+ gatewayRouteMap.Routes[mapKey] = make(sets.Set[string])
}
- listenerRouteMap := gatewayRouteMap[key]
- sectionName := ""
- if p.SectionName != nil {
- sectionName = string(*p.SectionName)
+ gatewayRouteMap.Routes[mapKey].Insert(utils.NamespacedName(targetedRoute).String())
+
+ // Register section name to Gateway index for efficient lookup when retrieving overridden and merged targets
+ if _, ok := gatewayRouteMap.SectionIndex[mapKey.NamespacedName]; !ok {
+ gatewayRouteMap.SectionIndex[mapKey.NamespacedName] = make(sets.Set[string])
}
- if _, ok := listenerRouteMap[sectionName]; !ok {
- listenerRouteMap[sectionName] = make(sets.Set[string])
+ gatewayRouteMap.SectionIndex[mapKey.NamespacedName].Insert(string(mapKey.SectionName))
+
+ // Do need a section name since the policy is targeting to a route.
+ ancestorRef := getAncestorRefForPolicy(mapKey.NamespacedName, p.SectionName)
+ ancestorRefs = append(ancestorRefs, &ancestorRef)
+
+ // Only process parentRefs that were handled by this translator
+ // (skip those referencing Gateways with different GatewayClasses)
+ if parentRefCtx := targetedRoute.GetRouteParentContext(p); parentRefCtx != nil {
+ parentRefCtxs = append(parentRefCtxs, parentRefCtx)
}
- listenerRouteMap[sectionName].Insert(utils.NamespacedName(targetedRoute).String())
- ancestorRef := getAncestorRefForPolicy(gwNN, p.SectionName)
- parentGateways = append(parentGateways, &ancestorRef)
}
}
// Set conditions for resolve error, then skip current xroute
if resolveErr != nil {
status.SetResolveErrorForPolicyAncestors(&policy.Status,
- parentGateways,
+ ancestorRefs,
t.GatewayControllerName,
policy.Generation,
resolveErr,
@@ -285,7 +353,7 @@ func (t *Translator) processSecurityPolicyForRoute(
}
if err := validator(policy); err != nil {
status.SetTranslationErrorForPolicyAncestors(&policy.Status,
- parentGateways,
+ ancestorRefs,
t.GatewayControllerName,
policy.Generation,
status.Error2ConditionMsg(fmt.Errorf("%s: %w", errMsg, err)),
@@ -294,21 +362,133 @@ func (t *Translator) processSecurityPolicyForRoute(
return
}
- if err := t.translateSecurityPolicyForRoute(policy, targetedRoute, currTarget, resources, xdsIR); err != nil {
- status.SetTranslationErrorForPolicyAncestors(&policy.Status,
- parentGateways,
- t.GatewayControllerName,
- policy.Generation,
- status.Error2ConditionMsg(err),
- )
+ // Check if merging is enabled
+ if policy.Spec.MergeType == nil {
+ // No merging - use existing translation logic
+ if err := t.translateSecurityPolicyForRoute(policy, targetedRoute, currTarget, resources, xdsIR, nil); err != nil {
+ status.SetTranslationErrorForPolicyAncestors(&policy.Status,
+ ancestorRefs,
+ t.GatewayControllerName,
+ policy.Generation,
+ status.Error2ConditionMsg(err),
+ )
+ }
+ } else {
+ // Merging enabled - merge with parent policies
+ for _, parentRefCtx := range parentRefCtxs {
+ for _, listener := range parentRefCtx.listeners {
+ gwNN := utils.NamespacedName(listener.gateway.Gateway)
+ ancestorRef := getAncestorRefForPolicy(gwNN, &listener.Name)
+
+ // Find Gateway listener level policy
+ listenerMapKey := NamespacedNameWithSection{
+ NamespacedName: gwNN,
+ SectionName: listener.Name,
+ }
+ listenerPolicy := gatewayPolicyMap[listenerMapKey]
+
+ // Find Gateway level policy
+ gwMapKey := NamespacedNameWithSection{
+ NamespacedName: gwNN,
+ }
+ gwPolicy := gatewayPolicyMap[gwMapKey]
+
+ if gwPolicy == nil && listenerPolicy == nil {
+ // No parent policy found, fall back to current policy
+ if err := t.translateSecurityPolicyForRoute(policy, targetedRoute, currTarget, resources, xdsIR, &listener.Name); err != nil {
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+ egv1a1.PolicyReasonInvalid,
+ status.Error2ConditionMsg(err),
+ policy.Generation,
+ )
+ }
+ continue
+ }
+
+ parentPolicy := gwPolicy
+ if listenerPolicy != nil {
+ parentPolicy = listenerPolicy
+ }
+
+ // Merge with parent policy
+ mergedPolicy, err := mergeSecurityPolicy(policy, parentPolicy)
+ if err != nil {
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+ egv1a1.PolicyReasonInvalid,
+ fmt.Sprintf("error merging policies: %v", err),
+ policy.Generation,
+ )
+ continue
+ }
+
+ if err := validator(mergedPolicy); err != nil {
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+ egv1a1.PolicyReasonInvalid,
+ status.Error2ConditionMsg(err),
+ policy.Generation,
+ )
+ continue
+ }
+
+ // Apply merged policy
+ if err := t.translateSecurityPolicyForRoute(mergedPolicy, targetedRoute, currTarget, resources, xdsIR, &listener.Name); err != nil {
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+ egv1a1.PolicyReasonInvalid,
+ status.Error2ConditionMsg(err),
+ policy.Generation,
+ )
+ continue
+ }
+
+ // Record the merged routes for gateway
+ if _, ok := gatewayPolicyMerged.Routes[listenerMapKey]; !ok {
+ gatewayPolicyMerged.Routes[listenerMapKey] = make(sets.Set[string])
+ }
+ gatewayPolicyMerged.Routes[listenerMapKey].Insert(utils.NamespacedName(targetedRoute).String())
+
+ // Register section name to Gateway index
+ if _, ok := gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName]; !ok {
+ gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName] = make(sets.Set[string])
+ }
+ gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName].Insert(string(listenerMapKey.SectionName))
+
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ egv1a1.PolicyConditionMerged,
+ metav1.ConditionTrue,
+ egv1a1.PolicyReasonMerged,
+ fmt.Sprintf("Merged with policy %s/%s", parentPolicy.Namespace, parentPolicy.Name),
+ policy.Generation,
+ )
+ }
+ }
}
// Set Accepted condition if it is unset
- status.SetAcceptedForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName, policy.Generation)
+ status.SetAcceptedForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, policy.Generation)
// Check for deprecated fields and set warning if any are found
if deprecatedFields := deprecatedFieldsUsedInSecurityPolicy(policy); len(deprecatedFields) > 0 {
- status.SetDeprecatedFieldsWarningForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName, policy.Generation, deprecatedFields)
+ status.SetDeprecatedFieldsWarningForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, policy.Generation, deprecatedFields)
+ }
+
+ // Check if this policy is overridden by other policies targeting at route rule levels
+ // If policy target is route rule, we can skip the check
+ if currTarget.SectionName != nil {
+ return
}
// Check if this policy is overridden by other policies targeting at route rule levels
@@ -320,7 +500,7 @@ func (t *Translator) processSecurityPolicyForRoute(
overriddenTargetsMessage := getOverriddenTargetsMessageForRoute(routeMap[key], currTarget.SectionName)
if overriddenTargetsMessage != "" {
status.SetConditionForPolicyAncestors(&policy.Status,
- parentGateways,
+ ancestorRefs,
t.GatewayControllerName,
egv1a1.PolicyConditionOverridden,
metav1.ConditionTrue,
@@ -335,7 +515,8 @@ func (t *Translator) processSecurityPolicyForGateway(
resources *resource.Resources,
xdsIR resource.XdsIRMap,
gatewayMap map[types.NamespacedName]*policyGatewayTargetContext,
- gatewayRouteMap map[string]map[string]sets.Set[string],
+ gatewayRouteMap *GatewayPolicyRouteMap,
+ gatewayPolicyMergedMap *GatewayPolicyRouteMap,
policy *egv1a1.SecurityPolicy,
currTarget gwapiv1.LocalPolicyTargetReferenceWithSectionName,
) {
@@ -355,23 +536,23 @@ func (t *Translator) processSecurityPolicyForGateway(
// Find its ancestor reference by resolved gateway, even with resolve error
gatewayNN := utils.NamespacedName(targetedGateway)
- parentGateway := getAncestorRefForPolicy(gatewayNN, currTarget.SectionName)
+ ancestorRef := getAncestorRefForPolicy(gatewayNN, currTarget.SectionName)
// Set conditions for resolve error, then skip current gateway
if resolveErr != nil {
status.SetResolveErrorForPolicyAncestor(&policy.Status,
- &parentGateway,
+ &ancestorRef,
t.GatewayControllerName,
policy.Generation,
resolveErr,
)
-
return
}
+ // Set conditions for translation error if it got any
if err := t.translateSecurityPolicyForGateway(policy, targetedGateway, currTarget, resources, xdsIR); err != nil {
status.SetTranslationErrorForPolicyAncestor(&policy.Status,
- &parentGateway,
+ &ancestorRef,
t.GatewayControllerName,
policy.Generation,
status.Error2ConditionMsg(err),
@@ -379,19 +560,30 @@ func (t *Translator) processSecurityPolicyForGateway(
}
// Set Accepted condition if it is unset
- status.SetAcceptedForPolicyAncestor(&policy.Status, &parentGateway, t.GatewayControllerName, policy.Generation)
+ status.SetAcceptedForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation)
- // Check if this policy is overridden by other policies targeting at route and listener levels
- overriddenTargetsMessage := getOverriddenTargetsMessageForGateway(
- gatewayMap[gatewayNN], gatewayRouteMap[gatewayNN.String()], currTarget.SectionName)
- if overriddenTargetsMessage != "" {
+ overriddenMessage, mergedMessage := getOverriddenAndMergedTargetsMessageForGateway(
+ gatewayMap[gatewayNN], gatewayRouteMap, gatewayPolicyMergedMap, currTarget.SectionName)
+
+ if mergedMessage != "" {
status.SetConditionForPolicyAncestor(&policy.Status,
- &parentGateway,
+ &ancestorRef,
+ t.GatewayControllerName,
+ egv1a1.PolicyConditionMerged,
+ metav1.ConditionTrue,
+ egv1a1.PolicyReasonMerged,
+ "This policy is being merged by other securityPolicies for "+mergedMessage,
+ policy.Generation,
+ )
+ }
+ if overriddenMessage != "" {
+ status.SetConditionForPolicyAncestor(&policy.Status,
+ &ancestorRef,
t.GatewayControllerName,
egv1a1.PolicyConditionOverridden,
metav1.ConditionTrue,
egv1a1.PolicyReasonOverridden,
- "This policy is being overridden by other securityPolicies for "+overriddenTargetsMessage,
+ "This policy is being overridden by other securityPolicies for "+overriddenMessage,
policy.Generation,
)
}
@@ -628,6 +820,7 @@ func (t *Translator) translateSecurityPolicyForRoute(
target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
resources *resource.Resources,
xdsIR resource.XdsIRMap,
+ policyTargetListener *gwapiv1.SectionName,
) error {
// Build IR
var (
@@ -748,6 +941,10 @@ func (t *Translator) translateSecurityPolicyForRoute(
switch route.GetRouteType() {
case resource.KindTCPRoute:
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 {
// If target.SectionName is specified it must match the route-rule section name
@@ -767,6 +964,10 @@ func (t *Translator) translateSecurityPolicyForRoute(
}
case resource.KindHTTPRoute, resource.KindGRPCRoute:
for _, listener := range parentRefCtx.listeners {
+ // If policyTargetListener is set, only apply to the specific listener
+ if policyTargetListener != nil && *policyTargetListener != listener.Name {
+ continue
+ }
irListener := xdsIR[irKey].GetHTTPListener(irListenerName(listener))
if irListener != nil {
for _, r := range irListener.Routes {
@@ -2124,3 +2325,12 @@ func defaultAuthorizationRuleName(policy *egv1a1.SecurityPolicy, index int) stri
irConfigName(policy),
strconv.Itoa(index))
}
+
+// mergeSecurityPolicy merges a route-level SecurityPolicy with a parent (Gateway/Listener) SecurityPolicy.
+func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, error) {
+ if routePolicy.Spec.MergeType == nil || parentPolicy == nil {
+ return routePolicy, nil
+ }
+
+ return utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, *routePolicy.Spec.MergeType)
+}
diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go
index 7967a781f3..820e254511 100644
--- a/internal/gatewayapi/securitypolicy_test.go
+++ b/internal/gatewayapi/securitypolicy_test.go
@@ -942,14 +942,22 @@ func Test_SecurityPolicy_TCP_Invalid_setsStatus_and_returns(t *testing.T) {
}
routeMap[key] = &policyRouteTargetContext{RouteContext: tcpRoute}
- gatewayRouteMap := make(map[string]map[string]sets.Set[string])
+ gatewayRouteMap := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string]),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string]),
+ }
resources := resource.NewResources()
xdsIR := make(resource.XdsIRMap)
trContext.SetServices(resources.Services)
tr.TranslatorContext = trContext
// Process the policy - this should set error status
- tr.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, policy, target)
+ gatewayPolicyMap := make(map[NamespacedNameWithSection]*egv1a1.SecurityPolicy)
+ gatewayPolicyMerged := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string]),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string]),
+ }
+ tr.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, target)
// Assert that the policy has a False condition (error was set)
require.True(t, hasParentFalseCondition(policy))
@@ -1019,14 +1027,22 @@ func Test_SecurityPolicy_HTTP_Invalid_setsStatus_and_returns(t *testing.T) {
}
routeMap[key] = &policyRouteTargetContext{RouteContext: httpRoute}
- gatewayRouteMap := make(map[string]map[string]sets.Set[string])
+ gatewayRouteMap := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string]),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string]),
+ }
resources := resource.NewResources()
xdsIR := make(resource.XdsIRMap)
trContext.SetServices(resources.Services)
tr.TranslatorContext = trContext
// Process the policy - this should set error status
- tr.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, policy, target)
+ gatewayPolicyMap := make(map[NamespacedNameWithSection]*egv1a1.SecurityPolicy)
+ gatewayPolicyMerged := &GatewayPolicyRouteMap{
+ Routes: make(map[NamespacedNameWithSection]sets.Set[string]),
+ SectionIndex: make(map[types.NamespacedName]sets.Set[string]),
+ }
+ tr.processSecurityPolicyForRoute(resources, xdsIR, routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, target)
// Assert that the policy has a False condition (error was set)
require.True(t, hasParentFalseCondition(policy))
@@ -1376,3 +1392,238 @@ func Test_buildContextExtensions(t *testing.T) {
})
}
}
+
+func TestMergeSecurityPolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ routePolicy *egv1a1.SecurityPolicy
+ parentPolicy *egv1a1.SecurityPolicy
+ wantSpec egv1a1.SecurityPolicySpec
+ wantErr bool
+ }{
+ {
+ name: "merge with StrategicMerge - different fields",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ },
+ },
+ {
+ name: "no merge when MergeType is nil",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ },
+ },
+ {
+ name: "no merge when parentPolicy is nil",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ },
+ },
+ parentPolicy: nil,
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ },
+ },
+ {
+ name: "merge CORS with Authorization",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://example.com"},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: ptr.To(egv1a1.AuthorizationActionDeny),
+ Rules: []egv1a1.AuthorizationRule{
+ {Name: ptr.To("allow-admin"), Action: egv1a1.AuthorizationActionAllow},
+ },
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://example.com"},
+ },
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: ptr.To(egv1a1.AuthorizationActionDeny),
+ Rules: []egv1a1.AuthorizationRule{
+ {Name: ptr.To("allow-admin"), Action: egv1a1.AuthorizationActionAllow},
+ },
+ },
+ },
+ },
+ {
+ name: "merge with JSONMerge type",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.JSONMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.JSONMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ },
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ },
+ },
+ {
+ name: "merge multiple fields - JWT, CORS, and BasicAuth",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: ptr.To(egv1a1.AuthorizationActionAllow),
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ JWT: &egv1a1.JWT{
+ Providers: []egv1a1.JWTProvider{{Name: "route-jwt"}},
+ },
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ },
+ BasicAuth: &egv1a1.BasicAuth{
+ Users: gwapiv1.SecretObjectReference{Name: "gateway-users"},
+ },
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: ptr.To(egv1a1.AuthorizationActionAllow),
+ },
+ },
+ },
+ {
+ name: "merge same field - route overrides parent",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ AllowMethods: []string{"GET", "POST"},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://gateway.com"},
+ AllowMethods: []string{"GET"},
+ },
+ },
+ },
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: ptr.To(egv1a1.StrategicMerge),
+ CORS: &egv1a1.CORS{
+ AllowOrigins: []egv1a1.Origin{"https://route.com"},
+ AllowMethods: []string{"GET", "POST"},
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := mergeSecurityPolicy(tt.routePolicy, tt.parentPolicy)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("mergeSecurityPolicy() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if err == nil {
+ // Compare all fields that could be merged
+ require.Equal(t, tt.wantSpec.MergeType, got.Spec.MergeType, "MergeType should match")
+ require.Equal(t, tt.wantSpec.JWT, got.Spec.JWT, "JWT should match")
+ require.Equal(t, tt.wantSpec.BasicAuth, got.Spec.BasicAuth, "BasicAuth should match")
+ require.Equal(t, tt.wantSpec.CORS, got.Spec.CORS, "CORS should match")
+ require.Equal(t, tt.wantSpec.Authorization, got.Spec.Authorization, "Authorization should match")
+ }
+ })
+ }
+}
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.in.yaml
new file mode 100644
index 0000000000..50083bdf3e
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.in.yaml
@@ -0,0 +1,74 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-1
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/api"
+ backendRefs:
+ - name: service-1
+ port: 8080
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: policy-for-gateway
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http
+ jwt:
+ providers:
+ - name: example
+ issuer: https://www.example.com
+ audiences:
+ - example.com
+ remoteJWKS:
+ uri: https://www.example.com/.well-known/jwks.json
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: policy-for-route
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ cors:
+ allowOrigins:
+ - "https://api.example.com"
+ allowMethods:
+ - GET
+ - POST
+ - PUT
+ allowHeaders:
+ - "authorization"
+ - "content-type"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.out.yaml
new file mode 100644
index 0000000000..e6d1bd47c2
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-jwt-cors.out.yaml
@@ -0,0 +1,276 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http
+ port: 80
+ protocol: HTTP
+ 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: http
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-1
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ value: /api
+ 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: http
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-1/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-route
+ namespace: default
+ spec:
+ cors:
+ allowHeaders:
+ - authorization
+ - content-type
+ allowMethods:
+ - GET
+ - POST
+ - PUT
+ allowOrigins:
+ - https://api.example.com
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/policy-for-gateway
+ reason: Merged
+ status: "True"
+ type: Merged
+ - 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:
+ name: policy-for-gateway
+ namespace: envoy-gateway
+ spec:
+ jwt:
+ providers:
+ - audiences:
+ - example.com
+ issuer: https://www.example.com
+ name: example
+ remoteJWKS:
+ uri: https://www.example.com/.well-known/jwks.json
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'This policy is being merged by other securityPolicies for these
+ routes: [default/httproute-1]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-1/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /api
+ security:
+ cors:
+ allowHeaders:
+ - authorization
+ - content-type
+ allowMethods:
+ - GET
+ - POST
+ - PUT
+ allowOrigins:
+ - distinct: false
+ exact: https://api.example.com
+ name: ""
+ jwt:
+ providers:
+ - audiences:
+ - example.com
+ issuer: https://www.example.com
+ name: example
+ remoteJWKS:
+ uri: https://www.example.com/.well-known/jwks.json
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml
new file mode 100644
index 0000000000..5dc3bb2a76
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml
@@ -0,0 +1,155 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http-a
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: All
+ - name: http-b
+ protocol: HTTP
+ port: 8080
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-1
+ spec:
+ parentRefs:
+ # Route attaches to BOTH listeners
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http-a
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http-b
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-1
+ port: 8080
+securityPolicies:
+# Policy for listener-a: BasicAuth
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: policy-for-listener-a
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-a
+ basicAuth:
+ users:
+ name: users-secret-a
+# Policy for listener-b: ExtAuth (different from listener-a)
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: policy-for-listener-b
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-b
+ extAuth:
+ http:
+ backendRefs:
+ - name: auth-service
+ namespace: envoy-gateway
+ port: 9001
+# Route-level policy that merges with parent
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: policy-for-route
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ cors:
+ allowOrigins:
+ - "https://www.example.com"
+ allowMethods:
+ - GET
+ - POST
+services:
+- apiVersion: v1
+ kind: Service
+ metadata:
+ namespace: envoy-gateway
+ name: auth-service
+ spec:
+ ports:
+ - port: 9001
+ name: http
+ protocol: TCP
+referenceGrants:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+ kind: ReferenceGrant
+ metadata:
+ namespace: envoy-gateway
+ name: allow-securitypolicy-to-auth-service
+ spec:
+ from:
+ - group: gateway.envoyproxy.io
+ kind: SecurityPolicy
+ namespace: default
+ to:
+ - group: ""
+ kind: Service
+endpointSlices:
+- apiVersion: discovery.k8s.io/v1
+ kind: EndpointSlice
+ metadata:
+ name: endpointslice-auth-service
+ namespace: envoy-gateway
+ labels:
+ kubernetes.io/service-name: auth-service
+ addressType: IPv4
+ ports:
+ - name: http
+ protocol: TCP
+ port: 9001
+ endpoints:
+ - addresses:
+ - 10.0.0.1
+ conditions:
+ ready: true
+secrets:
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: envoy-gateway
+ name: users-secret-a
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: default
+ name: users-secret-a
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml
new file mode 100644
index 0000000000..4b72cecb33
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml
@@ -0,0 +1,449 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http-a
+ port: 80
+ protocol: HTTP
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http-b
+ port: 8080
+ protocol: HTTP
+ 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: http-a
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+ - 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: http-b
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-1
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-a
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-b
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ value: /foo
+ 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: http-a
+ - 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: http-b
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-1/http-a
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ - name: envoy-gateway/gateway-1/http-b
+ ports:
+ - containerPort: 8080
+ name: http-8080
+ protocol: HTTP
+ servicePort: 8080
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-route
+ namespace: default
+ spec:
+ cors:
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - https://www.example.com
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-a
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/policy-for-listener-a
+ reason: Merged
+ status: "True"
+ type: Merged
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-b
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/policy-for-listener-b
+ reason: Merged
+ status: "True"
+ type: Merged
+ - 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:
+ name: policy-for-listener-a
+ namespace: envoy-gateway
+ spec:
+ basicAuth:
+ users:
+ group: null
+ kind: null
+ name: users-secret-a
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-a
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-a
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'This policy is being merged by other securityPolicies for these
+ routes: [default/httproute-1]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-listener-b
+ namespace: envoy-gateway
+ spec:
+ extAuth:
+ http:
+ backendRefs:
+ - name: auth-service
+ namespace: envoy-gateway
+ port: 9001
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-b
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-b
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'This policy is being merged by other securityPolicies for these
+ routes: [default/httproute-1]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-a
+ name: envoy-gateway/gateway-1/http-a
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-1/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ basicAuth:
+ name: securitypolicy/default/policy-for-route
+ users: '[redacted]'
+ cors:
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - distinct: false
+ exact: https://www.example.com
+ name: ""
+ - address: 0.0.0.0
+ externalPort: 8080
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-b
+ name: envoy-gateway/gateway-1/http-b
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 8080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-1/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ cors:
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - distinct: false
+ exact: https://www.example.com
+ name: ""
+ extAuth:
+ http:
+ authority: auth-service.envoy-gateway:9001
+ destination:
+ metadata:
+ kind: SecurityPolicy
+ name: policy-for-route
+ namespace: default
+ name: securitypolicy/default/policy-for-route/extauth/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 10.0.0.1
+ port: 9001
+ metadata:
+ kind: Service
+ name: auth-service
+ namespace: envoy-gateway
+ sectionName: "9001"
+ name: securitypolicy/default/policy-for-route/extauth/0/backend/0
+ protocol: HTTP
+ weight: 1
+ path: ""
+ name: securitypolicy/default/policy-for-route
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.in.yaml
new file mode 100644
index 0000000000..8898dadfe3
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.in.yaml
@@ -0,0 +1,110 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: tcp
+ protocol: TCP
+ port: 9000
+ allowedRoutes:
+ namespaces:
+ from: All
+tcpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+ kind: TCPRoute
+ metadata:
+ namespace: default
+ name: tcproute-1
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: tcp-service-1
+ port: 8080
+services:
+- apiVersion: v1
+ kind: Service
+ metadata:
+ namespace: default
+ name: tcp-service-1
+ spec:
+ ports:
+ - port: 8080
+ name: tcp
+ protocol: TCP
+endpointSlices:
+- apiVersion: discovery.k8s.io/v1
+ kind: EndpointSlice
+ metadata:
+ name: endpointslice-tcp-service-1
+ namespace: default
+ labels:
+ kubernetes.io/service-name: tcp-service-1
+ addressType: IPv4
+ ports:
+ - name: tcp
+ protocol: TCP
+ port: 8080
+ endpoints:
+ - addresses:
+ - "10.244.0.5"
+ conditions:
+ ready: true
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: policy-for-gateway
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: tcp
+ basicAuth:
+ users:
+ name: users-secret
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: policy-for-route
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcproute-1
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: "allow-from-internal"
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+secrets:
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: envoy-gateway
+ name: users-secret
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: default
+ name: users-secret
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml
new file mode 100644
index 0000000000..f4b5951fc9
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-tcp-invalid.out.yaml
@@ -0,0 +1,228 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: tcp
+ port: 9000
+ protocol: TCP
+ 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: tcp
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-1/tcp
+ ports:
+ - containerPort: 9000
+ name: tcp-9000
+ protocol: TCP
+ servicePort: 9000
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-route
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-from-internal
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcproute-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: tcp
+ conditions:
+ - lastTransitionTime: null
+ message: Only authorization is supported for TCP (routes/listeners).
+ reason: Invalid
+ status: "False"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-gateway
+ namespace: envoy-gateway
+ spec:
+ basicAuth:
+ users:
+ group: null
+ kind: null
+ name: users-secret
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: tcp
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: tcp
+ 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/tcproute-1]'
+ reason: Overridden
+ status: "True"
+ type: Overridden
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+tcpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+ kind: TCPRoute
+ metadata:
+ name: tcproute-1
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: tcp-service-1
+ port: 8080
+ 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: tcp
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ tcp:
+ - address: 0.0.0.0
+ externalPort: 9000
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: tcp
+ name: envoy-gateway/gateway-1/tcp
+ port: 9000
+ routes:
+ - destination:
+ metadata:
+ kind: TCPRoute
+ name: tcproute-1
+ namespace: default
+ name: tcproute/default/tcproute-1/rule/-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 10.244.0.5
+ port: 8080
+ metadata:
+ kind: Service
+ name: tcp-service-1
+ namespace: default
+ sectionName: "8080"
+ name: tcproute/default/tcproute-1/rule/-1/backend/0
+ protocol: TCP
+ weight: 1
+ metadata:
+ kind: TCPRoute
+ name: tcproute-1
+ namespace: default
+ name: tcproute/default/tcproute-1
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge.in.yaml
new file mode 100644
index 0000000000..052513bf81
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge.in.yaml
@@ -0,0 +1,84 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-1
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-1
+ port: 8080
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: policy-for-gateway
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http
+ basicAuth:
+ users:
+ name: users-secret
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: policy-for-route
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ cors:
+ allowOrigins:
+ - "https://www.example.com"
+ allowMethods:
+ - GET
+ - POST
+ allowHeaders:
+ - "x-header-1"
+secrets:
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: envoy-gateway
+ name: users-secret
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ namespace: default
+ name: users-secret
+ type: Opaque
+ data:
+ .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml
new file mode 100644
index 0000000000..50596180f7
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml
@@ -0,0 +1,264 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http
+ port: 80
+ protocol: HTTP
+ 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: http
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-1
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ value: /foo
+ 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: http
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-1/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: policy-for-route
+ namespace: default
+ spec:
+ cors:
+ allowHeaders:
+ - x-header-1
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - https://www.example.com
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/policy-for-gateway
+ reason: Merged
+ status: "True"
+ type: Merged
+ - 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:
+ name: policy-for-gateway
+ namespace: envoy-gateway
+ spec:
+ basicAuth:
+ users:
+ group: null
+ kind: null
+ name: users-secret
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'This policy is being merged by other securityPolicies for these
+ routes: [default/httproute-1]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-1/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ basicAuth:
+ name: securitypolicy/default/policy-for-route
+ users: '[redacted]'
+ cors:
+ allowHeaders:
+ - x-header-1
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - distinct: false
+ exact: https://www.example.com
+ name: ""
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index cdce7e9f52..13a03a8666 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -12,6 +12,7 @@ security updates: |
new features: |
Added support for configuring optional health check configuration.
Added support for shadow mode in local rate limiting.
+ Added support for MergeType in SecurityPolicy to enable route-level policies to merge with parent Gateway/Listener policies, similar to BackendTrafficPolicy.
Added `egctl config envoy-gateway` commands to retrieve Envoy Gateway admin config dumps.
The DirectResponse body in HTTPFilter now supports Envoy command operators for dynamic content. See https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators for more details.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index bad944cd57..1fd2d4b61b 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -3582,6 +3582,7 @@ MergeType defines the type of merge operation
_Appears in:_
- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
- [KubernetesPatchSpec](#kubernetespatchspec)
+- [SecurityPolicySpec](#securitypolicyspec)
| Value | Description |
| ----- | ----------- |
@@ -5088,6 +5089,7 @@ _Appears in:_
| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](#localpolicytargetreferencewithsectionname)_ | true | | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect
Deprecated: use targetRefs/targetSelectors instead |
| `targetRefs` | _LocalPolicyTargetReferenceWithSectionName array_ | true | | TargetRefs are the names of the Gateway resources this policy
is being attached to. |
| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | | TargetSelectors allow targeting resources for this policy based on labels |
+| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType determines how this configuration is merged with existing SecurityPolicy
configurations targeting a parent resource. When set, this configuration will be merged
into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
This field cannot be set when targeting a parent resource (Gateway).
If unset, no merging occurs, and only the most specific configuration takes effect. |
| `apiKeyAuth` | _[APIKeyAuth](#apikeyauth)_ | false | | APIKeyAuth defines the configuration for the API Key Authentication. |
| `cors` | _[CORS](#cors)_ | false | | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). |
| `basicAuth` | _[BasicAuth](#basicauth)_ | false | | BasicAuth defines the configuration for the HTTP Basic Authentication. |
diff --git a/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
index 9470efa15f..f7bdb45234 100644
--- a/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
+++ b/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
@@ -203,6 +203,83 @@ spec:
In the example, policy A affects only the HTTPS listener, while policy B applies to the rest of the listeners in the gateway. Since Policy A is more specific, the system will show Overridden=True for Policy B on the https listener.
+When the `mergeType` field is unset, no merging occurs and only the most specific configuration takes effect. However, policies can be configured to merge with parent policies using the `mergeType` field (see [Policy Merging](#policy-merging) section below).
+
+## Policy Merging
+
+SecurityPolicy supports merging configurations using the `mergeType` field, which allows route-level or route rule-level policies to combine with gateway-level or listener-level policies rather than completely overriding them. This enables layered security strategies where platform teams can set baseline security configurations at the Gateway level, while application teams can add specific security policies for their routes.
+
+When merging occurs, route-level policies will merge with either a gateway-level or listener-level policy, but not both. If both gateway and listener policies exist, the listener-level policy takes precedence.
+
+### Merge Types
+
+- **StrategicMerge**: Uses Kubernetes strategic merge patch semantics, providing intelligent merging for complex data structures including arrays
+- **JSONMerge**: Uses RFC 7396 JSON Merge Patch semantics, with simple replacement strategy where arrays are completely replaced
+
+### Example Usage
+
+Here's an example demonstrating policy merging for combining authentication and CORS policies:
+
+```yaml
+# Platform team: Gateway-level policy with baseline authentication
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: gateway-security
+ namespace: default
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: my-gateway
+ sectionName: https-listener
+ basicAuth:
+ users:
+ name: basic-auth-secret
+
+---
+# Application team: Route-level policy with CORS configuration
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: route-security
+ namespace: default
+spec:
+ mergeType: StrategicMerge # Enables merging with gateway policy
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: my-route
+ cors:
+ allowOrigins:
+ - exact: https://example.com
+ allowMethods:
+ - GET
+ - POST
+ allowHeaders:
+ - x-header-1
+```
+
+In this example, the route-level policy merges with the gateway-level policy, resulting in both security controls being enforced: the baseline BasicAuth (from Gateway) and the route-specific CORS policy (from Route). This allows platform teams to enforce organization-wide authentication requirements while enabling application teams to configure route-specific cross-origin policies.
+
+### Key Constraints
+
+- The `mergeType` field can only be set on policies targeting child resources (like HTTPRoute), not parent resources (like Gateway)
+- When `mergeType` is unset, no merging occurs - only the most specific policy takes effect
+- The merged configuration combines both policies, enabling layered security strategies
+- When the same security feature is configured in both parent and child policies (e.g., both define CORS), the child policy's configuration takes precedence for that specific feature
+
+### Important: Namespace Behavior with Secret References
+
+When policies are merged, secret references inherited from parent policies must be resolvable from the **route policy's namespace**. This is because the merged policy retains the identity (including namespace) of the route-level policy.
+
+**Example scenario:**
+- Gateway policy in namespace `envoy-gateway` references `basic-auth-secret`
+- Route policy in namespace `default` merges with the gateway policy
+- The secret `basic-auth-secret` must exist in the `default` namespace for the merged policy to work
+
+**Best Practice:** When using policy merging with secret-based authentication (BasicAuth, OIDC, JWT, APIKeyAuth), ensure that required secrets are available in each route's namespace, or design your namespace strategy accordingly.
+
## Related Resources
- [API Key Authentication](../../tasks/security/apikey-auth.md)
- [Basic Authentication](../../tasks/security/basic-auth.md)
diff --git a/test/e2e/testdata/securitypolicy-merged.yaml b/test/e2e/testdata/securitypolicy-merged.yaml
new file mode 100644
index 0000000000..a1ed0d4ea4
--- /dev/null
+++ b/test/e2e/testdata/securitypolicy-merged.yaml
@@ -0,0 +1,77 @@
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+ name: sp-merged
+ namespace: gateway-conformance-infra
+spec:
+ gatewayClassName: "{GATEWAY_CLASS_NAME}"
+ listeners:
+ - name: listener-1
+ hostname: "listener1.merged.example.com"
+ port: 80
+ protocol: HTTP
+ allowedRoutes:
+ namespaces:
+ from: Same
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: sp-merged-route
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: sp-merged
+ sectionName: listener-1
+ hostnames: ["listener1.merged.example.com"]
+ rules:
+ - matches:
+ - path:
+ value: "/merged"
+ backendRefs:
+ - name: infra-backend-v1
+ port: 8080
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: sp-merged-gateway
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: sp-merged
+ sectionName: listener-1
+ authorization:
+ defaultAction: Deny
+ rules:
+ - name: allow-all
+ action: Allow
+ principal:
+ clientCIDRs:
+ - 0.0.0.0/0
+ - ::/0
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: sp-merged-route
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: sp-merged-route
+ mergeType: StrategicMerge
+ cors:
+ allowOrigins:
+ - "https://www.example.com"
+ - "https://www.foo.com"
+ allowMethods:
+ - GET
+ - POST
+ allowHeaders:
+ - "x-header-1"
+ - "x-header-2"
diff --git a/test/e2e/tests/securitypolicy_merged.go b/test/e2e/tests/securitypolicy_merged.go
new file mode 100644
index 0000000000..ad04fb270d
--- /dev/null
+++ b/test/e2e/tests/securitypolicy_merged.go
@@ -0,0 +1,91 @@
+// 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 (
+ "testing"
+
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/utils/ptr"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+ httputils "sigs.k8s.io/gateway-api/conformance/utils/http"
+ "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, SecurityPolicyMergedTest)
+}
+
+var SecurityPolicyMergedTest = suite.ConformanceTest{
+ ShortName: "SecurityPolicyMerged",
+ Description: "Test section level policy attach and merged parent policy for SecurityPolicy",
+ Manifests: []string{"testdata/securitypolicy-merged.yaml"},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ t.Run("SecurityPolicyMerged", func(t *testing.T) {
+ ns := "gateway-conformance-infra"
+ routeNN := types.NamespacedName{Name: "sp-merged-route", Namespace: ns}
+ gwNN := types.NamespacedName{Name: "sp-merged", Namespace: ns}
+
+ gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t,
+ suite.Client,
+ suite.TimeoutConfig,
+ suite.ControllerName,
+ kubernetes.NewGatewayRef(gwNN),
+ routeNN,
+ )
+
+ ancestorRef := gwapiv1a2.ParentReference{
+ Group: gatewayapi.GroupPtr(gwapiv1.GroupName),
+ Kind: gatewayapi.KindPtr(resource.KindGateway),
+ Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+ Name: gwapiv1.ObjectName(gwNN.Name),
+ SectionName: ptr.To(gwapiv1.SectionName("listener-1")),
+ }
+
+ // Verify gateway-level policy is accepted
+ SecurityPolicyMustBeAccepted(t,
+ suite.Client,
+ types.NamespacedName{Name: "sp-merged-gateway", Namespace: ns},
+ suite.ControllerName,
+ ancestorRef,
+ )
+
+ // Verify route-level policy has Merged condition
+ SecurityPolicyMustBeMerged(t,
+ suite.Client,
+ types.NamespacedName{Name: "sp-merged-route", Namespace: ns},
+ suite.ControllerName,
+ ancestorRef,
+ )
+
+ // Test that merged policies work - Authorization (from gateway) allows, CORS (from route) adds headers
+ expectedResponse := httputils.ExpectedResponse{
+ Namespace: ns,
+ Request: httputils.Request{
+ Host: "listener1.merged.example.com",
+ Path: "/merged",
+ Headers: map[string]string{
+ "Origin": "https://www.example.com",
+ },
+ },
+ Response: httputils.Response{
+ StatusCode: 200,
+ Headers: map[string]string{
+ "Access-Control-Allow-Origin": "https://www.example.com",
+ },
+ },
+ }
+ httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+ })
+ },
+}
diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go
index 495d2830d1..8e8416f176 100644
--- a/test/e2e/tests/utils.go
+++ b/test/e2e/tests/utils.go
@@ -170,6 +170,29 @@ func SecurityPolicyMustFail(
require.NoErrorf(t, waitErr, "error waiting for SecurityPolicy to fail with message: %s policy %v", message, policy)
}
+// SecurityPolicyMustBeMerged waits for the specified SecurityPolicy to have Merged condition.
+func SecurityPolicyMustBeMerged(t *testing.T, client client.Client, policyName types.NamespacedName, controllerName string, ancestorRef gwapiv1.ParentReference) {
+ t.Helper()
+
+ waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
+ policy := &egv1a1.SecurityPolicy{}
+ err := client.Get(ctx, policyName, policy)
+ if err != nil {
+ return false, fmt.Errorf("error fetching SecurityPolicy: %w", err)
+ }
+
+ if policyMergedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef) {
+ tlog.Logf(t, "SecurityPolicy has Merged condition: %v", policy)
+ return true, nil
+ }
+
+ tlog.Logf(t, "SecurityPolicy does not have Merged condition yet: %v", policy)
+ return false, nil
+ })
+
+ require.NoErrorf(t, waitErr, "error waiting for SecurityPolicy to have Merged condition")
+}
+
// BackendTrafficPolicyMustBeAccepted waits for the specified BackendTrafficPolicy to be accepted.
func BackendTrafficPolicyMustBeAccepted(t *testing.T, client client.Client, policyName types.NamespacedName, controllerName string, ancestorRef gwapiv1.ParentReference) {
t.Helper()
@@ -318,6 +341,19 @@ func policyAcceptedByAncestor(ancestors []gwapiv1.PolicyAncestorStatus, controll
return false
}
+func policyMergedByAncestor(ancestors []gwapiv1.PolicyAncestorStatus, controllerName string, ancestorRef gwapiv1.ParentReference) bool {
+ for _, ancestor := range ancestors {
+ if string(ancestor.ControllerName) == controllerName && cmp.Equal(ancestor.AncestorRef, ancestorRef) {
+ for _, condition := range ancestor.Conditions {
+ if condition.Type == string(egv1a1.PolicyConditionMerged) && condition.Status == metav1.ConditionTrue {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
// EnvoyExtensionPolicyMustFail waits for an EnvoyExtensionPolicy to fail with the specified reason.
func EnvoyExtensionPolicyMustFail(
t *testing.T, client client.Client, policyName types.NamespacedName,
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 831d23b3ea..d118613d4d 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -50344,6 +50344,14 @@ spec:
required:
- providers
type: object
+ mergeType:
+ description: |-
+ MergeType determines how this configuration is merged with existing SecurityPolicy
+ configurations targeting a parent resource. When set, this configuration will be merged
+ into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
+ This field cannot be set when targeting a parent resource (Gateway).
+ If unset, no merging occurs, and only the most specific configuration takes effect.
+ type: string
oidc:
description: OIDC defines the configuration for the OpenID Connect
(OIDC) authentication.
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 ea8e53af07..5cec3bd962 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -29674,6 +29674,14 @@ spec:
required:
- providers
type: object
+ mergeType:
+ description: |-
+ MergeType determines how this configuration is merged with existing SecurityPolicy
+ configurations targeting a parent resource. When set, this configuration will be merged
+ into a parent SecurityPolicy (i.e. the one targeting a Gateway or Listener).
+ This field cannot be set when targeting a parent resource (Gateway).
+ If unset, no merging occurs, and only the most specific configuration takes effect.
+ type: string
oidc:
description: OIDC defines the configuration for the OpenID Connect
(OIDC) authentication.