From 24ad4d58a32477ab88b1f201e4034f80644f1915 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Sat, 14 Mar 2026 03:02:28 +0900 Subject: [PATCH 01/11] feat: policy field owner Signed-off-by: kkk777-7 --- internal/gatewayapi/helpers.go | 13 + internal/gatewayapi/securitypolicy.go | 236 +++++++++++--- internal/gatewayapi/securitypolicy_test.go | 158 ++++++++- ...merge-multi-gateway-same-listener.out.yaml | 12 +- ...ypolicy-with-merge-multi-listener.out.yaml | 12 +- ...policy-with-merge-needs-fieldowner.in.yaml | 155 +++++++++ ...olicy-with-merge-needs-fieldowner.out.yaml | 307 ++++++++++++++++++ .../securitypolicy-with-merge.out.yaml | 2 +- internal/utils/merge_test.go | 66 ++-- .../utils/testdata/securitypolicy_all.in.yaml | 49 +++ .../securitypolicy_all.jsonmerge.out.yaml | 58 ++++ .../testdata/securitypolicy_all.patch.yaml | 50 +++ ...securitypolicy_all.strategicmerge.out.yaml | 58 ++++ 13 files changed, 1087 insertions(+), 89 deletions(-) create mode 100644 internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml create mode 100644 internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml create mode 100644 internal/utils/testdata/securitypolicy_all.in.yaml create mode 100644 internal/utils/testdata/securitypolicy_all.jsonmerge.out.yaml create mode 100644 internal/utils/testdata/securitypolicy_all.patch.yaml create mode 100644 internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 5f324590e2..fc77c251ef 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -632,6 +632,19 @@ type GatewayPolicyRouteMap struct { SectionIndex map[types.NamespacedName]sets.Set[string] } +type FieldPath string +type PolicyFieldOwners[T client.Object] map[FieldPath]T + +func resolvePolicyFieldOwner[T client.Object](owners PolicyFieldOwners[T], field FieldPath, def T) T { + ownerPolicy := def + if owners != nil { + if owner, ok := owners[field]; ok { + ownerPolicy = owner + } + } + return ownerPolicy +} + // listenersWithSameHTTPPort returns a list of the names of all other HTTP listeners // that would share the same filter chain as the provided listener when translated // to XDS diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index f4b575b120..a9f030533d 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -53,6 +53,21 @@ const ( oidcHMACSecretKey = "hmac-secret" // JWKSConfigMapKey is the key used in ConfigMaps to store JWKS data JWKSConfigMapKey = "jwks" + + spFieldBasicAuth FieldPath = "spec.basicAuth" + spFieldAPIKeyAuthCreds FieldPath = "spec.apiKeyAuth.credentialRefs" + spFieldAuthRules FieldPath = "spec.authorization.rules" + spFieldExtAuth FieldPath = "spec.extAuth" + spFieldExtAuthHTTPBackendRef FieldPath = "spec.extAuth.http.backendRef" + spFieldExtAuthHTTPBackendRefs FieldPath = "spec.extAuth.http.backendRefs" + spFieldExtAuthGRPCBackendRef FieldPath = "spec.extAuth.grpc.backendRef" + spFieldExtAuthGRPCBackendRefs FieldPath = "spec.extAuth.grpc.backendRefs" + spFieldExtAuthContextExtensions FieldPath = "spec.extAuth.contextExtensions" + spFieldOIDC FieldPath = "spec.oidc" + spFieldOIDCProviderBackendRefs FieldPath = "spec.oidc.provider.backendRefs" + spFieldOIDCClientIDRef FieldPath = "spec.oidc.clientIDRef" + spFieldOIDCClientSecret FieldPath = "spec.oidc.clientSecret" + spFieldJwtProviders FieldPath = "spec.jwt.providers" ) // deprecatedFieldsUsedInSecurityPolicy returns a map of deprecated field paths to their alternatives. @@ -365,7 +380,7 @@ func (t *Translator) processSecurityPolicyForRoute( // 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, nil); err != nil { + if err := t.translateSecurityPolicyForRoute(policy, nil, targetedRoute, currTarget, resources, xdsIR, nil, nil); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, @@ -395,7 +410,7 @@ func (t *Translator) processSecurityPolicyForRoute( if gwPolicy == nil && listenerPolicy == nil { // No parent policy found, fall back to current policy - if err := t.translateSecurityPolicyForRoute(policy, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { + if err := t.translateSecurityPolicyForRoute(policy, nil, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, @@ -414,7 +429,7 @@ func (t *Translator) processSecurityPolicyForRoute( } // Merge with parent policy - mergedPolicy, err := mergeSecurityPolicy(policy, parentPolicy) + mergedPolicy, fieldOwners, err := mergeSecurityPolicy(policy, parentPolicy) if err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, @@ -440,7 +455,7 @@ func (t *Translator) processSecurityPolicyForRoute( } // Apply merged policy - if err := t.translateSecurityPolicyForRoute(mergedPolicy, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { + if err := t.translateSecurityPolicyForRoute(mergedPolicy, fieldOwners, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, @@ -817,6 +832,7 @@ func resolveSecurityPolicyRouteTargetRef( func (t *Translator) translateSecurityPolicyForRoute( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], route RouteContext, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, resources *resource.Resources, @@ -841,7 +857,9 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.BasicAuth != nil { if basicAuth, err = t.buildBasicAuth( policy, - resources); err != nil { + fieldOwners, + resources, + ); err != nil { err = perr.WithMessage(err, "BasicAuth") errs = errors.Join(errs, err) } @@ -850,14 +868,16 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.APIKeyAuth != nil { if apiKeyAuth, err = t.buildAPIKeyAuth( policy, - resources); err != nil { + fieldOwners, + resources, + ); err != nil { err = perr.WithMessage(err, "APIKeyAuth") errs = errors.Join(errs, err) } } if policy.Spec.Authorization != nil { - if authorization, err = t.buildAuthorization(policy); err != nil { + if authorization, err = t.buildAuthorization(policy, fieldOwners); err != nil { err = perr.WithMessage(err, "Authorization") errs = errors.Join(errs, err) } @@ -897,8 +917,10 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.ExtAuth != nil { if extAuth, extAuthErr = t.buildExtAuth( policy, + fieldOwners, resources, - gtwCtx.envoyProxy); extAuthErr != nil { + gtwCtx.envoyProxy, + ); extAuthErr != nil { extAuthErr = perr.WithMessage(extAuthErr, "ExtAuth") errs = errors.Join(errs, extAuthErr) } @@ -908,8 +930,10 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.OIDC != nil { if oidc, err = t.buildOIDC( policy, + fieldOwners, resources, - gtwCtx.envoyProxy); err != nil { + gtwCtx.envoyProxy, + ); err != nil { err = perr.WithMessage(err, "OIDC") errs = errors.Join(errs, err) hasNonExtAuthError = true @@ -920,8 +944,10 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.JWT != nil { if jwt, err = t.buildJWT( policy, + fieldOwners, resources, - gtwCtx.envoyProxy); err != nil { + gtwCtx.envoyProxy, + ); err != nil { err = perr.WithMessage(err, "JWT") errs = errors.Join(errs, err) hasNonExtAuthError = true @@ -1046,8 +1072,10 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.JWT != nil { if jwt, err = t.buildJWT( policy, + nil, resources, - gateway.envoyProxy); err != nil { + gateway.envoyProxy, + ); err != nil { err = perr.WithMessage(err, "JWT") errs = errors.Join(errs, err) } @@ -1056,8 +1084,10 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.OIDC != nil { if oidc, err = t.buildOIDC( policy, + nil, resources, - gateway.envoyProxy); err != nil { + gateway.envoyProxy, + ); err != nil { err = perr.WithMessage(err, "OIDC") errs = errors.Join(errs, err) } @@ -1066,7 +1096,9 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.BasicAuth != nil { if basicAuth, err = t.buildBasicAuth( policy, - resources); err != nil { + nil, + resources, + ); err != nil { err = perr.WithMessage(err, "BasicAuth") errs = errors.Join(errs, err) } @@ -1075,14 +1107,16 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.APIKeyAuth != nil { if apiKeyAuth, err = t.buildAPIKeyAuth( policy, - resources); err != nil { + nil, + resources, + ); err != nil { err = perr.WithMessage(err, "APIKeyAuth") errs = errors.Join(errs, err) } } if policy.Spec.Authorization != nil { - if authorization, err = t.buildAuthorization(policy); err != nil { + if authorization, err = t.buildAuthorization(policy, nil); err != nil { errs = errors.Join(errs, err) } } @@ -1092,8 +1126,10 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.ExtAuth != nil { if extAuth, extAuthErr = t.buildExtAuth( policy, + nil, resources, - gateway.envoyProxy); extAuthErr != nil { + gateway.envoyProxy, + ); extAuthErr != nil { extAuthErr = perr.WithMessage(extAuthErr, "ExtAuth") errs = errors.Join(errs, extAuthErr) } @@ -1249,6 +1285,7 @@ func wildcard2regex(wildcard string) string { func (t *Translator) buildJWT( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy, ) (*ir.JWT, error) { @@ -1256,6 +1293,7 @@ func (t *Translator) buildJWT( return nil, err } + jwtOwnerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldJwtProviders, policy) providers := make([]ir.JWTProvider, 0, len(policy.Spec.JWT.Providers)) for i, p := range policy.Spec.JWT.Providers { provider := ir.JWTProvider{ @@ -1267,13 +1305,13 @@ func (t *Translator) buildJWT( ExtractFrom: p.ExtractFrom, } if p.RemoteJWKS != nil { - remoteJWKS, err := t.buildRemoteJWKS(policy, p.RemoteJWKS, i, resources, envoyProxy) + remoteJWKS, err := t.buildRemoteJWKS(jwtOwnerPolicy, p.RemoteJWKS, i, resources, envoyProxy) if err != nil { return nil, err } provider.RemoteJWKS = remoteJWKS } else { - localJWKS, err := t.buildLocalJWKS(policy, p.LocalJWKS) + localJWKS, err := t.buildLocalJWKS(jwtOwnerPolicy, p.LocalJWKS) if err != nil { return nil, err } @@ -1464,6 +1502,7 @@ func (t *Translator) buildLocalJWKS( func (t *Translator) buildOIDC( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy, ) (*ir.OIDC, error) { @@ -1482,21 +1521,22 @@ func (t *Translator) buildOIDC( err error ) - if provider, err = t.buildOIDCProvider(policy, resources, envoyProxy); err != nil { + if provider, err = t.buildOIDCProvider(policy, fieldOwners, resources, envoyProxy); err != nil { return nil, err } - from := crossNamespaceFrom{ - group: egv1a1.GroupName, - kind: resource.KindSecurityPolicy, - namespace: policy.Namespace, - } - // Client ID can be specified either as a string or as a reference to a secret. switch { case oidc.ClientID != nil: clientID = *oidc.ClientID case oidc.ClientIDRef != nil: + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCClientIDRef, policy) + from := crossNamespaceFrom{ + group: egv1a1.GroupName, + kind: resource.KindSecurityPolicy, + namespace: ownerPolicy.Namespace, + } + var clientIDSecret *corev1.Secret if clientIDSecret, err = t.validateSecretRef(true, from, *oidc.ClientIDRef, resources); err != nil { return nil, err @@ -1511,6 +1551,12 @@ func (t *Translator) buildOIDC( return nil, fmt.Errorf("client ID must be specified in OIDC policy %s/%s", policy.Namespace, policy.Name) } + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCClientSecret, policy) + from := crossNamespaceFrom{ + group: egv1a1.GroupName, + kind: resource.KindSecurityPolicy, + namespace: ownerPolicy.Namespace, + } if clientSecret, err = t.validateSecretRef(true, from, oidc.ClientSecret, resources); err != nil { return nil, err } @@ -1569,7 +1615,7 @@ func (t *Translator) buildOIDC( } irOIDC := &ir.OIDC{ - Name: irConfigName(policy), + Name: irConfigName(resolvePolicyFieldOwner(fieldOwners, spFieldOIDC, policy)), Provider: *provider, ClientID: clientID, ClientSecret: clientSecretBytes, @@ -1619,6 +1665,7 @@ func (t *Translator) buildOIDC( func (t *Translator) buildOIDCProvider( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy, ) (*ir.OIDCProvider, error) { @@ -1651,9 +1698,10 @@ func (t *Translator) buildOIDCProvider( protocol = ir.HTTP } + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCProviderBackendRefs, policy) if len(provider.BackendRefs) > 0 { if rd, err = t.translateExtServiceBackendRefs( - policy, provider.BackendRefs, protocol, resources, envoyProxy, "oidc", 0); err != nil { + ownerPolicy, provider.BackendRefs, protocol, resources, envoyProxy, "oidc", 0); err != nil { return nil, err } } @@ -1933,12 +1981,17 @@ func validateTokenEndpoint(tokenEndpoint string) error { func (t *Translator) buildAPIKeyAuth( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, ) (*ir.APIKeyAuth, error) { + // SecurityPolicy Spec.APIKeyAuth.CredentialRefs does not define patchStrategy/patchMergeKey. + // Therefore both JSONMerge and StrategicMerge replace the whole list, so all credential refs + // can be treated as owned by a single policy. + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAPIKeyAuthCreds, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, - namespace: policy.Namespace, + namespace: ownerPolicy.Namespace, } expected := len(policy.Spec.APIKeyAuth.CredentialRefs) @@ -1989,6 +2042,7 @@ func (t *Translator) buildAPIKeyAuth( func (t *Translator) buildBasicAuth( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, ) (*ir.BasicAuth, error) { var ( @@ -1997,10 +2051,11 @@ func (t *Translator) buildBasicAuth( err error ) + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldBasicAuth, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, - namespace: policy.Namespace, + namespace: ownerPolicy.Namespace, } if usersSecret, err = t.validateSecretRef(true, from, basicAuth.Users, resources); err != nil { return nil, err @@ -2019,7 +2074,7 @@ func (t *Translator) buildBasicAuth( } return &ir.BasicAuth{ - Name: irConfigName(policy), + Name: irConfigName(ownerPolicy), Users: usersSecretBytes, ForwardUsernameHeader: basicAuth.ForwardUsernameHeader, }, nil @@ -2058,20 +2113,22 @@ func validateHtpasswdFormat(data []byte) error { func (t *Translator) buildExtAuth( policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy, ) (*ir.ExtAuth, error) { var ( - http = policy.Spec.ExtAuth.HTTP - grpc = policy.Spec.ExtAuth.GRPC - backendRefs []egv1a1.BackendRef - backendSettings *egv1a1.ClusterSettings - protocol ir.AppProtocol - rd *ir.RouteDestination - authority string - err error - traffic *ir.TrafficFeatures - contextExtensions []*ir.ContextExtention + http = policy.Spec.ExtAuth.HTTP + grpc = policy.Spec.ExtAuth.GRPC + backendRefs []egv1a1.BackendRef + backendSettings *egv1a1.ClusterSettings + protocol ir.AppProtocol + rd *ir.RouteDestination + authority string + err error + traffic *ir.TrafficFeatures + contextExtensions []*ir.ContextExtention + backendRefsOwnerPolicy *egv1a1.SecurityPolicy ) // These are sanity checks, they should never happen because the API server @@ -2089,12 +2146,14 @@ func (t *Translator) buildExtAuth( switch { case len(http.BackendRefs) > 0: backendRefs = http.BackendRefs + backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthHTTPBackendRefs, policy) case http.BackendRef != nil: backendRefs = []egv1a1.BackendRef{ { BackendObjectReference: *http.BackendRef, }, } + backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthHTTPBackendRef, policy) default: // This is a sanity check, it should never happen because the API server should have caught it return nil, errors.New("http backend refs must be specified") @@ -2105,12 +2164,14 @@ func (t *Translator) buildExtAuth( switch { case len(grpc.BackendRefs) > 0: backendRefs = grpc.BackendRefs + backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthGRPCBackendRefs, policy) case grpc.BackendRef != nil: backendRefs = []egv1a1.BackendRef{ { BackendObjectReference: *grpc.BackendRef, }, } + backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthGRPCBackendRef, policy) default: // This is a sanity check, it should never happen because the API server should have caught it return nil, errors.New("grpc backend refs must be specified") @@ -2118,7 +2179,7 @@ func (t *Translator) buildExtAuth( } if rd, err = t.translateExtServiceBackendRefs( - policy, backendRefs, protocol, resources, envoyProxy, "extauth", 0); err != nil { + backendRefsOwnerPolicy, backendRefs, protocol, resources, envoyProxy, "extauth", 0); err != nil { return nil, err } @@ -2128,7 +2189,7 @@ func (t *Translator) buildExtAuth( // When translated to XDS, the authority is used on the filter level not on the cluster level. // There's no way to translate to XDS and use a different authority for each backendref if authority == "" { - authority = t.backendRefAuthority(&backendRef.BackendObjectReference, policy) + authority = t.backendRefAuthority(&backendRef.BackendObjectReference, backendRefsOwnerPolicy) } } @@ -2136,12 +2197,13 @@ func (t *Translator) buildExtAuth( return nil, err } - if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, policy.Namespace); err != nil { + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthContextExtensions, policy) + if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, ownerPolicy.Namespace); err != nil { return nil, err } extAuth := &ir.ExtAuth{ - Name: irConfigName(policy), + Name: irConfigName(resolvePolicyFieldOwner(fieldOwners, spFieldExtAuth, policy)), HeadersToExtAuth: policy.Spec.ExtAuth.HeadersToExtAuth, ContextExtensions: contextExtensions, FailOpen: policy.Spec.ExtAuth.FailOpen, @@ -2288,13 +2350,20 @@ func (t *Translator) backendRefAuthority( return fmt.Sprintf("%s.%s", backendRef.Name, backendNamespace) } -func (t *Translator) buildAuthorization(policy *egv1a1.SecurityPolicy) (*ir.Authorization, error) { +func (t *Translator) buildAuthorization( + policy *egv1a1.SecurityPolicy, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], +) (*ir.Authorization, error) { var ( authorization = policy.Spec.Authorization irAuth = &ir.Authorization{} // The default action is Deny if not specified defaultAction = egv1a1.AuthorizationActionDeny ) + // SecurityPolicy Spec.Authorization.AuthorizationRule does not define patchStrategy/patchMergeKey. + // Therefore both JSONMerge and StrategicMerge replace the whole list, so all AuthorizationRules + // can be treated as owned by a single policy. + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAuthRules, policy) if authorization.DefaultAction != nil { defaultAction = *authorization.DefaultAction @@ -2321,7 +2390,7 @@ func (t *Translator) buildAuthorization(policy *egv1a1.SecurityPolicy) (*ir.Auth if rule.Name != nil && *rule.Name != "" { name = *rule.Name } else { - name = defaultAuthorizationRuleName(policy, i) + name = defaultAuthorizationRuleName(ownerPolicy, i) } irAuth.Rules = append(irAuth.Rules, &ir.AuthorizationRule{ Name: name, @@ -2342,10 +2411,79 @@ func defaultAuthorizationRuleName(policy *egv1a1.SecurityPolicy, index int) stri } // mergeSecurityPolicy merges a route-level SecurityPolicy with a parent (Gateway/Listener) SecurityPolicy. -func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, error) { +func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, PolicyFieldOwners[*egv1a1.SecurityPolicy], error) { if routePolicy.Spec.MergeType == nil || parentPolicy == nil { - return routePolicy, nil + return routePolicy, nil, nil + } + fieldOwners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) + mergedPolicy, err := utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, *routePolicy.Spec.MergeType) + if err != nil { + return nil, nil, err + } + return mergedPolicy, fieldOwners, nil +} + +func buildSecurityPolicyFieldOwners( + routePolicy *egv1a1.SecurityPolicy, + parentPolicy *egv1a1.SecurityPolicy, +) PolicyFieldOwners[*egv1a1.SecurityPolicy] { + // Route policy owners are applied last so they override parent owners when both define the same field key. + owners := make(PolicyFieldOwners[*egv1a1.SecurityPolicy]) + applySecurityPolicyFieldOwners(owners, parentPolicy) + applySecurityPolicyFieldOwners(owners, routePolicy) + return owners +} + +func applySecurityPolicyFieldOwners( + owners PolicyFieldOwners[*egv1a1.SecurityPolicy], + policy *egv1a1.SecurityPolicy, +) { + if policy == nil { + return } - return utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, *routePolicy.Spec.MergeType) + if policy.Spec.BasicAuth != nil { + owners[spFieldBasicAuth] = policy + } + if policy.Spec.APIKeyAuth != nil && + len(policy.Spec.APIKeyAuth.CredentialRefs) > 0 { + owners[spFieldAPIKeyAuthCreds] = policy + } + if policy.Spec.Authorization != nil && + len(policy.Spec.Authorization.Rules) > 0 { + owners[spFieldAuthRules] = policy + } + if policy.Spec.ExtAuth != nil { + owners[spFieldExtAuth] = policy + + if policy.Spec.ExtAuth.HTTP != nil && len(policy.Spec.ExtAuth.HTTP.BackendRefs) > 0 { + owners[spFieldExtAuthHTTPBackendRefs] = policy + } + if policy.Spec.ExtAuth.HTTP != nil && policy.Spec.ExtAuth.HTTP.BackendRef != nil { + owners[spFieldExtAuthHTTPBackendRef] = policy + } + if policy.Spec.ExtAuth.GRPC != nil && len(policy.Spec.ExtAuth.GRPC.BackendRefs) > 0 { + owners[spFieldExtAuthGRPCBackendRefs] = policy + } + if policy.Spec.ExtAuth.GRPC != nil && policy.Spec.ExtAuth.GRPC.BackendRef != nil { + owners[spFieldExtAuthGRPCBackendRef] = policy + } + if len(policy.Spec.ExtAuth.ContextExtensions) > 0 { + owners[spFieldExtAuthContextExtensions] = policy + } + } + if policy.Spec.OIDC != nil { + owners[spFieldOIDC] = policy + owners[spFieldOIDCClientSecret] = policy + + if len(policy.Spec.OIDC.Provider.BackendRefs) > 0 { + owners[spFieldOIDCProviderBackendRefs] = policy + } + if policy.Spec.OIDC.ClientIDRef != nil { + owners[spFieldOIDCClientIDRef] = policy + } + } + if policy.Spec.JWT != nil && len(policy.Spec.JWT.Providers) > 0 { + owners[spFieldJwtProviders] = policy + } } diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 263000c7e7..a0ece6dc96 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1630,7 +1630,7 @@ func TestMergeSecurityPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := mergeSecurityPolicy(tt.routePolicy, tt.parentPolicy) + got, _, err := mergeSecurityPolicy(tt.routePolicy, tt.parentPolicy) if (err != nil) != tt.wantErr { t.Errorf("mergeSecurityPolicy() error = %v, wantErr %v", err, tt.wantErr) return @@ -1646,3 +1646,159 @@ func TestMergeSecurityPolicy(t *testing.T) { }) } } + +func Test_buildSecurityPolicyFieldOwners(t *testing.T) { + t.Run("route policy overrides parent for the same owner keys", func(t *testing.T) { + parentPolicy := &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + Namespace: "parent-ns", + }, + Spec: egv1a1.SecurityPolicySpec{ + BasicAuth: &egv1a1.BasicAuth{ + Users: gwapiv1.SecretObjectReference{Name: "parent-users"}, + }, + APIKeyAuth: &egv1a1.APIKeyAuth{ + CredentialRefs: []gwapiv1.SecretObjectReference{{Name: "parent-cred"}}, + }, + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.AuthorizationRule{{Name: ptr.To("parent-rule")}}, + }, + ExtAuth: &egv1a1.ExtAuth{ + HTTP: &egv1a1.HTTPExtAuthService{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "parent-http-backend-refs"}, + }}, + }, + }, + ContextExtensions: []*egv1a1.ContextExtension{ + {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("parent-shared")}, + {Name: "parent-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("parent-only")}, + }, + }, + OIDC: &egv1a1.OIDC{ + ClientSecret: gwapiv1.SecretObjectReference{Name: "parent-client-secret"}, + ClientIDRef: &gwapiv1.SecretObjectReference{Name: "parent-client-id"}, + Provider: egv1a1.OIDCProvider{ + Issuer: "https://parent.example.com", + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "parent-oidc-provider"}, + }}, + }, + }, + }, + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{{ + Name: "parent-jwt-provider", + RemoteJWKS: &egv1a1.RemoteJWKS{ + URI: "https://parent.example.com/jwks", + }, + }}, + }, + }, + } + + routePolicy := &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route", + Namespace: "route-ns", + }, + Spec: egv1a1.SecurityPolicySpec{ + BasicAuth: &egv1a1.BasicAuth{ + Users: gwapiv1.SecretObjectReference{Name: "route-users"}, + }, + APIKeyAuth: &egv1a1.APIKeyAuth{ + CredentialRefs: []gwapiv1.SecretObjectReference{{Name: "route-cred"}}, + }, + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.AuthorizationRule{{Name: ptr.To("route-rule")}}, + }, + ExtAuth: &egv1a1.ExtAuth{ + HTTP: &egv1a1.HTTPExtAuthService{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "route-http-backend-refs"}, + }}, + }, + }, + ContextExtensions: []*egv1a1.ContextExtension{ + {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("route-shared")}, + {Name: "route-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("route-only")}, + }, + }, + OIDC: &egv1a1.OIDC{ + ClientSecret: gwapiv1.SecretObjectReference{Name: "route-client-secret"}, + ClientIDRef: &gwapiv1.SecretObjectReference{Name: "route-client-id"}, + Provider: egv1a1.OIDCProvider{ + Issuer: "https://route.example.com", + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "route-oidc-provider"}, + }}, + }, + }, + }, + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{{ + Name: "route-jwt-provider", + LocalJWKS: &egv1a1.LocalJWKS{ + Type: ptr.To(egv1a1.LocalJWKSTypeInline), + Inline: ptr.To(`{"keys":[]}`), + }, + }}, + }, + }, + } + + owners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) + require.NotNil(t, owners) + + assert.Same(t, routePolicy, owners[spFieldBasicAuth]) + assert.Same(t, routePolicy, owners[spFieldAPIKeyAuthCreds]) + assert.Same(t, routePolicy, owners[spFieldAuthRules]) + assert.Same(t, routePolicy, owners[spFieldExtAuth]) + assert.Same(t, routePolicy, owners[spFieldExtAuthHTTPBackendRefs]) + assert.Same(t, routePolicy, owners[spFieldOIDC]) + assert.Same(t, routePolicy, owners[spFieldOIDCClientIDRef]) + assert.Same(t, routePolicy, owners[spFieldOIDCClientSecret]) + assert.Same(t, routePolicy, owners[spFieldOIDCProviderBackendRefs]) + assert.Same(t, routePolicy, owners[spFieldJwtProviders]) + assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtensions]) + }) + + t.Run("uses parent owner for grpc backend fields when route does not set them", func(t *testing.T) { + parentPolicy := &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + Namespace: "parent-ns", + }, + Spec: egv1a1.SecurityPolicySpec{ + ExtAuth: &egv1a1.ExtAuth{ + GRPC: &egv1a1.GRPCExtAuthService{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "parent-grpc-backend-refs"}, + }}, + }, + }, + }, + }, + } + + routePolicy := &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route", + Namespace: "route-ns", + }, + Spec: egv1a1.SecurityPolicySpec{}, + } + + owners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) + require.NotNil(t, owners) + + assert.Same(t, parentPolicy, owners[spFieldExtAuth]) + assert.Same(t, parentPolicy, owners[spFieldExtAuthGRPCBackendRefs]) + }) +} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.out.yaml index 96bfbf8a65..90fae2f530 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.out.yaml @@ -372,7 +372,7 @@ xdsIR: prefix: /foo security: basicAuth: - name: securitypolicy/default/policy-for-route + name: securitypolicy/envoy-gateway/policy-for-gateway-1 users: '[redacted]' cors: allowMethods: @@ -473,9 +473,9 @@ xdsIR: destination: metadata: kind: SecurityPolicy - name: policy-for-route - namespace: default - name: securitypolicy/default/policy-for-route/extauth/0 + name: policy-for-gateway-2 + namespace: envoy-gateway + name: securitypolicy/envoy-gateway/policy-for-gateway-2/extauth/0 settings: - addressType: IP endpoints: @@ -486,11 +486,11 @@ xdsIR: name: auth-service namespace: envoy-gateway sectionName: "9001" - name: securitypolicy/default/policy-for-route/extauth/0/backend/0 + name: securitypolicy/envoy-gateway/policy-for-gateway-2/extauth/0/backend/0 protocol: HTTP weight: 1 path: "" - name: securitypolicy/default/policy-for-route + name: securitypolicy/envoy-gateway/policy-for-gateway-2 readyListener: address: 0.0.0.0 ipFamily: IPv4 diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml index 19999e159b..5c344fd5ea 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.out.yaml @@ -350,7 +350,7 @@ xdsIR: prefix: /foo security: basicAuth: - name: securitypolicy/default/policy-for-route + name: securitypolicy/envoy-gateway/policy-for-listener-a users: '[redacted]' cors: allowMethods: @@ -420,9 +420,9 @@ xdsIR: destination: metadata: kind: SecurityPolicy - name: policy-for-route - namespace: default - name: securitypolicy/default/policy-for-route/extauth/0 + name: policy-for-listener-b + namespace: envoy-gateway + name: securitypolicy/envoy-gateway/policy-for-listener-b/extauth/0 settings: - addressType: IP endpoints: @@ -433,11 +433,11 @@ xdsIR: name: auth-service namespace: envoy-gateway sectionName: "9001" - name: securitypolicy/default/policy-for-route/extauth/0/backend/0 + name: securitypolicy/envoy-gateway/policy-for-listener-b/extauth/0/backend/0 protocol: HTTP weight: 1 path: "" - name: securitypolicy/default/policy-for-route + name: securitypolicy/envoy-gateway/policy-for-listener-b readyListener: address: 0.0.0.0 ipFamily: IPv4 diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml new file mode 100644 index 0000000000..e869d6637c --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.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 + 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 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: envoy-gateway + name: grpc-backend + spec: + ports: + - port: 9000 + name: grpc + protocol: TCP +endpointSlices: +- apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + namespace: envoy-gateway + name: endpointslice-grpc-backend + labels: + kubernetes.io/service-name: grpc-backend + addressType: IPv4 + ports: + - name: grpc + protocol: TCP + port: 9000 + endpoints: + - addresses: + - 8.8.8.8 + 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: http + # merge basicAuth check: + # - can resolve auth secret in parent ns (envoy-gateway) + basicAuth: + users: + name: users-secret + # merge extAuth check: + # - can resolve backendRef in parent ns (envoy-gateway) + # - replace contextExtensions entirely with the values configured in route policy + extAuth: + grpc: + backendRefs: + - name: grpc-backend + port: 9000 + contextExtensions: + - name: shared + type: ValueRef + valueRef: + Group: "" + Kind: ConfigMap + Name: context-extension-for-gateway + key: data-shared + - name: parent-only + type: ValueRef + valueRef: + Group: "" + Kind: ConfigMap + Name: context-extension-for-gateway + key: data +- 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 + extAuth: + contextExtensions: + - name: shared + type: ValueRef + valueRef: + Group: "" + Kind: ConfigMap + Name: context-extension-for-route + key: data-shared + - name: route-only + type: ValueRef + valueRef: + Group: "" + Kind: ConfigMap + Name: context-extension-for-route + key: data +secrets: +- apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: users-secret + type: Opaque + data: + .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9 +configmaps: +- apiVersion: v1 + kind: ConfigMap + metadata: + namespace: envoy-gateway + name: context-extension-for-gateway + data: + data-shared: test-key-gateway + data: test-key-gateway +- apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + name: context-extension-for-route + data: + data-shared: test-key-route + data: test-key-route diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml new file mode 100644 index 0000000000..1314be63ae --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml @@ -0,0 +1,307 @@ +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: + extAuth: + contextExtensions: + - name: shared + type: ValueRef + valueRef: + group: "" + key: data-shared + kind: ConfigMap + name: context-extension-for-route + - name: route-only + type: ValueRef + valueRef: + group: "" + key: data + kind: ConfigMap + name: context-extension-for-route + 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: + name: users-secret + extAuth: + contextExtensions: + - name: shared + type: ValueRef + valueRef: + group: "" + key: data-shared + kind: ConfigMap + name: context-extension-for-gateway + - name: parent-only + type: ValueRef + valueRef: + group: "" + key: data + kind: ConfigMap + name: context-extension-for-gateway + grpc: + backendRefs: + - name: grpc-backend + port: 9000 + 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: + basicAuth: + name: securitypolicy/envoy-gateway/policy-for-gateway + users: '[redacted]' + extAuth: + contextExtensions: + - name: shared + value: '[redacted]' + - name: route-only + value: '[redacted]' + grpc: + authority: grpc-backend.envoy-gateway:9000 + destination: + metadata: + kind: SecurityPolicy + name: policy-for-gateway + namespace: envoy-gateway + name: securitypolicy/envoy-gateway/policy-for-gateway/extauth/0 + settings: + - addressType: IP + endpoints: + - host: 8.8.8.8 + port: 9000 + metadata: + kind: Service + name: grpc-backend + namespace: envoy-gateway + sectionName: "9000" + name: securitypolicy/envoy-gateway/policy-for-gateway/extauth/0/backend/0 + protocol: GRPC + weight: 1 + 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.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml index ee2655e8a0..b155bfe2ed 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge.out.yaml @@ -242,7 +242,7 @@ xdsIR: prefix: /foo security: basicAuth: - name: securitypolicy/default/policy-for-route + name: securitypolicy/envoy-gateway/policy-for-gateway users: '[redacted]' cors: allowHeaders: diff --git a/internal/utils/merge_test.go b/internal/utils/merge_test.go index 73f6a584c9..2c91e27bcd 100644 --- a/internal/utils/merge_test.go +++ b/internal/utils/merge_test.go @@ -19,41 +19,55 @@ import ( "github.com/envoyproxy/gateway/internal/utils/test" ) -func TestMergeBackendTrafficPolicy(t *testing.T) { +func TestMergePolicy(t *testing.T) { baseDir := "testdata" - caseFiles, err := filepath.Glob(filepath.Join(baseDir, "backendtrafficpolicy_*.in.yaml")) + caseFiles, err := filepath.Glob(filepath.Join(baseDir, "*policy_*.in.yaml")) require.NoError(t, err) for _, caseFile := range caseFiles { - // get case name from path - caseName := strings.TrimPrefix(strings.TrimSuffix(caseFile, ".in.yaml"), baseDir+"/backendtrafficpolicy_") + caseName := strings.TrimPrefix(strings.TrimSuffix(caseFile, ".in.yaml"), baseDir+"/") + policyType := strings.SplitN(caseName, "_", 2)[0] + t.Run(caseName, func(t *testing.T) { - for _, mergeType := range []egv1a1.MergeType{egv1a1.StrategicMerge, egv1a1.JSONMerge} { - patchedInput := strings.Replace(caseFile, ".in.yaml", ".patch.yaml", 1) - var output string - if mergeType == egv1a1.StrategicMerge { - output = strings.Replace(caseFile, ".in.yaml", ".strategicmerge.out.yaml", 1) - } else { - output = strings.Replace(caseFile, ".in.yaml", ".jsonmerge.out.yaml", 1) - } + switch policyType { + case "backendtrafficpolicy": + runMergePolicyTest[*egv1a1.BackendTrafficPolicy](t, caseFile) + case "securitypolicy": + runMergePolicyTest[*egv1a1.SecurityPolicy](t, caseFile) + default: + t.Fatalf("unsupported policy type %q in %s", policyType, caseFile) + } + }) + } +} - original := readObject[*egv1a1.BackendTrafficPolicy](t, caseFile) - patch := readObject[*egv1a1.BackendTrafficPolicy](t, patchedInput) +func runMergePolicyTest[T client.Object](t *testing.T, caseFile string) { + t.Helper() - got, err := Merge(original, patch, mergeType) - require.NoError(t, err) + for _, mergeType := range []egv1a1.MergeType{egv1a1.StrategicMerge, egv1a1.JSONMerge} { + patchedInput := strings.Replace(caseFile, ".in.yaml", ".patch.yaml", 1) + var output string + if mergeType == egv1a1.StrategicMerge { + output = strings.Replace(caseFile, ".in.yaml", ".strategicmerge.out.yaml", 1) + } else { + output = strings.Replace(caseFile, ".in.yaml", ".jsonmerge.out.yaml", 1) + } - if test.OverrideTestData() { - b, err := yaml.Marshal(got) - require.NoError(t, err) - require.NoError(t, os.WriteFile(output, b, 0o600)) - continue - } + original := readObject[T](t, caseFile) + patch := readObject[T](t, patchedInput) - expected := readObject[*egv1a1.BackendTrafficPolicy](t, output) - require.Equal(t, expected, got) - } - }) + got, err := Merge(original, patch, mergeType) + require.NoError(t, err) + + if test.OverrideTestData() { + b, err := yaml.Marshal(got) + require.NoError(t, err) + require.NoError(t, os.WriteFile(output, b, 0o600)) + continue + } + + expected := readObject[T](t, output) + require.Equal(t, expected, got) } } diff --git a/internal/utils/testdata/securitypolicy_all.in.yaml b/internal/utils/testdata/securitypolicy_all.in.yaml new file mode 100644 index 0000000000..2524a7a0f9 --- /dev/null +++ b/internal/utils/testdata/securitypolicy_all.in.yaml @@ -0,0 +1,49 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: original +spec: + basicAuth: + users: + name: users-parent + apiKeyAuth: + credentialRefs: + - name: api-keys-parent + authorization: + rules: + - name: parent-rule + action: Allow + principal: + headers: + - name: x-role + values: + - parent + extAuth: + http: + backendRefs: + - name: ext-auth-parent + port: 9001 + contextExtensions: + - name: shared + type: Value + value: parent + - name: parent-only + type: Value + value: parent-only + oidc: + provider: + issuer: https://oidc.parent.example.com + backendRefs: + - name: oidc-parent + port: 443 + clientIDRef: + name: oidc-client-id-parent + clientSecret: + name: oidc-client-secret-parent + jwt: + providers: + - name: jwt-parent + issuer: https://issuer.parent.example.com + localJWKS: + type: Inline + inline: "{}" diff --git a/internal/utils/testdata/securitypolicy_all.jsonmerge.out.yaml b/internal/utils/testdata/securitypolicy_all.jsonmerge.out.yaml new file mode 100644 index 0000000000..eeb4cb0d16 --- /dev/null +++ b/internal/utils/testdata/securitypolicy_all.jsonmerge.out.yaml @@ -0,0 +1,58 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: patched +spec: + apiKeyAuth: + credentialRefs: + - name: api-keys-route + extractFrom: null + sanitize: true + authorization: + defaultAction: null + rules: + - action: Deny + name: route-rule + principal: + headers: + - name: x-role + values: + - route + basicAuth: + users: + name: users-route + extAuth: + contextExtensions: + - name: shared + type: Value + value: route + - name: route-only + type: Value + value: route-only + grpc: + backendRefs: + - name: ext-auth-route + port: 9002 + http: + backendRefs: + - name: ext-auth-parent + port: 9001 + jwt: + providers: + - issuer: https://issuer.route.example.com + localJWKS: + inline: '{}' + type: Inline + name: jwt-route + oidc: + clientIDRef: + name: oidc-client-id-route + clientSecret: + name: oidc-client-secret-route + provider: + backendRefs: + - name: oidc-route + port: 8443 + issuer: "" +status: + ancestors: null diff --git a/internal/utils/testdata/securitypolicy_all.patch.yaml b/internal/utils/testdata/securitypolicy_all.patch.yaml new file mode 100644 index 0000000000..fcee7aca7d --- /dev/null +++ b/internal/utils/testdata/securitypolicy_all.patch.yaml @@ -0,0 +1,50 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: patched +spec: + basicAuth: + users: + name: users-route + apiKeyAuth: + credentialRefs: + - name: api-keys-route + sanitize: true + authorization: + rules: + - name: route-rule + action: Deny + principal: + headers: + - name: x-role + values: + - route + extAuth: + grpc: + backendRefs: + - name: ext-auth-route + port: 9002 + # arrays all replace + contextExtensions: + - name: shared + type: Value + value: route + - name: route-only + type: Value + value: route-only + oidc: + provider: + backendRefs: + - name: oidc-route + port: 8443 + clientIDRef: + name: oidc-client-id-route + clientSecret: + name: oidc-client-secret-route + jwt: + providers: + - name: jwt-route + issuer: https://issuer.route.example.com + localJWKS: + type: Inline + inline: "{}" diff --git a/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml new file mode 100644 index 0000000000..eeb4cb0d16 --- /dev/null +++ b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml @@ -0,0 +1,58 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: patched +spec: + apiKeyAuth: + credentialRefs: + - name: api-keys-route + extractFrom: null + sanitize: true + authorization: + defaultAction: null + rules: + - action: Deny + name: route-rule + principal: + headers: + - name: x-role + values: + - route + basicAuth: + users: + name: users-route + extAuth: + contextExtensions: + - name: shared + type: Value + value: route + - name: route-only + type: Value + value: route-only + grpc: + backendRefs: + - name: ext-auth-route + port: 9002 + http: + backendRefs: + - name: ext-auth-parent + port: 9001 + jwt: + providers: + - issuer: https://issuer.route.example.com + localJWKS: + inline: '{}' + type: Inline + name: jwt-route + oidc: + clientIDRef: + name: oidc-client-id-route + clientSecret: + name: oidc-client-secret-route + provider: + backendRefs: + - name: oidc-route + port: 8443 + issuer: "" +status: + ancestors: null From c9abe3a3d8a7cc700607780352a1f9b827e6cf91 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Wed, 18 Mar 2026 01:43:41 +0900 Subject: [PATCH 02/11] update gatewayapi test input Signed-off-by: kkk777-7 --- ...-merge-multi-gateway-same-listener.in.yaml | 22 ------- ...typolicy-with-merge-multi-listener.in.yaml | 22 ------- ...policy-with-merge-needs-fieldowner.in.yaml | 41 ++++++++++++ ...olicy-with-merge-needs-fieldowner.out.yaml | 64 +++++++++++++++++++ 4 files changed, 105 insertions(+), 44 deletions(-) diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.in.yaml index e13c11c4b5..ee8b0bb9d9 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-gateway-same-listener.in.yaml @@ -108,20 +108,6 @@ services: - 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 @@ -149,11 +135,3 @@ secrets: 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-multi-listener.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml index 5dc3bb2a76..4b1592dfc3 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-multi-listener.in.yaml @@ -104,20 +104,6 @@ services: - 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 @@ -145,11 +131,3 @@ secrets: 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-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml index e869d6637c..0485a1f390 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml @@ -100,6 +100,22 @@ securityPolicies: Kind: ConfigMap Name: context-extension-for-gateway key: data + # merge oidc check: + # - can resolve provider backendRef in parent ns (envoy-gateway) + # - can resolve client secret in parent ns (envoy-gateway) + oidc: + provider: + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-ip + port: 3000 + issuer: "https://oauth.foo.com" + authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth" + tokenEndpoint: "https://oauth.foo.com/token" + clientID: "client1.apps.googleusercontent.com" + clientSecret: + name: "client-secret" - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: @@ -127,6 +143,17 @@ securityPolicies: Kind: ConfigMap Name: context-extension-for-route key: data +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + namespace: envoy-gateway + name: backend-ip + spec: + endpoints: + - ip: + address: 7.7.7.7 + port: 3000 secrets: - apiVersion: v1 kind: Secret @@ -136,6 +163,20 @@ secrets: type: Opaque data: .htpasswd: dXNlcjE6e1NIQX15LzJzWUFqNXlyUUlONFRMMFlkUGRtR05LcGM9 +- apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: client-secret + data: + client-secret: Y2xpZW50MTpzZWNyZXQK +- apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway-system + name: envoy-oidc-hmac + data: + hmac-secret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY= configmaps: - apiVersion: v1 kind: ConfigMap diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml index 1314be63ae..1f53dc1806 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml @@ -1,3 +1,21 @@ +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-ip + namespace: envoy-gateway + spec: + endpoints: + - ip: + address: 7.7.7.7 + port: 3000 + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -170,6 +188,19 @@ securityPolicies: backendRefs: - name: grpc-backend port: 9000 + oidc: + clientID: client1.apps.googleusercontent.com + clientSecret: + name: client-secret + provider: + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-ip + port: 3000 + issuer: https://oauth.foo.com + tokenEndpoint: https://oauth.foo.com/token targetRefs: - group: gateway.networking.k8s.io kind: Gateway @@ -300,6 +331,39 @@ xdsIR: protocol: GRPC weight: 1 name: securitypolicy/default/policy-for-route + oidc: + clientID: client1.apps.googleusercontent.com + clientSecret: '[redacted]' + cookieSuffix: 811c9dc5 + hmacSecret: '[redacted]' + logoutPath: /logout + name: securitypolicy/envoy-gateway/policy-for-gateway + provider: + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + destination: + metadata: + kind: SecurityPolicy + name: policy-for-gateway + namespace: envoy-gateway + name: securitypolicy/envoy-gateway/policy-for-gateway/oidc/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 3000 + metadata: + kind: Backend + name: backend-ip + namespace: envoy-gateway + name: securitypolicy/envoy-gateway/policy-for-gateway/oidc/0/backend/0 + protocol: HTTPS + weight: 1 + tokenEndpoint: https://oauth.foo.com/token + redirectPath: /oauth2/callback + redirectURL: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback' + refreshToken: true + scopes: + - openid readyListener: address: 0.0.0.0 ipFamily: IPv4 From 678c747c29f5598ddc1bf58a60ebc27392a0353b Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Wed, 18 Mar 2026 09:35:08 +0900 Subject: [PATCH 03/11] fix lint Signed-off-by: kkk777-7 --- internal/gatewayapi/helpers.go | 1 + internal/gatewayapi/securitypolicy.go | 8 +++++--- .../securitypolicy-with-merge-needs-fieldowner.in.yaml | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index fc77c251ef..f18c8d6419 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -633,6 +633,7 @@ type GatewayPolicyRouteMap struct { } type FieldPath string + type PolicyFieldOwners[T client.Object] map[FieldPath]T func resolvePolicyFieldOwner[T client.Object](owners PolicyFieldOwners[T], field FieldPath, def T) T { diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index a9f030533d..d1c4388821 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -54,7 +54,8 @@ const ( // JWKSConfigMapKey is the key used in ConfigMaps to store JWKS data JWKSConfigMapKey = "jwks" - spFieldBasicAuth FieldPath = "spec.basicAuth" + spFieldBasicAuth FieldPath = "spec.basicAuth" + // nolint: gosec spFieldAPIKeyAuthCreds FieldPath = "spec.apiKeyAuth.credentialRefs" spFieldAuthRules FieldPath = "spec.authorization.rules" spFieldExtAuth FieldPath = "spec.extAuth" @@ -66,8 +67,9 @@ const ( spFieldOIDC FieldPath = "spec.oidc" spFieldOIDCProviderBackendRefs FieldPath = "spec.oidc.provider.backendRefs" spFieldOIDCClientIDRef FieldPath = "spec.oidc.clientIDRef" - spFieldOIDCClientSecret FieldPath = "spec.oidc.clientSecret" - spFieldJwtProviders FieldPath = "spec.jwt.providers" + // nolint: gosec + spFieldOIDCClientSecret FieldPath = "spec.oidc.clientSecret" + spFieldJwtProviders FieldPath = "spec.jwt.providers" ) // deprecatedFieldsUsedInSecurityPolicy returns a map of deprecated field paths to their alternatives. diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml index 0485a1f390..cee5c07d67 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml @@ -106,10 +106,10 @@ securityPolicies: oidc: provider: backendRefs: - - group: gateway.envoyproxy.io - kind: Backend - name: backend-ip - port: 3000 + - group: gateway.envoyproxy.io + kind: Backend + name: backend-ip + port: 3000 issuer: "https://oauth.foo.com" authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth" tokenEndpoint: "https://oauth.foo.com/token" From d1df896e3896737014922c8a6a1c27389ddc51da Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Wed, 18 Mar 2026 10:26:15 +0900 Subject: [PATCH 04/11] fix comments and docs Signed-off-by: kkk777-7 --- internal/gatewayapi/helpers.go | 6 ++++-- internal/gatewayapi/securitypolicy.go | 7 +------ .../gateway_api_extensions/security-policy.md | 11 ----------- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index f18c8d6419..ca2c66dc82 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -636,8 +636,10 @@ type FieldPath string type PolicyFieldOwners[T client.Object] map[FieldPath]T -func resolvePolicyFieldOwner[T client.Object](owners PolicyFieldOwners[T], field FieldPath, def T) T { - ownerPolicy := def +// resolvePolicyFieldOwner returns the owner for a policy spec field. +// If owners is nil or does not contain the field key, defaultOwner is returned as the owner. +func resolvePolicyFieldOwner[T client.Object](owners PolicyFieldOwners[T], field FieldPath, defaultOwner T) T { + ownerPolicy := defaultOwner if owners != nil { if owner, ok := owners[field]; ok { ownerPolicy = owner diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index d1c4388821..5b867d099c 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -1986,9 +1986,6 @@ func (t *Translator) buildAPIKeyAuth( fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], resources *resource.Resources, ) (*ir.APIKeyAuth, error) { - // SecurityPolicy Spec.APIKeyAuth.CredentialRefs does not define patchStrategy/patchMergeKey. - // Therefore both JSONMerge and StrategicMerge replace the whole list, so all credential refs - // can be treated as owned by a single policy. ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAPIKeyAuthCreds, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, @@ -2362,9 +2359,7 @@ func (t *Translator) buildAuthorization( // The default action is Deny if not specified defaultAction = egv1a1.AuthorizationActionDeny ) - // SecurityPolicy Spec.Authorization.AuthorizationRule does not define patchStrategy/patchMergeKey. - // Therefore both JSONMerge and StrategicMerge replace the whole list, so all AuthorizationRules - // can be treated as owned by a single policy. + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAuthRules, policy) if authorization.DefaultAction != nil { 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 f7bdb45234..177c4b9535 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 @@ -269,17 +269,6 @@ In this example, the route-level policy merges with the gateway-level policy, re - 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) From 48d6e7cf206cf9c91ec6b08c9bf5b58c359344ea Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Thu, 16 Apr 2026 00:17:54 +0900 Subject: [PATCH 05/11] remove ptr Signed-off-by: kkk777-7 --- internal/gatewayapi/securitypolicy_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 1a2c10f8b7..a63f713012 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1868,7 +1868,7 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { CredentialRefs: []gwapiv1.SecretObjectReference{{Name: "parent-cred"}}, }, Authorization: &egv1a1.Authorization{ - Rules: []egv1a1.AuthorizationRule{{Name: ptr.To("parent-rule")}}, + Rules: []egv1a1.AuthorizationRule{{Name: new("parent-rule")}}, }, ExtAuth: &egv1a1.ExtAuth{ HTTP: &egv1a1.HTTPExtAuthService{ @@ -1879,8 +1879,8 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { }, }, ContextExtensions: []*egv1a1.ContextExtension{ - {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("parent-shared")}, - {Name: "parent-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("parent-only")}, + {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: new("parent-shared")}, + {Name: "parent-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: new("parent-only")}, }, }, OIDC: &egv1a1.OIDC{ @@ -1919,7 +1919,7 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { CredentialRefs: []gwapiv1.SecretObjectReference{{Name: "route-cred"}}, }, Authorization: &egv1a1.Authorization{ - Rules: []egv1a1.AuthorizationRule{{Name: ptr.To("route-rule")}}, + Rules: []egv1a1.AuthorizationRule{{Name: new("route-rule")}}, }, ExtAuth: &egv1a1.ExtAuth{ HTTP: &egv1a1.HTTPExtAuthService{ @@ -1930,8 +1930,8 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { }, }, ContextExtensions: []*egv1a1.ContextExtension{ - {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("route-shared")}, - {Name: "route-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: ptr.To("route-only")}, + {Name: "shared", Type: egv1a1.ContextExtensionValueTypeValue, Value: new("route-shared")}, + {Name: "route-only", Type: egv1a1.ContextExtensionValueTypeValue, Value: new("route-only")}, }, }, OIDC: &egv1a1.OIDC{ @@ -1950,8 +1950,8 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { Providers: []egv1a1.JWTProvider{{ Name: "route-jwt-provider", LocalJWKS: &egv1a1.LocalJWKS{ - Type: ptr.To(egv1a1.LocalJWKSTypeInline), - Inline: ptr.To(`{"keys":[]}`), + Type: new(egv1a1.LocalJWKSTypeInline), + Inline: new(`{"keys":[]}`), }, }}, }, From 145ef59f64182341a0a9e3f3afff568f21d1623b Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Thu, 16 Apr 2026 01:09:22 +0900 Subject: [PATCH 06/11] update: owner key of contextExtensions Signed-off-by: kkk777-7 --- internal/gatewayapi/securitypolicy.go | 17 +++-- internal/gatewayapi/securitypolicy_test.go | 67 ++++++++++++++++++- ...policy-with-merge-needs-fieldowner.in.yaml | 2 +- ...olicy-with-merge-needs-fieldowner.out.yaml | 2 + ...securitypolicy_all.strategicmerge.out.yaml | 3 + 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index bb73eca5fe..e5fd5714a3 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -73,6 +73,10 @@ const ( spFieldJwtProviders FieldPath = "spec.jwt.providers" ) +func spFieldExtAuthContextExtension(name string) FieldPath { + return FieldPath(fmt.Sprintf("%s.%s", spFieldExtAuthContextExtensions, name)) +} + // deprecatedFieldsUsedInSecurityPolicy returns a map of deprecated field paths to their alternatives. func deprecatedFieldsUsedInSecurityPolicy(policy *egv1a1.SecurityPolicy) map[string]string { deprecatedFields := make(map[string]string) @@ -2244,8 +2248,7 @@ func (t *Translator) buildExtAuth( return nil, err } - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthContextExtensions, policy) - if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, ownerPolicy.Namespace); err != nil { + if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, fieldOwners, policy); err != nil { return nil, err } @@ -2299,7 +2302,8 @@ func parseExtAuthTimeout(timeout *gwapiv1.Duration) *metav1.Duration { func (t *Translator) buildContextExtensions( contextExtensions []*egv1a1.ContextExtension, - policyNs string, + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + defaultOwner *egv1a1.SecurityPolicy, ) ([]*ir.ContextExtention, error) { if len(contextExtensions) == 0 { return nil, nil @@ -2309,8 +2313,9 @@ func (t *Translator) buildContextExtensions( for _, ext := range contextExtensions { var value ir.PrivateBytes if ext.Type == egv1a1.ContextExtensionValueTypeValueRef { + ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthContextExtension(ext.Name), defaultOwner) var err error - if value, err = t.getContextExtensionValueFromRef(ext.ValueRef, policyNs); err != nil { + if value, err = t.getContextExtensionValueFromRef(ext.ValueRef, ownerPolicy.Namespace); err != nil { return nil, err } } else if ext.Value != nil { @@ -2595,7 +2600,9 @@ func applySecurityPolicyFieldOwners( owners[spFieldExtAuthGRPCBackendRef] = policy } if len(policy.Spec.ExtAuth.ContextExtensions) > 0 { - owners[spFieldExtAuthContextExtensions] = policy + for _, ext := range policy.Spec.ExtAuth.ContextExtensions { + owners[spFieldExtAuthContextExtension(ext.Name)] = policy + } } } if policy.Spec.OIDC != nil { diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index a63f713012..4534a45865 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1423,9 +1423,14 @@ func Test_validateAuthorizationGeoIPForHTTP(t *testing.T) { func Test_buildContextExtensions(t *testing.T) { policyNs := "default" + defaultOwner := &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: policyNs}, + } tests := []struct { name string contextExtensions []*egv1a1.ContextExtension + fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy] + defaultOwner *egv1a1.SecurityPolicy translatorContext *TranslatorContext want []*ir.ContextExtention wantErr bool @@ -1528,6 +1533,58 @@ func Test_buildContextExtensions(t *testing.T) { }, want: []*ir.ContextExtention{{Name: "foo", Value: ir.PrivateBytes("bar")}}, }, + { + name: "TypeValueRefUsesPerKeyOwnerNamespace", + contextExtensions: []*egv1a1.ContextExtension{ + { + Name: "parent-only", + Type: egv1a1.ContextExtensionValueTypeValueRef, + ValueRef: &egv1a1.LocalObjectKeyReference{ + LocalObjectReference: gwapiv1.LocalObjectReference{ + Kind: resource.KindConfigMap, + Name: "parent-cm", + }, + Key: "test-key", + }, + }, + { + Name: "route-only", + Type: egv1a1.ContextExtensionValueTypeValueRef, + ValueRef: &egv1a1.LocalObjectKeyReference{ + LocalObjectReference: gwapiv1.LocalObjectReference{ + Kind: resource.KindConfigMap, + Name: "route-cm", + }, + Key: "test-key", + }, + }, + }, + fieldOwners: PolicyFieldOwners[*egv1a1.SecurityPolicy]{ + spFieldExtAuthContextExtension("parent-only"): &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "parent-ns"}, + }, + spFieldExtAuthContextExtension("route-only"): &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "route-ns"}, + }, + }, + defaultOwner: &egv1a1.SecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default-ns"}, + }, + translatorContext: &TranslatorContext{ + ConfigMapMap: map[types.NamespacedName]*corev1.ConfigMap{ + {Namespace: "parent-ns", Name: "parent-cm"}: { + Data: map[string]string{"test-key": "parent-bar"}, + }, + {Namespace: "route-ns", Name: "route-cm"}: { + Data: map[string]string{"test-key": "route-bar"}, + }, + }, + }, + want: []*ir.ContextExtention{ + {Name: "parent-only", Value: ir.PrivateBytes("parent-bar")}, + {Name: "route-only", Value: ir.PrivateBytes("route-bar")}, + }, + }, { name: "TypeValueRefSecretNotFound", contextExtensions: []*egv1a1.ContextExtension{{ @@ -1605,7 +1662,11 @@ func Test_buildContextExtensions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { translator := &Translator{TranslatorContext: tt.translatorContext} - got, err := translator.buildContextExtensions(tt.contextExtensions, policyNs) + owner := tt.defaultOwner + if owner == nil { + owner = defaultOwner + } + got, err := translator.buildContextExtensions(tt.contextExtensions, tt.fieldOwners, owner) if tt.wantErr { require.Error(t, err) require.Nil(t, got) @@ -1971,7 +2032,9 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { assert.Same(t, routePolicy, owners[spFieldOIDCClientSecret]) assert.Same(t, routePolicy, owners[spFieldOIDCProviderBackendRefs]) assert.Same(t, routePolicy, owners[spFieldJwtProviders]) - assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtensions]) + assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtension("shared")]) + assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtension("route-only")]) + assert.Same(t, parentPolicy, owners[spFieldExtAuthContextExtension("parent-only")]) }) t.Run("uses parent owner for grpc backend fields when route does not set them", func(t *testing.T) { diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml index cee5c07d67..10d7f06540 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml @@ -79,7 +79,7 @@ securityPolicies: name: users-secret # merge extAuth check: # - can resolve backendRef in parent ns (envoy-gateway) - # - replace contextExtensions entirely with the values configured in route policy + # - merge contextExtensions by name and resolve each item from its owning policy namespace extAuth: grpc: backendRefs: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml index 1f53dc1806..0909c3ff40 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml @@ -309,6 +309,8 @@ xdsIR: value: '[redacted]' - name: route-only value: '[redacted]' + - name: parent-only + value: '[redacted]' grpc: authority: grpc-backend.envoy-gateway:9000 destination: diff --git a/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml index eeb4cb0d16..8eeabc5212 100644 --- a/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml +++ b/internal/utils/testdata/securitypolicy_all.strategicmerge.out.yaml @@ -29,6 +29,9 @@ spec: - name: route-only type: Value value: route-only + - name: parent-only + type: Value + value: parent-only grpc: backendRefs: - name: ext-auth-route From 9bfdafb64004ae26c165b94e5b4e049ba07f5d85 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Thu, 30 Apr 2026 12:11:25 +0900 Subject: [PATCH 07/11] update: merge owner logic Signed-off-by: kkk777-7 --- internal/gatewayapi/helpers.go | 16 -- internal/gatewayapi/securitypolicy.go | 298 ++++++++++++--------- internal/gatewayapi/securitypolicy_test.go | 110 ++++---- 3 files changed, 220 insertions(+), 204 deletions(-) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 0a8e0168ae..ea9ad9b5be 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -683,22 +683,6 @@ type GatewayPolicyRouteMap struct { SectionIndex map[types.NamespacedName]sets.Set[string] } -type FieldPath string - -type PolicyFieldOwners[T client.Object] map[FieldPath]T - -// resolvePolicyFieldOwner returns the owner for a policy spec field. -// If owners is nil or does not contain the field key, defaultOwner is returned as the owner. -func resolvePolicyFieldOwner[T client.Object](owners PolicyFieldOwners[T], field FieldPath, defaultOwner T) T { - ownerPolicy := defaultOwner - if owners != nil { - if owner, ok := owners[field]; ok { - ownerPolicy = owner - } - } - return ownerPolicy -} - // listenersWithSameHTTPPort returns a list of the names of all other HTTP listeners // that would share the same filter chain as the provided listener when translated // to XDS diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index acb3f65695..188b6b800d 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -54,29 +54,8 @@ const ( oidcHMACSecretKey = "hmac-secret" // JWKSConfigMapKey is the key used in ConfigMaps to store JWKS data JWKSConfigMapKey = "jwks" - - spFieldBasicAuth FieldPath = "spec.basicAuth" - // nolint: gosec - spFieldAPIKeyAuthCreds FieldPath = "spec.apiKeyAuth.credentialRefs" - spFieldAuthRules FieldPath = "spec.authorization.rules" - spFieldExtAuth FieldPath = "spec.extAuth" - spFieldExtAuthHTTPBackendRef FieldPath = "spec.extAuth.http.backendRef" - spFieldExtAuthHTTPBackendRefs FieldPath = "spec.extAuth.http.backendRefs" - spFieldExtAuthGRPCBackendRef FieldPath = "spec.extAuth.grpc.backendRef" - spFieldExtAuthGRPCBackendRefs FieldPath = "spec.extAuth.grpc.backendRefs" - spFieldExtAuthContextExtensions FieldPath = "spec.extAuth.contextExtensions" - spFieldOIDC FieldPath = "spec.oidc" - spFieldOIDCProviderBackendRefs FieldPath = "spec.oidc.provider.backendRefs" - spFieldOIDCClientIDRef FieldPath = "spec.oidc.clientIDRef" - // nolint: gosec - spFieldOIDCClientSecret FieldPath = "spec.oidc.clientSecret" - spFieldJwtProviders FieldPath = "spec.jwt.providers" ) -func spFieldExtAuthContextExtension(name string) FieldPath { - return FieldPath(fmt.Sprintf("%s.%s", spFieldExtAuthContextExtensions, name)) -} - // deprecatedFieldsUsedInSecurityPolicy returns a map of deprecated field paths to their alternatives. func deprecatedFieldsUsedInSecurityPolicy(policy *egv1a1.SecurityPolicy) map[string]string { deprecatedFields := make(map[string]string) @@ -389,7 +368,7 @@ func (t *Translator) processSecurityPolicyForRoute( // Check if merging is enabled if policy.Spec.MergeType == nil { // No merging - use existing translation logic - if err := t.translateSecurityPolicyForRoute(policy, nil, targetedRoute, currTarget, resources, xdsIR, nil, nil); err != nil { + if err := t.translateSecurityPolicyForRoute(policy, &securityPolicyOwners{}, targetedRoute, currTarget, resources, xdsIR, nil, nil); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, @@ -419,7 +398,7 @@ func (t *Translator) processSecurityPolicyForRoute( if gwPolicy == nil && listenerPolicy == nil { // No parent policy found, fall back to current policy - if err := t.translateSecurityPolicyForRoute(policy, nil, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { + if err := t.translateSecurityPolicyForRoute(policy, &securityPolicyOwners{}, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, @@ -438,7 +417,7 @@ func (t *Translator) processSecurityPolicyForRoute( } // Merge with parent policy - mergedPolicy, fieldOwners, err := mergeSecurityPolicy(policy, parentPolicy) + mergedPolicy, owners, err := mergeSecurityPolicy(policy, parentPolicy) if err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, @@ -464,7 +443,7 @@ func (t *Translator) processSecurityPolicyForRoute( } // Apply merged policy - if err := t.translateSecurityPolicyForRoute(mergedPolicy, fieldOwners, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { + if err := t.translateSecurityPolicyForRoute(mergedPolicy, owners, targetedRoute, currTarget, resources, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, @@ -844,7 +823,7 @@ func resolveSecurityPolicyRouteTargetRef( func (t *Translator) translateSecurityPolicyForRoute( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, route RouteContext, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, resources *resource.Resources, @@ -869,7 +848,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.BasicAuth != nil { if basicAuth, err = t.buildBasicAuth( policy, - fieldOwners, + owners, resources, ); err != nil { err = perr.WithMessage(err, "BasicAuth") @@ -880,7 +859,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.APIKeyAuth != nil { if apiKeyAuth, err = t.buildAPIKeyAuth( policy, - fieldOwners, + owners, resources, ); err != nil { err = perr.WithMessage(err, "APIKeyAuth") @@ -889,7 +868,7 @@ func (t *Translator) translateSecurityPolicyForRoute( } if policy.Spec.Authorization != nil { - if authorization, err = t.buildAuthorization(policy, fieldOwners); err != nil { + if authorization, err = t.buildAuthorization(policy, owners); err != nil { err = perr.WithMessage(err, "Authorization") errs = errors.Join(errs, err) } @@ -929,7 +908,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.ExtAuth != nil { if extAuth, extAuthErr = t.buildExtAuth( policy, - fieldOwners, + owners, resources, gtwCtx, ); extAuthErr != nil { @@ -942,7 +921,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.OIDC != nil { if oidc, err = t.buildOIDC( policy, - fieldOwners, + owners, resources, gtwCtx, ); err != nil { @@ -956,7 +935,7 @@ func (t *Translator) translateSecurityPolicyForRoute( if policy.Spec.JWT != nil { if jwt, err = t.buildJWT( policy, - fieldOwners, + owners, resources, gtwCtx, ); err != nil { @@ -1087,6 +1066,7 @@ func (t *Translator) translateSecurityPolicyForGateway( xdsIR resource.XdsIRMap, ) error { // Build IR + noOwners := &securityPolicyOwners{} var ( cors *ir.CORS jwt *ir.JWT @@ -1106,7 +1086,7 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.JWT != nil { if jwt, err = t.buildJWT( policy, - nil, + noOwners, resources, gtwCtx, ); err != nil { @@ -1118,7 +1098,7 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.OIDC != nil { if oidc, err = t.buildOIDC( policy, - nil, + noOwners, resources, gtwCtx, ); err != nil { @@ -1130,7 +1110,7 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.BasicAuth != nil { if basicAuth, err = t.buildBasicAuth( policy, - nil, + noOwners, resources, ); err != nil { err = perr.WithMessage(err, "BasicAuth") @@ -1141,7 +1121,7 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.APIKeyAuth != nil { if apiKeyAuth, err = t.buildAPIKeyAuth( policy, - nil, + noOwners, resources, ); err != nil { err = perr.WithMessage(err, "APIKeyAuth") @@ -1150,7 +1130,7 @@ func (t *Translator) translateSecurityPolicyForGateway( } if policy.Spec.Authorization != nil { - if authorization, err = t.buildAuthorization(policy, nil); err != nil { + if authorization, err = t.buildAuthorization(policy, noOwners); err != nil { errs = errors.Join(errs, err) } } @@ -1160,7 +1140,7 @@ func (t *Translator) translateSecurityPolicyForGateway( if policy.Spec.ExtAuth != nil { if extAuth, extAuthErr = t.buildExtAuth( policy, - nil, + noOwners, resources, gtwCtx, ); extAuthErr != nil { @@ -1337,7 +1317,7 @@ func wildcard2regex(wildcard string) string { func (t *Translator) buildJWT( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, gtwCtx *GatewayContext, ) (*ir.JWT, error) { @@ -1345,7 +1325,7 @@ func (t *Translator) buildJWT( return nil, err } - jwtOwnerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldJwtProviders, policy) + jwtOwnerPolicy := policyOwnerOr(owners.jwtProviders, policy) providers := make([]ir.JWTProvider, 0, len(policy.Spec.JWT.Providers)) for i, p := range policy.Spec.JWT.Providers { provider := ir.JWTProvider{ @@ -1554,7 +1534,7 @@ func (t *Translator) buildLocalJWKS( func (t *Translator) buildOIDC( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, gtwCtx *GatewayContext, ) (*ir.OIDC, error) { @@ -1573,7 +1553,7 @@ func (t *Translator) buildOIDC( err error ) - if provider, err = t.buildOIDCProvider(policy, fieldOwners, resources, gtwCtx); err != nil { + if provider, err = t.buildOIDCProvider(policy, owners, resources, gtwCtx); err != nil { return nil, err } @@ -1582,7 +1562,7 @@ func (t *Translator) buildOIDC( case oidc.ClientID != nil: clientID = *oidc.ClientID case oidc.ClientIDRef != nil: - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCClientIDRef, policy) + ownerPolicy := policyOwnerOr(owners.oidcClientIDRef, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, @@ -1603,11 +1583,11 @@ func (t *Translator) buildOIDC( return nil, fmt.Errorf("client ID must be specified in OIDC policy %s/%s", policy.Namespace, policy.Name) } - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCClientSecret, policy) + clientSecretOwner := policyOwnerOr(owners.oidcClientSecret, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, - namespace: ownerPolicy.Namespace, + namespace: clientSecretOwner.Namespace, } if clientSecret, err = t.validateSecretRef(true, from, oidc.ClientSecret, resources); err != nil { return nil, err @@ -1666,8 +1646,9 @@ func (t *Translator) buildOIDC( "HMAC secret not found in secret %s/%s", t.ControllerNamespace, oidcHMACSecretName) } + oidcOwner := policyOwnerOr(owners.oidc, policy) irOIDC := &ir.OIDC{ - Name: irConfigName(resolvePolicyFieldOwner(fieldOwners, spFieldOIDC, policy)), + Name: irConfigName(oidcOwner), Provider: *provider, ClientID: clientID, ClientSecret: clientSecretBytes, @@ -1717,7 +1698,7 @@ func (t *Translator) buildOIDC( func (t *Translator) buildOIDCProvider( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, gtwCtx *GatewayContext, ) (*ir.OIDCProvider, error) { @@ -1750,10 +1731,10 @@ func (t *Translator) buildOIDCProvider( protocol = ir.HTTP } - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldOIDCProviderBackendRefs, policy) + oidcProviderOwner := policyOwnerOr(owners.oidcProviderBackendRefs, policy) if len(provider.BackendRefs) > 0 { if rd, err = t.translateExtServiceBackendRefs( - ownerPolicy, provider.BackendRefs, protocol, resources, gtwCtx, "oidc", 0); err != nil { + oidcProviderOwner, provider.BackendRefs, protocol, resources, gtwCtx, "oidc", 0); err != nil { return nil, err } } @@ -2033,10 +2014,10 @@ func validateTokenEndpoint(tokenEndpoint string) error { func (t *Translator) buildAPIKeyAuth( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, ) (*ir.APIKeyAuth, error) { - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAPIKeyAuthCreds, policy) + ownerPolicy := policyOwnerOr(owners.apiKeyAuthCredentialRefs, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, @@ -2091,7 +2072,7 @@ func (t *Translator) buildAPIKeyAuth( func (t *Translator) buildBasicAuth( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, ) (*ir.BasicAuth, error) { var ( @@ -2100,7 +2081,7 @@ func (t *Translator) buildBasicAuth( err error ) - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldBasicAuth, policy) + ownerPolicy := policyOwnerOr(owners.basicAuth, policy) from := crossNamespaceFrom{ group: egv1a1.GroupName, kind: resource.KindSecurityPolicy, @@ -2166,24 +2147,25 @@ func validateHtpasswdFormat(data []byte) error { func (t *Translator) buildExtAuth( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, resources *resource.Resources, gtwCtx *GatewayContext, ) (*ir.ExtAuth, error) { var ( - http = policy.Spec.ExtAuth.HTTP - grpc = policy.Spec.ExtAuth.GRPC - backendRefs []egv1a1.BackendRef - backendSettings *egv1a1.ClusterSettings - protocol ir.AppProtocol - rd *ir.RouteDestination - authority string - err error - traffic *ir.TrafficFeatures - contextExtensions []*ir.ContextExtention - backendRefsOwnerPolicy *egv1a1.SecurityPolicy + http = policy.Spec.ExtAuth.HTTP + grpc = policy.Spec.ExtAuth.GRPC + backendRefs []egv1a1.BackendRef + backendSettings *egv1a1.ClusterSettings + protocol ir.AppProtocol + rd *ir.RouteDestination + authority string + err error + traffic *ir.TrafficFeatures + contextExtensions []*ir.ContextExtention ) + backendRefsOwnerPolicy := policyOwnerOr(owners.extAuthBackendRefs, policy) + // These are sanity checks, they should never happen because the API server // should have caught them if http == nil && grpc == nil { @@ -2199,14 +2181,12 @@ func (t *Translator) buildExtAuth( switch { case len(http.BackendRefs) > 0: backendRefs = http.BackendRefs - backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthHTTPBackendRefs, policy) case http.BackendRef != nil: backendRefs = []egv1a1.BackendRef{ { BackendObjectReference: *http.BackendRef, }, } - backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthHTTPBackendRef, policy) default: // This is a sanity check, it should never happen because the API server should have caught it return nil, errors.New("http backend refs must be specified") @@ -2217,14 +2197,12 @@ func (t *Translator) buildExtAuth( switch { case len(grpc.BackendRefs) > 0: backendRefs = grpc.BackendRefs - backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthGRPCBackendRefs, policy) case grpc.BackendRef != nil: backendRefs = []egv1a1.BackendRef{ { BackendObjectReference: *grpc.BackendRef, }, } - backendRefsOwnerPolicy = resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthGRPCBackendRef, policy) default: // This is a sanity check, it should never happen because the API server should have caught it return nil, errors.New("grpc backend refs must be specified") @@ -2250,12 +2228,13 @@ func (t *Translator) buildExtAuth( return nil, err } - if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, fieldOwners, policy); err != nil { + if contextExtensions, err = t.buildContextExtensions(policy.Spec.ExtAuth.ContextExtensions, owners, policy); err != nil { return nil, err } + extAuthOwner := policyOwnerOr(owners.extAuth, policy) extAuth := &ir.ExtAuth{ - Name: irConfigName(resolvePolicyFieldOwner(fieldOwners, spFieldExtAuth, policy)), + Name: irConfigName(extAuthOwner), HeadersToExtAuth: policy.Spec.ExtAuth.HeadersToExtAuth, ContextExtensions: contextExtensions, FailOpen: policy.Spec.ExtAuth.FailOpen, @@ -2306,7 +2285,7 @@ func parseExtAuthTimeout(timeout *gwapiv1.Duration) *metav1.Duration { func (t *Translator) buildContextExtensions( contextExtensions []*egv1a1.ContextExtension, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, defaultOwner *egv1a1.SecurityPolicy, ) ([]*ir.ContextExtention, error) { if len(contextExtensions) == 0 { @@ -2317,7 +2296,7 @@ func (t *Translator) buildContextExtensions( for _, ext := range contextExtensions { var value ir.PrivateBytes if ext.Type == egv1a1.ContextExtensionValueTypeValueRef { - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldExtAuthContextExtension(ext.Name), defaultOwner) + ownerPolicy := policyOwnerOr(owners.extAuthContextExtensions[ext.Name], defaultOwner) var err error if value, err = t.getContextExtensionValueFromRef(ext.ValueRef, ownerPolicy.Namespace); err != nil { return nil, err @@ -2408,7 +2387,7 @@ func (t *Translator) backendRefAuthority( func (t *Translator) buildAuthorization( policy *egv1a1.SecurityPolicy, - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy], + owners *securityPolicyOwners, ) (*ir.Authorization, error) { var ( authorization = policy.Spec.Authorization @@ -2417,7 +2396,7 @@ func (t *Translator) buildAuthorization( defaultAction = egv1a1.AuthorizationActionDeny ) - ownerPolicy := resolvePolicyFieldOwner(fieldOwners, spFieldAuthRules, policy) + ownerPolicy := policyOwnerOr(owners.authorizationRules, policy) if authorization.DefaultAction != nil { defaultAction = *authorization.DefaultAction @@ -2545,84 +2524,145 @@ func defaultAuthorizationRuleName(policy *egv1a1.SecurityPolicy, index int) stri strconv.Itoa(index)) } +type securityPolicyOwners struct { + basicAuth *egv1a1.SecurityPolicy + apiKeyAuthCredentialRefs *egv1a1.SecurityPolicy + authorizationRules *egv1a1.SecurityPolicy + extAuth *egv1a1.SecurityPolicy + extAuthBackendRefs *egv1a1.SecurityPolicy + extAuthContextExtensions map[string]*egv1a1.SecurityPolicy + oidc *egv1a1.SecurityPolicy + oidcProviderBackendRefs *egv1a1.SecurityPolicy + oidcClientIDRef *egv1a1.SecurityPolicy + oidcClientSecret *egv1a1.SecurityPolicy + jwtProviders *egv1a1.SecurityPolicy +} + +// policyOwnerOr returns owner if non-nil, otherwise fallback. +// Used to resolve per-field owners from securityPolicyOwners: the owner is the policy +// that contributed the field (route overrides parent), falling back to the active policy +// when no merge occurred or the field was not set by either side. +func policyOwnerOr(owner, fallback *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if owner != nil { + return owner + } + return fallback +} + // mergeSecurityPolicy merges a route-level SecurityPolicy with a parent (Gateway/Listener) SecurityPolicy. -func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, PolicyFieldOwners[*egv1a1.SecurityPolicy], error) { +func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, *securityPolicyOwners, error) { if routePolicy.Spec.MergeType == nil || parentPolicy == nil { return routePolicy, nil, nil } - fieldOwners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) mergedPolicy, err := utils.Merge[*egv1a1.SecurityPolicy](parentPolicy, routePolicy, *routePolicy.Spec.MergeType) if err != nil { return nil, nil, err } - return mergedPolicy, fieldOwners, nil + return mergedPolicy, &securityPolicyOwners{ + basicAuth: chooseBasicAuthOwner(routePolicy, parentPolicy), + apiKeyAuthCredentialRefs: chooseAPIKeyAuthOwner(routePolicy, parentPolicy), + authorizationRules: chooseAuthorizationRulesOwner(routePolicy, parentPolicy), + extAuth: chooseExtAuthOwner(routePolicy, parentPolicy), + extAuthBackendRefs: chooseExtAuthBackendRefsOwner(routePolicy, parentPolicy), + extAuthContextExtensions: buildExtAuthContextExtensionOwners(routePolicy, parentPolicy), + oidc: chooseOIDCOwner(routePolicy, parentPolicy), + oidcProviderBackendRefs: chooseOIDCProviderBackendRefsOwner(routePolicy, parentPolicy), + oidcClientIDRef: chooseOIDCClientIDRefOwner(routePolicy, parentPolicy), + oidcClientSecret: chooseOIDCClientSecretOwner(routePolicy, parentPolicy), + jwtProviders: chooseJWTProvidersOwner(routePolicy, parentPolicy), + }, nil } -func buildSecurityPolicyFieldOwners( - routePolicy *egv1a1.SecurityPolicy, - parentPolicy *egv1a1.SecurityPolicy, -) PolicyFieldOwners[*egv1a1.SecurityPolicy] { - // Route policy owners are applied last so they override parent owners when both define the same field key. - owners := make(PolicyFieldOwners[*egv1a1.SecurityPolicy]) - applySecurityPolicyFieldOwners(owners, parentPolicy) - applySecurityPolicyFieldOwners(owners, routePolicy) - return owners +func chooseBasicAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.BasicAuth != nil { + return route + } + return parent } -func applySecurityPolicyFieldOwners( - owners PolicyFieldOwners[*egv1a1.SecurityPolicy], - policy *egv1a1.SecurityPolicy, -) { - if policy == nil { - return +func chooseAPIKeyAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.APIKeyAuth != nil && len(route.Spec.APIKeyAuth.CredentialRefs) > 0 { + return route } + return parent +} - if policy.Spec.BasicAuth != nil { - owners[spFieldBasicAuth] = policy +func chooseAuthorizationRulesOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.Authorization != nil && len(route.Spec.Authorization.Rules) > 0 { + return route } - if policy.Spec.APIKeyAuth != nil && - len(policy.Spec.APIKeyAuth.CredentialRefs) > 0 { - owners[spFieldAPIKeyAuthCreds] = policy + return parent +} + +func chooseExtAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.ExtAuth != nil { + return route } - if policy.Spec.Authorization != nil && - len(policy.Spec.Authorization.Rules) > 0 { - owners[spFieldAuthRules] = policy + return parent +} + +func chooseExtAuthBackendRefsOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + ea := route.Spec.ExtAuth + if ea == nil { + return parent } - if policy.Spec.ExtAuth != nil { - owners[spFieldExtAuth] = policy + if ea.HTTP != nil && (len(ea.HTTP.BackendRefs) > 0 || ea.HTTP.BackendRef != nil) { + return route + } + if ea.GRPC != nil && (len(ea.GRPC.BackendRefs) > 0 || ea.GRPC.BackendRef != nil) { + return route + } + return parent +} - if policy.Spec.ExtAuth.HTTP != nil && len(policy.Spec.ExtAuth.HTTP.BackendRefs) > 0 { - owners[spFieldExtAuthHTTPBackendRefs] = policy +func buildExtAuthContextExtensionOwners(route, parent *egv1a1.SecurityPolicy) map[string]*egv1a1.SecurityPolicy { + owners := make(map[string]*egv1a1.SecurityPolicy) + if parent.Spec.ExtAuth != nil { + for _, ext := range parent.Spec.ExtAuth.ContextExtensions { + owners[ext.Name] = parent } - if policy.Spec.ExtAuth.HTTP != nil && policy.Spec.ExtAuth.HTTP.BackendRef != nil { - owners[spFieldExtAuthHTTPBackendRef] = policy - } - if policy.Spec.ExtAuth.GRPC != nil && len(policy.Spec.ExtAuth.GRPC.BackendRefs) > 0 { - owners[spFieldExtAuthGRPCBackendRefs] = policy - } - if policy.Spec.ExtAuth.GRPC != nil && policy.Spec.ExtAuth.GRPC.BackendRef != nil { - owners[spFieldExtAuthGRPCBackendRef] = policy - } - if len(policy.Spec.ExtAuth.ContextExtensions) > 0 { - for _, ext := range policy.Spec.ExtAuth.ContextExtensions { - owners[spFieldExtAuthContextExtension(ext.Name)] = policy - } + } + if route.Spec.ExtAuth != nil { + for _, ext := range route.Spec.ExtAuth.ContextExtensions { + owners[ext.Name] = route } } - if policy.Spec.OIDC != nil { - owners[spFieldOIDC] = policy - owners[spFieldOIDCClientSecret] = policy + return owners +} - if len(policy.Spec.OIDC.Provider.BackendRefs) > 0 { - owners[spFieldOIDCProviderBackendRefs] = policy - } - if policy.Spec.OIDC.ClientIDRef != nil { - owners[spFieldOIDCClientIDRef] = policy - } +func chooseOIDCOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.OIDC != nil { + return route } - if policy.Spec.JWT != nil && len(policy.Spec.JWT.Providers) > 0 { - owners[spFieldJwtProviders] = policy + return parent +} + +func chooseOIDCClientSecretOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.OIDC != nil { + return route + } + return parent +} + +func chooseOIDCProviderBackendRefsOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.OIDC != nil && len(route.Spec.OIDC.Provider.BackendRefs) > 0 { + return route + } + return parent +} + +func chooseOIDCClientIDRefOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.OIDC != nil && route.Spec.OIDC.ClientIDRef != nil { + return route + } + return parent +} + +func chooseJWTProvidersOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { + if route.Spec.JWT != nil && len(route.Spec.JWT.Providers) > 0 { + return route } + return parent } // securityPolicyCopiesWithStatusDeepCopy returns shallow copies with deep-copied Status fields. diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 4534a45865..55219f1221 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1429,7 +1429,7 @@ func Test_buildContextExtensions(t *testing.T) { tests := []struct { name string contextExtensions []*egv1a1.ContextExtension - fieldOwners PolicyFieldOwners[*egv1a1.SecurityPolicy] + owners *securityPolicyOwners defaultOwner *egv1a1.SecurityPolicy translatorContext *TranslatorContext want []*ir.ContextExtention @@ -1438,11 +1438,13 @@ func Test_buildContextExtensions(t *testing.T) { { name: "Nil", contextExtensions: nil, + owners: &securityPolicyOwners{}, want: nil, }, { name: "Empty", contextExtensions: []*egv1a1.ContextExtension{}, + owners: &securityPolicyOwners{}, want: nil, }, { @@ -1450,11 +1452,13 @@ func Test_buildContextExtensions(t *testing.T) { contextExtensions: []*egv1a1.ContextExtension{ {Name: "foo", Value: new("bar")}, }, - want: []*ir.ContextExtention{{Name: "foo", Value: ir.PrivateBytes("bar")}}, + owners: &securityPolicyOwners{}, + want: []*ir.ContextExtention{{Name: "foo", Value: ir.PrivateBytes("bar")}}, }, { name: "TypeValueEmpty", contextExtensions: []*egv1a1.ContextExtension{{Name: "foo"}}, + owners: &securityPolicyOwners{}, want: []*ir.ContextExtention{{Name: "foo", Value: nil}}, }, { @@ -1466,13 +1470,15 @@ func Test_buildContextExtensions(t *testing.T) { Value: new("bar"), }, }, - want: []*ir.ContextExtention{{Name: "foo", Value: ir.PrivateBytes("bar")}}, + owners: &securityPolicyOwners{}, + want: []*ir.ContextExtention{{Name: "foo", Value: ir.PrivateBytes("bar")}}, }, { name: "TypeValueRefNil", contextExtensions: []*egv1a1.ContextExtension{ {Name: "foo", Type: egv1a1.ContextExtensionValueTypeValueRef}, }, + owners: &securityPolicyOwners{}, wantErr: true, }, { @@ -1488,6 +1494,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{}, wantErr: true, }, @@ -1504,6 +1511,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{ ConfigMapMap: map[types.NamespacedName]*corev1.ConfigMap{ {Namespace: policyNs, Name: "test-cm"}: {}, @@ -1524,6 +1532,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{ ConfigMapMap: map[types.NamespacedName]*corev1.ConfigMap{ {Namespace: policyNs, Name: "test-cm"}: { @@ -1559,12 +1568,10 @@ func Test_buildContextExtensions(t *testing.T) { }, }, }, - fieldOwners: PolicyFieldOwners[*egv1a1.SecurityPolicy]{ - spFieldExtAuthContextExtension("parent-only"): &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "parent-ns"}, - }, - spFieldExtAuthContextExtension("route-only"): &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "route-ns"}, + owners: &securityPolicyOwners{ + extAuthContextExtensions: map[string]*egv1a1.SecurityPolicy{ + "parent-only": {ObjectMeta: metav1.ObjectMeta{Namespace: "parent-ns"}}, + "route-only": {ObjectMeta: metav1.ObjectMeta{Namespace: "route-ns"}}, }, }, defaultOwner: &egv1a1.SecurityPolicy{ @@ -1598,6 +1605,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{}, wantErr: true, }, @@ -1614,6 +1622,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{ SecretMap: map[types.NamespacedName]*corev1.Secret{ {Namespace: policyNs, Name: "test-secret"}: {}, @@ -1634,6 +1643,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, translatorContext: &TranslatorContext{ SecretMap: map[types.NamespacedName]*corev1.Secret{ {Namespace: policyNs, Name: "test-secret"}: { @@ -1656,6 +1666,7 @@ func Test_buildContextExtensions(t *testing.T) { Key: "test-key", }, }}, + owners: &securityPolicyOwners{}, wantErr: true, }, } @@ -1666,7 +1677,7 @@ func Test_buildContextExtensions(t *testing.T) { if owner == nil { owner = defaultOwner } - got, err := translator.buildContextExtensions(tt.contextExtensions, tt.fieldOwners, owner) + got, err := translator.buildContextExtensions(tt.contextExtensions, tt.owners, owner) if tt.wantErr { require.Error(t, err) require.Nil(t, got) @@ -1914,13 +1925,10 @@ func TestMergeSecurityPolicy(t *testing.T) { } } -func Test_buildSecurityPolicyFieldOwners(t *testing.T) { - t.Run("route policy overrides parent for the same owner keys", func(t *testing.T) { +func Test_securityPolicyOwnerChoose(t *testing.T) { + t.Run("route policy overrides parent for the same owner fields", func(t *testing.T) { parentPolicy := &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "parent", - Namespace: "parent-ns", - }, + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "parent-ns"}, Spec: egv1a1.SecurityPolicySpec{ BasicAuth: &egv1a1.BasicAuth{ Users: gwapiv1.SecretObjectReference{Name: "parent-users"}, @@ -1957,22 +1965,15 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { }, }, JWT: &egv1a1.JWT{ - Providers: []egv1a1.JWTProvider{{ - Name: "parent-jwt-provider", - RemoteJWKS: &egv1a1.RemoteJWKS{ - URI: "https://parent.example.com/jwks", - }, - }}, + Providers: []egv1a1.JWTProvider{{Name: "parent-jwt-provider"}}, }, }, } routePolicy := &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "route", - Namespace: "route-ns", - }, + ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: "route-ns"}, Spec: egv1a1.SecurityPolicySpec{ + MergeType: new(egv1a1.StrategicMerge), BasicAuth: &egv1a1.BasicAuth{ Users: gwapiv1.SecretObjectReference{Name: "route-users"}, }, @@ -2008,42 +2009,35 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { }, }, JWT: &egv1a1.JWT{ - Providers: []egv1a1.JWTProvider{{ - Name: "route-jwt-provider", - LocalJWKS: &egv1a1.LocalJWKS{ - Type: new(egv1a1.LocalJWKSTypeInline), - Inline: new(`{"keys":[]}`), - }, - }}, + Providers: []egv1a1.JWTProvider{{Name: "route-jwt-provider"}}, }, }, } - owners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) + _, owners, err := mergeSecurityPolicy(routePolicy, parentPolicy) + require.NoError(t, err) require.NotNil(t, owners) - assert.Same(t, routePolicy, owners[spFieldBasicAuth]) - assert.Same(t, routePolicy, owners[spFieldAPIKeyAuthCreds]) - assert.Same(t, routePolicy, owners[spFieldAuthRules]) - assert.Same(t, routePolicy, owners[spFieldExtAuth]) - assert.Same(t, routePolicy, owners[spFieldExtAuthHTTPBackendRefs]) - assert.Same(t, routePolicy, owners[spFieldOIDC]) - assert.Same(t, routePolicy, owners[spFieldOIDCClientIDRef]) - assert.Same(t, routePolicy, owners[spFieldOIDCClientSecret]) - assert.Same(t, routePolicy, owners[spFieldOIDCProviderBackendRefs]) - assert.Same(t, routePolicy, owners[spFieldJwtProviders]) - assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtension("shared")]) - assert.Same(t, routePolicy, owners[spFieldExtAuthContextExtension("route-only")]) - assert.Same(t, parentPolicy, owners[spFieldExtAuthContextExtension("parent-only")]) + assert.Same(t, routePolicy, owners.basicAuth) + assert.Same(t, routePolicy, owners.apiKeyAuthCredentialRefs) + assert.Same(t, routePolicy, owners.authorizationRules) + assert.Same(t, routePolicy, owners.extAuth) + assert.Same(t, routePolicy, owners.extAuthBackendRefs) + assert.Same(t, routePolicy, owners.oidc) + assert.Same(t, routePolicy, owners.oidcClientIDRef) + assert.Same(t, routePolicy, owners.oidcClientSecret) + assert.Same(t, routePolicy, owners.oidcProviderBackendRefs) + assert.Same(t, routePolicy, owners.jwtProviders) + assert.Same(t, routePolicy, owners.extAuthContextExtensions["shared"]) + assert.Same(t, routePolicy, owners.extAuthContextExtensions["route-only"]) + assert.Same(t, parentPolicy, owners.extAuthContextExtensions["parent-only"]) }) t.Run("uses parent owner for grpc backend fields when route does not set them", func(t *testing.T) { parentPolicy := &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "parent", - Namespace: "parent-ns", - }, + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "parent-ns"}, Spec: egv1a1.SecurityPolicySpec{ + MergeType: new(egv1a1.StrategicMerge), ExtAuth: &egv1a1.ExtAuth{ GRPC: &egv1a1.GRPCExtAuthService{ BackendCluster: egv1a1.BackendCluster{ @@ -2057,17 +2051,15 @@ func Test_buildSecurityPolicyFieldOwners(t *testing.T) { } routePolicy := &egv1a1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "route", - Namespace: "route-ns", - }, - Spec: egv1a1.SecurityPolicySpec{}, + ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: "route-ns"}, + Spec: egv1a1.SecurityPolicySpec{MergeType: new(egv1a1.StrategicMerge)}, } - owners := buildSecurityPolicyFieldOwners(routePolicy, parentPolicy) + _, owners, err := mergeSecurityPolicy(routePolicy, parentPolicy) + require.NoError(t, err) require.NotNil(t, owners) - assert.Same(t, parentPolicy, owners[spFieldExtAuth]) - assert.Same(t, parentPolicy, owners[spFieldExtAuthGRPCBackendRefs]) + assert.Same(t, parentPolicy, owners.extAuth) + assert.Same(t, parentPolicy, owners.extAuthBackendRefs) }) } From c553d7635613ebb4b9a68673396f09392516ea5f Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Thu, 30 Apr 2026 22:47:13 +0900 Subject: [PATCH 08/11] refactor: build policy owner Signed-off-by: kkk777-7 --- internal/gatewayapi/securitypolicy.go | 145 +++++++++++--------------- 1 file changed, 60 insertions(+), 85 deletions(-) diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 188b6b800d..9ec38bbf04 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -2558,63 +2558,73 @@ func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv if err != nil { return nil, nil, err } - return mergedPolicy, &securityPolicyOwners{ - basicAuth: chooseBasicAuthOwner(routePolicy, parentPolicy), - apiKeyAuthCredentialRefs: chooseAPIKeyAuthOwner(routePolicy, parentPolicy), - authorizationRules: chooseAuthorizationRulesOwner(routePolicy, parentPolicy), - extAuth: chooseExtAuthOwner(routePolicy, parentPolicy), - extAuthBackendRefs: chooseExtAuthBackendRefsOwner(routePolicy, parentPolicy), - extAuthContextExtensions: buildExtAuthContextExtensionOwners(routePolicy, parentPolicy), - oidc: chooseOIDCOwner(routePolicy, parentPolicy), - oidcProviderBackendRefs: chooseOIDCProviderBackendRefsOwner(routePolicy, parentPolicy), - oidcClientIDRef: chooseOIDCClientIDRefOwner(routePolicy, parentPolicy), - oidcClientSecret: chooseOIDCClientSecretOwner(routePolicy, parentPolicy), - jwtProviders: chooseJWTProvidersOwner(routePolicy, parentPolicy), - }, nil -} - -func chooseBasicAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.BasicAuth != nil { - return route - } - return parent -} - -func chooseAPIKeyAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.APIKeyAuth != nil && len(route.Spec.APIKeyAuth.CredentialRefs) > 0 { - return route - } - return parent + return mergedPolicy, buildSecurityPolicyOwners(routePolicy, parentPolicy), nil } -func chooseAuthorizationRulesOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.Authorization != nil && len(route.Spec.Authorization.Rules) > 0 { - return route - } - return parent -} - -func chooseExtAuthOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.ExtAuth != nil { - return route - } - return parent -} - -func chooseExtAuthBackendRefsOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - ea := route.Spec.ExtAuth - if ea == nil { - return parent - } - if ea.HTTP != nil && (len(ea.HTTP.BackendRefs) > 0 || ea.HTTP.BackendRef != nil) { - return route - } - if ea.GRPC != nil && (len(ea.GRPC.BackendRefs) > 0 || ea.GRPC.BackendRef != nil) { +// ownerOf returns route if routeOwns(route) is true, otherwise parent. +// Use this when ownership of a merged field is determined by a single predicate. +func ownerOf( + route, parent *egv1a1.SecurityPolicy, + routeOwns func(*egv1a1.SecurityPolicy) bool, +) *egv1a1.SecurityPolicy { + if routeOwns(route) { return route } return parent } +// buildSecurityPolicyOwners determines, for each merged field, which policy +// (route or parent) is considered the owner. The owner is used later to resolve +// references (e.g. Secrets, BackendRefs) scoped to the owning policy's namespace, +// and to derive IR resource names tied to the owning policy. +func buildSecurityPolicyOwners(route, parent *egv1a1.SecurityPolicy) *securityPolicyOwners { + return &securityPolicyOwners{ + basicAuth: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.BasicAuth != nil + }), + apiKeyAuthCredentialRefs: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.APIKeyAuth != nil && len(p.Spec.APIKeyAuth.CredentialRefs) > 0 + }), + authorizationRules: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.Authorization != nil && len(p.Spec.Authorization.Rules) > 0 + }), + extAuth: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.ExtAuth != nil + }), + extAuthBackendRefs: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + ea := p.Spec.ExtAuth + if ea == nil { + return false + } + if ea.HTTP != nil && (len(ea.HTTP.BackendRefs) > 0 || ea.HTTP.BackendRef != nil) { + return true + } + if ea.GRPC != nil && (len(ea.GRPC.BackendRefs) > 0 || ea.GRPC.BackendRef != nil) { + return true + } + return false + }), + extAuthContextExtensions: buildExtAuthContextExtensionOwners(route, parent), + oidc: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.OIDC != nil + }), + oidcProviderBackendRefs: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.OIDC != nil && len(p.Spec.OIDC.Provider.BackendRefs) > 0 + }), + oidcClientIDRef: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.OIDC != nil && p.Spec.OIDC.ClientIDRef != nil + }), + oidcClientSecret: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.OIDC != nil + }), + jwtProviders: ownerOf(route, parent, func(p *egv1a1.SecurityPolicy) bool { + return p.Spec.JWT != nil && len(p.Spec.JWT.Providers) > 0 + }), + } +} + +// buildExtAuthContextExtensionOwners returns a per-key owner map for ExtAuth ContextExtensions. +// Parent keys are added first so that route-level extensions take precedence on conflict. func buildExtAuthContextExtensionOwners(route, parent *egv1a1.SecurityPolicy) map[string]*egv1a1.SecurityPolicy { owners := make(map[string]*egv1a1.SecurityPolicy) if parent.Spec.ExtAuth != nil { @@ -2630,41 +2640,6 @@ func buildExtAuthContextExtensionOwners(route, parent *egv1a1.SecurityPolicy) ma return owners } -func chooseOIDCOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.OIDC != nil { - return route - } - return parent -} - -func chooseOIDCClientSecretOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.OIDC != nil { - return route - } - return parent -} - -func chooseOIDCProviderBackendRefsOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.OIDC != nil && len(route.Spec.OIDC.Provider.BackendRefs) > 0 { - return route - } - return parent -} - -func chooseOIDCClientIDRefOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.OIDC != nil && route.Spec.OIDC.ClientIDRef != nil { - return route - } - return parent -} - -func chooseJWTProvidersOwner(route, parent *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if route.Spec.JWT != nil && len(route.Spec.JWT.Providers) > 0 { - return route - } - return parent -} - // securityPolicyCopiesWithStatusDeepCopy returns shallow copies with deep-copied Status fields. // Status is mutated during translation and shares a pointer with the watchable coalesce goroutine. func securityPolicyCopiesWithStatusDeepCopy(policies []*egv1a1.SecurityPolicy) []*egv1a1.SecurityPolicy { From 2906e1d0f1291978fd252f88d7a45b9d61b1f989 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Thu, 30 Apr 2026 23:31:05 +0900 Subject: [PATCH 09/11] add: release note and behavior of policy references Signed-off-by: kkk777-7 --- release-notes/current.yaml | 1 + .../en/latest/concepts/gateway_api_extensions/security-policy.md | 1 + 2 files changed, 2 insertions(+) diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 50e41645ad..7d1de9747e 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -79,6 +79,7 @@ bug fixes: | Fixed a control plane panic caused by concurrent Status mutation racing with the watchable Map coalesce goroutine. Fixed BackendTrafficPolicy rate limit `requests` values above uint32 max (4294967295) being silently truncated modulo 2^32 by the rate limit service and Envoy token bucket. The field now rejects such values at admission time with a clear schema validation error. See envoyproxy/ai-gateway#2012. Fixed status conditions not being updated when a route is rejected due to multiple errors. + Fixed SecurityPolicy merge using the wrong policy as the owner for resource references and IR generation. # Enhancements that improve performance. performance improvements: | 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 b576cff9ac..d1454f2f03 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 @@ -268,6 +268,7 @@ In this example, the route-level policy merges with the gateway-level policy, re - 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 +- Secret references and backend references are resolved against the namespace of the **policy that originally configured the field** (either route or parent). For example, if a Gateway policy defines BasicAuth, its secret is looked up in the Gateway policy's namespace even after merging. ## Related Resources - [API Key Authentication](../../tasks/security/apikey-auth.md) From edac8d243e96349a46fc6deb81966e4bbdb38c23 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Mon, 4 May 2026 16:15:08 +0900 Subject: [PATCH 10/11] address codex comments Signed-off-by: kkk777-7 --- internal/gatewayapi/securitypolicy.go | 5 +-- ...policy-with-merge-needs-fieldowner.in.yaml | 33 +++++++++++++++++++ ...olicy-with-merge-needs-fieldowner.out.yaml | 22 ++++++++----- release-notes/current.yaml | 1 + 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 8d4d455dc4..f9d9fd43d2 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -1669,10 +1669,12 @@ func (t *Translator) buildOIDC( disableTokenEncryption = *oidc.DisableTokenEncryption } + oidcOwner := policyOwnerOr(owners.oidc, policy) + // Generate a unique cookie suffix for oauth filters. // This is to avoid cookie name collision when multiple security policies are applied // to the same route. - suffix := utils.Digest32(string(policy.UID)) + suffix := utils.Digest32(string(oidcOwner.UID)) // Get the HMAC secret. // HMAC secret is generated by the CertGen job and stored in a secret @@ -1688,7 +1690,6 @@ func (t *Translator) buildOIDC( "HMAC secret not found in secret %s/%s", t.ControllerNamespace, oidcHMACSecretName) } - oidcOwner := policyOwnerOr(owners.oidc, policy) irOIDC := &ir.OIDC{ Name: irConfigName(oidcOwner), Provider: *provider, diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml index 10d7f06540..a4e0a5fa78 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml @@ -42,6 +42,16 @@ services: - port: 9000 name: grpc protocol: TCP +- apiVersion: v1 + kind: Service + metadata: + namespace: default + name: grpc-backend + spec: + ports: + - port: 9000 + name: grpc + protocol: TCP endpointSlices: - apiVersion: discovery.k8s.io/v1 kind: EndpointSlice @@ -60,12 +70,30 @@ endpointSlices: - 8.8.8.8 conditions: ready: true +- apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + namespace: default + name: endpointslice-grpc-backend + labels: + kubernetes.io/service-name: grpc-backend + addressType: IPv4 + ports: + - name: grpc + protocol: TCP + port: 9000 + endpoints: + - addresses: + - 9.9.9.9 + conditions: + ready: true securityPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: namespace: envoy-gateway name: policy-for-gateway + uid: "aaaaaaaa-0000-0000-0000-000000000001" # check: oidc suffix value [c3556d06] spec: targetRefs: - group: gateway.networking.k8s.io @@ -121,6 +149,7 @@ securityPolicies: metadata: namespace: default name: policy-for-route + uid: "bbbbbbbb-0000-0000-0000-000000000002" # check: oidc suffix value [6ac6170b] spec: mergeType: StrategicMerge targetRefs: @@ -128,6 +157,10 @@ securityPolicies: kind: HTTPRoute name: httproute-1 extAuth: + grpc: + backendRefs: + - name: grpc-backend + port: 9000 contextExtensions: - name: shared type: ValueRef diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml index 0909c3ff40..6abe6abfb9 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.out.yaml @@ -117,6 +117,7 @@ securityPolicies: metadata: name: policy-for-route namespace: default + uid: bbbbbbbb-0000-0000-0000-000000000002 spec: extAuth: contextExtensions: @@ -134,6 +135,10 @@ securityPolicies: key: data kind: ConfigMap name: context-extension-for-route + grpc: + backendRefs: + - name: grpc-backend + port: 9000 mergeType: StrategicMerge targetRefs: - group: gateway.networking.k8s.io @@ -164,6 +169,7 @@ securityPolicies: metadata: name: policy-for-gateway namespace: envoy-gateway + uid: aaaaaaaa-0000-0000-0000-000000000001 spec: basicAuth: users: @@ -312,31 +318,31 @@ xdsIR: - name: parent-only value: '[redacted]' grpc: - authority: grpc-backend.envoy-gateway:9000 + authority: grpc-backend.default:9000 destination: metadata: kind: SecurityPolicy - name: policy-for-gateway - namespace: envoy-gateway - name: securitypolicy/envoy-gateway/policy-for-gateway/extauth/0 + name: policy-for-route + namespace: default + name: securitypolicy/default/policy-for-route/extauth/0 settings: - addressType: IP endpoints: - - host: 8.8.8.8 + - host: 9.9.9.9 port: 9000 metadata: kind: Service name: grpc-backend - namespace: envoy-gateway + namespace: default sectionName: "9000" - name: securitypolicy/envoy-gateway/policy-for-gateway/extauth/0/backend/0 + name: securitypolicy/default/policy-for-route/extauth/0/backend/0 protocol: GRPC weight: 1 name: securitypolicy/default/policy-for-route oidc: clientID: client1.apps.googleusercontent.com clientSecret: '[redacted]' - cookieSuffix: 811c9dc5 + cookieSuffix: c3556d06 hmacSecret: '[redacted]' logoutPath: /logout name: securitypolicy/envoy-gateway/policy-for-gateway diff --git a/release-notes/current.yaml b/release-notes/current.yaml index d092e7f776..7211ceb5ea 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -2,6 +2,7 @@ date: Pending # Changes that are expected to cause an incompatibility with previous versions, such as deletions or modifications to existing APIs. breaking changes: | + Merged SecurityPolicy IR/xDS resource names (OIDC, BasicAuth, ExtAuth, JWT) now derive from the policy that contributes the field (parent or route) rather than always using the route-level policy. EnvoyPatchPolicy users who reference those generated names must update their patch targets. # Updates addressing vulnerabilities, security flaws, or compliance requirements. security updates: | From 3428bd3200aee945e24aa8e82181d6fcd19f7425 Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Mon, 4 May 2026 22:33:02 +0900 Subject: [PATCH 11/11] fix: yaml lint Signed-off-by: kkk777-7 --- .../securitypolicy-with-merge-needs-fieldowner.in.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml index a4e0a5fa78..691512bb28 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-merge-needs-fieldowner.in.yaml @@ -93,7 +93,8 @@ securityPolicies: metadata: namespace: envoy-gateway name: policy-for-gateway - uid: "aaaaaaaa-0000-0000-0000-000000000001" # check: oidc suffix value [c3556d06] + # check: oidc suffix value [c3556d06] + uid: "aaaaaaaa-0000-0000-0000-000000000001" spec: targetRefs: - group: gateway.networking.k8s.io @@ -149,7 +150,8 @@ securityPolicies: metadata: namespace: default name: policy-for-route - uid: "bbbbbbbb-0000-0000-0000-000000000002" # check: oidc suffix value [6ac6170b] + # check: oidc suffix value [6ac6170b] + uid: "bbbbbbbb-0000-0000-0000-000000000002" spec: mergeType: StrategicMerge targetRefs: