diff --git a/api/v1alpha1/authorization_types.go b/api/v1alpha1/authorization_types.go
index 1e52ffacec..033be27014 100644
--- a/api/v1alpha1/authorization_types.go
+++ b/api/v1alpha1/authorization_types.go
@@ -20,8 +20,21 @@ type Authorization struct {
// For example, if there are two rules: the first rule allows the request
// and the second rule denies it, when a request matches both rules, it will be allowed.
//
+ // When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ // `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ // child rules with the same name as parent rules override the parent's
+ // configuration, and child rules with new names are concatenated with the
+ // parent's. If any rule on either side omits `name`, the controller falls
+ // back to `JSONMerge` for the entire merge operation (not just this field):
+ // every spec slice is slice-replaced instead of strategic-merged, matching
+ // the pre-keyed-merge behavior. A `Warning` condition with reason
+ // `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
+ //
+ // +patchMergeKey=name
+ // +patchStrategy=merge
+ //
// +optional
- Rules []AuthorizationRule `json:"rules,omitempty"`
+ Rules []AuthorizationRule `json:"rules,omitempty" patchMergeKey:"name" patchStrategy:"merge"`
// DefaultAction defines the default action to be taken if no rules match.
// If not specified, the default action is Deny.
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 4d618a9f25..52b48d2b74 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
@@ -178,6 +178,16 @@ spec:
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
+
+ When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ child rules with the same name as parent rules override the parent's
+ configuration, and child rules with new names are concatenated with the
+ parent's. If any rule on either side omits `name`, the controller falls
+ back to `JSONMerge` for the entire merge operation (not just this field):
+ every spec slice is slice-replaced instead of strategic-merged, matching
+ the pre-keyed-merge behavior. A `Warning` condition with reason
+ `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
items:
description: AuthorizationRule defines a single authorization
rule.
diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 47686dab2d..df968fc892 100644
--- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -177,6 +177,16 @@ spec:
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
+
+ When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ child rules with the same name as parent rules override the parent's
+ configuration, and child rules with new names are concatenated with the
+ parent's. If any rule on either side omits `name`, the controller falls
+ back to `JSONMerge` for the entire merge operation (not just this field):
+ every spec slice is slice-replaced instead of strategic-merged, matching
+ the pre-keyed-merge behavior. A `Warning` condition with reason
+ `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
items:
description: AuthorizationRule defines a single authorization
rule.
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 463042508a..13a7e6cb01 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -439,7 +439,7 @@ func (t *Translator) processSecurityPolicyForRoute(
}
// Merge with parent policy
- mergedPolicy, owners, err := mergeSecurityPolicy(policy, parentPolicy)
+ mergedPolicy, owners, authzRulesMergeFellBack, err := mergeSecurityPolicy(policy, parentPolicy)
if err != nil {
status.SetConditionForPolicyAncestor(&policy.Status,
&ancestorRef,
@@ -498,6 +498,27 @@ func (t *Translator) processSecurityPolicyForRoute(
fmt.Sprintf("Merged with policy %s/%s", parentPolicy.Namespace, parentPolicy.Name),
policy.Generation,
)
+
+ // Surface a warning when the requested StrategicMerge was downgraded
+ // to JSONMerge because an authorization rule omits its `name` merge
+ // key. The downgrade applies to the entire policy spec — not only
+ // `authorization.rules` — so the user sees that other slices
+ // (e.g. `extAuth.contextExtensions`, `jwt.providers`) are also
+ // slice-replaced instead of strategic-merged.
+ if authzRulesMergeFellBack {
+ status.SetWarningForPolicyAncestor(&policy.Status,
+ &ancestorRef,
+ t.GatewayControllerName,
+ status.PolicyReasonAuthorizationRulesMergeFallback,
+ fmt.Sprintf("policy was merged with %s/%s using JSONMerge (slice-replace for all fields) "+
+ "instead of the requested StrategicMerge because one or more `authorization.rules` "+
+ "omit the `name` field used as the strategic-merge key; this affects every spec field, "+
+ "not only `authorization.rules`. Set a unique `name` on every authorization rule to "+
+ "keep the requested StrategicMerge semantics",
+ parentPolicy.Namespace, parentPolicy.Name),
+ policy.Generation,
+ )
+ }
}
}
}
@@ -2576,15 +2597,45 @@ func policyOwnerOr(owner, fallback *egv1a1.SecurityPolicy) *egv1a1.SecurityPolic
}
// mergeSecurityPolicy merges a route-level SecurityPolicy with a parent (Gateway/Listener) SecurityPolicy.
-func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, *securityPolicyOwners, error) {
+// It also reports whether the requested StrategicMerge for the whole policy fell back to JSONMerge
+// because an authorization rule omits the `name` strategic-merge key.
+func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, *securityPolicyOwners, bool, error) {
if routePolicy.Spec.MergeType == nil || parentPolicy == nil {
- return routePolicy, nil, nil
- }
- mergedPolicy, err := utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, *routePolicy.Spec.MergeType)
+ return routePolicy, nil, false, nil
+ }
+ mergeType := *routePolicy.Spec.MergeType
+ // Strategic merge of authorization.rules is keyed by the rule's `name`. If
+ // any rule on either side omits its name, strategic merge would fail with
+ // `does not contain declared merge key: name`. Fall back to JSONMerge for
+ // the entire policy — JSONMerge slice-replaces every array (including
+ // authorization.rules), matching the pre-keyed-merge behavior for unnamed
+ // rules — instead of rejecting the child policy.
+ authzRulesMergeFellBack := mergeType == egv1a1.StrategicMerge && hasUnnamedAuthorizationRule(parentPolicy, routePolicy)
+ if authzRulesMergeFellBack {
+ mergeType = egv1a1.JSONMerge
+ }
+ mergedPolicy, err := utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, mergeType)
if err != nil {
- return nil, nil, err
+ return nil, nil, false, err
+ }
+ return mergedPolicy, buildSecurityPolicyOwners(routePolicy, parentPolicy), authzRulesMergeFellBack, nil
+}
+
+// hasUnnamedAuthorizationRule reports whether any of the given policies declares
+// an authorization rule whose `name` is empty. Used to skip the keyed strategic
+// merge path on the Authorization.Rules slice.
+func hasUnnamedAuthorizationRule(policies ...*egv1a1.SecurityPolicy) bool {
+ for _, p := range policies {
+ if p == nil || p.Spec.Authorization == nil {
+ continue
+ }
+ for _, r := range p.Spec.Authorization.Rules {
+ if r.Name == nil || *r.Name == "" {
+ return true
+ }
+ }
}
- return mergedPolicy, buildSecurityPolicyOwners(routePolicy, parentPolicy), nil
+ return false
}
// ownerOf returns route if routeOwns(route) is true, otherwise parent.
diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go
index 189db2ff32..6c4c9559ea 100644
--- a/internal/gatewayapi/securitypolicy_test.go
+++ b/internal/gatewayapi/securitypolicy_test.go
@@ -1695,6 +1695,7 @@ func TestMergeSecurityPolicy(t *testing.T) {
parentPolicy *egv1a1.SecurityPolicy
wantSpec egv1a1.SecurityPolicySpec
wantErr bool
+ wantFellBack bool
}{
{
name: "merge with StrategicMerge - different fields",
@@ -1902,11 +1903,54 @@ func TestMergeSecurityPolicy(t *testing.T) {
},
},
},
+ {
+ name: "fallback to JSONMerge when an authorization rule omits its name",
+ routePolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "route-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ MergeType: new(egv1a1.StrategicMerge),
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: new(egv1a1.AuthorizationActionDeny),
+ Rules: []egv1a1.AuthorizationRule{{
+ // No Name: forces the keyed strategic merge to fall back.
+ Action: egv1a1.AuthorizationActionAllow,
+ Principal: egv1a1.Principal{ClientCIDRs: []egv1a1.CIDR{"1.2.3.4/32"}},
+ }},
+ },
+ },
+ },
+ parentPolicy: &egv1a1.SecurityPolicy{
+ ObjectMeta: metav1.ObjectMeta{Name: "gateway-policy", Namespace: "default"},
+ Spec: egv1a1.SecurityPolicySpec{
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: new(egv1a1.AuthorizationActionAllow),
+ Rules: []egv1a1.AuthorizationRule{{
+ Name: new("parent-internal"),
+ Action: egv1a1.AuthorizationActionAllow,
+ Principal: egv1a1.Principal{ClientCIDRs: []egv1a1.CIDR{"10.0.0.0/8"}},
+ }},
+ },
+ },
+ },
+ // JSONMerge slice-replaces rules with the route's array while the
+ // route's DefaultAction overrides the parent's.
+ wantSpec: egv1a1.SecurityPolicySpec{
+ MergeType: new(egv1a1.StrategicMerge),
+ Authorization: &egv1a1.Authorization{
+ DefaultAction: new(egv1a1.AuthorizationActionDeny),
+ Rules: []egv1a1.AuthorizationRule{{
+ Action: egv1a1.AuthorizationActionAllow,
+ Principal: egv1a1.Principal{ClientCIDRs: []egv1a1.CIDR{"1.2.3.4/32"}},
+ }},
+ },
+ },
+ wantFellBack: true,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got, _, err := mergeSecurityPolicy(tt.routePolicy, tt.parentPolicy)
+ got, _, fellBack, err := mergeSecurityPolicy(tt.routePolicy, tt.parentPolicy)
if (err != nil) != tt.wantErr {
t.Errorf("mergeSecurityPolicy() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -1918,6 +1962,7 @@ func TestMergeSecurityPolicy(t *testing.T) {
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")
+ require.Equal(t, tt.wantFellBack, fellBack, "authorization rules merge fallback should match")
}
})
}
@@ -2012,7 +2057,7 @@ func Test_securityPolicyOwnerChoose(t *testing.T) {
},
}
- _, owners, err := mergeSecurityPolicy(routePolicy, parentPolicy)
+ _, owners, _, err := mergeSecurityPolicy(routePolicy, parentPolicy)
require.NoError(t, err)
require.NotNil(t, owners)
@@ -2053,7 +2098,7 @@ func Test_securityPolicyOwnerChoose(t *testing.T) {
Spec: egv1a1.SecurityPolicySpec{MergeType: new(egv1a1.StrategicMerge)},
}
- _, owners, err := mergeSecurityPolicy(routePolicy, parentPolicy)
+ _, owners, _, err := mergeSecurityPolicy(routePolicy, parentPolicy)
require.NoError(t, err)
require.NotNil(t, owners)
diff --git a/internal/gatewayapi/status/policy.go b/internal/gatewayapi/status/policy.go
index 6322e42928..ff3e652920 100644
--- a/internal/gatewayapi/status/policy.go
+++ b/internal/gatewayapi/status/policy.go
@@ -25,6 +25,12 @@ const (
// PolicyReasonMultipleWarnings is used with the "Warning" condition when multiple warning
// messages need to be surfaced on the same ancestor.
PolicyReasonMultipleWarnings gwapiv1.PolicyConditionReason = "Warnings"
+
+ // PolicyReasonAuthorizationRulesMergeFallback is used with the "Warning" condition when a
+ // SecurityPolicy requesting StrategicMerge falls back to JSONMerge for the entire policy
+ // because one or more authorization rules omit the `name` field used as the
+ // strategic-merge key. The downgrade affects every spec field, not only `authorization.rules`.
+ PolicyReasonAuthorizationRulesMergeFallback gwapiv1.PolicyConditionReason = "AuthorizationRulesMergeFallback"
)
type PolicyResolveError struct {
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.in.yaml
new file mode 100644
index 0000000000..3617a2dd4e
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.in.yaml
@@ -0,0 +1,213 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-A
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ hostname: a.example.com
+ allowedRoutes:
+ namespaces:
+ from: All
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-B
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ hostname: b.example.com
+ allowedRoutes:
+ namespaces:
+ from: All
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-C
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ hostname: c.example.com
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-A
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-A
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-1
+ port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-B
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-B
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-2
+ port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-C
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-C
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-3
+ port: 8080
+securityPolicies:
+# CASE A: parent has UNNAMED rule, child has NAMED rule. Tests whether parent's
+# nameless rule is preserved / dropped / errors when merge key 'name' is missing.
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: parent-A
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-A
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: child-A
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-A
+ authorization:
+ defaultAction: Deny
+ rules:
+ - name: child-specific
+ action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+# CASE B: parent has NAMED rule, child has UNNAMED rule. Tests whether child's
+# nameless rule is appended / dropped / errors with merge key 'name' missing.
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: parent-B
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-B
+ authorization:
+ defaultAction: Allow
+ rules:
+ - name: parent-internal
+ action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: child-B
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-B
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+# CASE C: both parent and child have UNNAMED rules. Tests the worst case for
+# regression — pre-fix behavior was full slice-replace; we want to confirm we
+# don't break it.
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: parent-C
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-C
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: child-C
+ spec:
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-C
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.out.yaml
new file mode 100644
index 0000000000..014a199f4f
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules-unnamed.out.yaml
@@ -0,0 +1,808 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-A
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ hostname: a.example.com
+ 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
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-B
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ hostname: b.example.com
+ 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
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-C
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ hostname: c.example.com
+ 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-A
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-A
+ 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-A
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-B
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-B
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-2
+ 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-B
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-C
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-C
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-3
+ 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-C
+ namespace: envoy-gateway
+ sectionName: http
+infraIR:
+ envoy-gateway/gateway-A:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-A/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-A
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-A
+ namespace: envoy-gateway-system
+ envoy-gateway/gateway-B:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-B/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-B
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-B
+ namespace: envoy-gateway-system
+ envoy-gateway/gateway-C:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-C/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-C
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-C
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: child-A
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: child-specific
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-A
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-A
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/parent-A
+ reason: Merged
+ status: "True"
+ type: Merged
+ - lastTransitionTime: null
+ message: policy was merged with envoy-gateway/parent-A using JSONMerge (slice-replace
+ for all fields) instead of the requested StrategicMerge because one or more
+ `authorization.rules` omit the `name` field used as the strategic-merge
+ key; this affects every spec field, not only `authorization.rules`. Set
+ a unique `name` on every authorization rule to keep the requested StrategicMerge
+ semantics
+ reason: AuthorizationRulesMergeFallback
+ status: "True"
+ type: Warning
+ - 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: child-B
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-B
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-B
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/parent-B
+ reason: Merged
+ status: "True"
+ type: Merged
+ - lastTransitionTime: null
+ message: policy was merged with envoy-gateway/parent-B using JSONMerge (slice-replace
+ for all fields) instead of the requested StrategicMerge because one or more
+ `authorization.rules` omit the `name` field used as the strategic-merge
+ key; this affects every spec field, not only `authorization.rules`. Set
+ a unique `name` on every authorization rule to keep the requested StrategicMerge
+ semantics
+ reason: AuthorizationRulesMergeFallback
+ status: "True"
+ type: Warning
+ - 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: child-C
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+ mergeType: StrategicMerge
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-C
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-C
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Merged with policy envoy-gateway/parent-C
+ reason: Merged
+ status: "True"
+ type: Merged
+ - lastTransitionTime: null
+ message: policy was merged with envoy-gateway/parent-C using JSONMerge (slice-replace
+ for all fields) instead of the requested StrategicMerge because one or more
+ `authorization.rules` omit the `name` field used as the strategic-merge
+ key; this affects every spec field, not only `authorization.rules`. Set
+ a unique `name` on every authorization rule to keep the requested StrategicMerge
+ semantics
+ reason: AuthorizationRulesMergeFallback
+ status: "True"
+ type: Warning
+ - 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: parent-A
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-A
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-A
+ namespace: envoy-gateway
+ 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-A]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: parent-B
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ name: parent-internal
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-B
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-B
+ namespace: envoy-gateway
+ 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-B]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: parent-C
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-C
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-C
+ namespace: envoy-gateway
+ 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-C]'
+ reason: Merged
+ status: "True"
+ type: Merged
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+ envoy-gateway/gateway-A:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-A-55bef863
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-A
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-A-55bef863
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-A
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - a.example.com
+ metadata:
+ kind: Gateway
+ name: gateway-A
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-A/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-A
+ namespace: default
+ name: httproute/default/httproute-A/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-A/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: a.example.com
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-A
+ namespace: default
+ name: httproute/default/httproute-A/rule/0/match/0/a_example_com
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: child-specific
+ principal:
+ clientCIDRs:
+ - cidr: 1.2.3.4/32
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 32
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ envoy-gateway/gateway-B:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-B-93731b94
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-B
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-B-93731b94
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-B
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - b.example.com
+ metadata:
+ kind: Gateway
+ name: gateway-B
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-B/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-B
+ namespace: default
+ name: httproute/default/httproute-B/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-2
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-B/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: b.example.com
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-B
+ namespace: default
+ name: httproute/default/httproute-B/rule/0/match/0/b_example_com
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: securitypolicy/default/child-B/authorization/rule/0
+ principal:
+ clientCIDRs:
+ - cidr: 1.2.3.4/32
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 32
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ envoy-gateway/gateway-C:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-C-e2859049
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-C
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-C-e2859049
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-C
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - c.example.com
+ metadata:
+ kind: Gateway
+ name: gateway-C
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-C/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-C
+ namespace: default
+ name: httproute/default/httproute-C/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-3
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-C/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: c.example.com
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-C
+ namespace: default
+ name: httproute/default/httproute-C/rule/0/match/0/c_example_com
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ security:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: securitypolicy/default/child-C/authorization/rule/0
+ principal:
+ clientCIDRs:
+ - cidr: 1.2.3.4/32
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 32
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.in.yaml
new file mode 100644
index 0000000000..7d29d9f626
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.in.yaml
@@ -0,0 +1,72 @@
+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
+ authorization:
+ defaultAction: Allow
+ rules:
+ - name: parent-internal
+ action: Allow
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+- 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
+ authorization:
+ defaultAction: Deny
+ rules:
+ - name: child-specific
+ action: Allow
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.out.yaml
new file mode 100644
index 0000000000..284d5102fd
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-authorization-rules.out.yaml
@@ -0,0 +1,274 @@
+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:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: child-specific
+ principal:
+ clientCIDRs:
+ - 1.2.3.4/32
+ 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:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ name: parent-internal
+ principal:
+ clientCIDRs:
+ - 10.0.0.0/8
+ 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:
+ - '*'
+ 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:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: child-specific
+ principal:
+ clientCIDRs:
+ - cidr: 1.2.3.4/32
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 32
+ - action: Allow
+ name: parent-internal
+ principal:
+ clientCIDRs:
+ - cidr: 10.0.0.0/8
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 8
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml
index 8eeabc5212..d2ba7fb62b 100644
--- a/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml
+++ b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml
@@ -18,6 +18,13 @@ spec:
- name: x-role
values:
- route
+ - action: Allow
+ name: parent-rule
+ principal:
+ headers:
+ - name: x-role
+ values:
+ - parent
basicAuth:
users:
name: users-route
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index fd38db9f92..4fbe03dd30 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -18,6 +18,7 @@ bug fixes: |
Fixed missing deprecated field warning in ClientTrafficPolicy and SecurityPolicy.
Fixed ClientTrafficPolicy TLS cipher validation rejecting supported IANA/RFC cipher suite names.
Fixed TLS secrets with non-canonical PEM formatting (e.g. unusual line endings) being passed verbatim to Envoy, which could cause BoringSSL errors such as `BAD_END_LINE`. Cert and key PEM data is now re-encoded to a canonical form before being delivered as xDS resources.
+ Fixed SecurityPolicy `authorization.rules` not being merged across hierarchy levels when using `mergeType: StrategicMerge` by adding the missing `patchMergeKey` and `patchStrategy` annotations on the Rules slice. If any authorization rule omits `name`, the controller falls back to `JSONMerge` for the entire policy (every spec field is slice-replaced, not only `authorization.rules`) and surfaces a `Warning` condition with reason `AuthorizationRulesMergeFallback` on the SecurityPolicy.
Fixed `MaxStreamDuration` not being set on `CommonHttpProtocolOptions` for non-route cluster.
# Enhancements that improve performance.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 8a8bd691ea..0470426cf1 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -259,7 +259,7 @@ _Appears in:_
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
-| `rules` | _[AuthorizationRule](#authorizationrule) array_ | false | | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed. |
+| `rules` | _[AuthorizationRule](#authorizationrule) array_ | false | | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
When this `SecurityPolicy` is merged with another `SecurityPolicy` via
`mergeType: StrategicMerge`, rules are merged by their `name` field —
child rules with the same name as parent rules override the parent's
configuration, and child rules with new names are concatenated with the
parent's. If any rule on either side omits `name`, the controller falls
back to `JSONMerge` for the entire merge operation (not just this field):
every spec slice is slice-replaced instead of strategic-merged, matching
the pre-keyed-merge behavior. A `Warning` condition with reason
`AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy. |
| `defaultAction` | _[AuthorizationAction](#authorizationaction)_ | false | | DefaultAction defines the default action to be taken if no rules match.
If not specified, the default action is Deny. |
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index e0f84d9c12..059c710f51 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -50183,6 +50183,16 @@ spec:
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
+
+ When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ child rules with the same name as parent rules override the parent's
+ configuration, and child rules with new names are concatenated with the
+ parent's. If any rule on either side omits `name`, the controller falls
+ back to `JSONMerge` for the entire merge operation (not just this field):
+ every spec slice is slice-replaced instead of strategic-merged, matching
+ the pre-keyed-merge behavior. A `Warning` condition with reason
+ `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
items:
description: AuthorizationRule defines a single authorization
rule.
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index fb6087113e..1b59314157 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -28156,6 +28156,16 @@ spec:
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
+
+ When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ child rules with the same name as parent rules override the parent's
+ configuration, and child rules with new names are concatenated with the
+ parent's. If any rule on either side omits `name`, the controller falls
+ back to `JSONMerge` for the entire merge operation (not just this field):
+ every spec slice is slice-replaced instead of strategic-merged, matching
+ the pre-keyed-merge behavior. A `Warning` condition with reason
+ `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
items:
description: AuthorizationRule defines a single authorization
rule.
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 9a6ff0b16d..96e5c8cc9e 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -28156,6 +28156,16 @@ spec:
For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed.
+
+ When this `SecurityPolicy` is merged with another `SecurityPolicy` via
+ `mergeType: StrategicMerge`, rules are merged by their `name` field —
+ child rules with the same name as parent rules override the parent's
+ configuration, and child rules with new names are concatenated with the
+ parent's. If any rule on either side omits `name`, the controller falls
+ back to `JSONMerge` for the entire merge operation (not just this field):
+ every spec slice is slice-replaced instead of strategic-merged, matching
+ the pre-keyed-merge behavior. A `Warning` condition with reason
+ `AuthorizationRulesMergeFallback` is surfaced on the SecurityPolicy.
items:
description: AuthorizationRule defines a single authorization
rule.