Repository: envoyproxy/gateway

Issue:
error when updating gateway status
```
2025-11-03T07:18:26.156Z	INFO	gateway-api	gatewayapi/translator.go:402	EnvoyProxy for Gateway invalid	{"runner": "gateway-api", "namespace": "default", "name": "metrics"}
2025-11-03T07:18:26.156Z	INFO	provider	kubernetes/status_updater.go:143	received a status update	{"runner": "provider", "namespace": "default", "name": "metrics", "kind": "Gateway"}
2025-11-03T07:18:26.162Z	ERROR	provider	kubernetes/status_updater.go:118	unable to update status	{"runner": "provider", "name": "metrics", "namespace": "default", "kind": "Gateway", "error": "Gateway.gateway.networking.k8s.io \"metrics\" is invalid: [status.listeners[0].conditions: Required value, status.listeners[0].supportedKinds: Required value, <nil>: Invalid value: \"null\": some validation rules were not checked because the object was invalid; correct the existing errors to complete validation]"}
```

Hints from issue comments:

Solution patch:
diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go
index bc895a244..f7c2fc8ca 100644
--- a/api/v1alpha1/ratelimit_types.go
+++ b/api/v1alpha1/ratelimit_types.go
@@ -10,13 +10,14 @@ import (
 )
 
 // RateLimitSpec defines the desired state of RateLimitSpec.
-// +union
 type RateLimitSpec struct {
 	// Type decides the scope for the RateLimits.
 	// Valid RateLimitType values are "Global" or "Local".
 	//
-	// +unionDiscriminator
-	Type RateLimitType `json:"type"`
+	// Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting.
+	//
+	// +optional
+	Type *RateLimitType `json:"type,omitempty"`
 	// Global defines global rate limit configuration.
 	//
 	// +optional
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index e8c1089d5..fe24643fa 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -6113,6 +6113,11 @@ func (in *RateLimitSelectCondition) DeepCopy() *RateLimitSelectCondition {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RateLimitSpec) DeepCopyInto(out *RateLimitSpec) {
 	*out = *in
+	if in.Type != nil {
+		in, out := &in.Type, &out.Type
+		*out = new(RateLimitType)
+		**out = **in
+	}
 	if in.Global != nil {
 		in, out := &in.Global, &out.Global
 		*out = new(GlobalRateLimit)
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 180277e12..be726b946 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -1544,12 +1544,12 @@ spec:
                     description: |-
                       Type decides the scope for the RateLimits.
                       Valid RateLimitType values are "Global" or "Local".
+
+                      Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting.
                     enum:
                     - Global
                     - Local
                     type: string
-                required:
-                - type
                 type: object
               requestBuffer:
                 description: |-
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 42308f8f3..f0fa3b569 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -1543,12 +1543,12 @@ spec:
                     description: |-
                       Type decides the scope for the RateLimits.
                       Valid RateLimitType values are "Global" or "Local".
+
+                      Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting.
                     enum:
                     - Global
                     - Local
                     type: string
-                required:
-                - type
                 type: object
               requestBuffer:
                 description: |-
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index e3fa4843e..a339d3b8c 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -1037,14 +1037,18 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 }
 
 func (t *Translator) buildRateLimit(policy *egv1a1.BackendTrafficPolicy) (*ir.RateLimit, error) {
-	switch policy.Spec.RateLimit.Type {
-	case egv1a1.GlobalRateLimitType:
-		return t.buildGlobalRateLimit(policy)
-	case egv1a1.LocalRateLimitType:
-		return t.buildLocalRateLimit(policy)
+	// For backward compatibility, process the deprecated Type field if specified.
+	if policy.Spec.RateLimit.Type != nil {
+		switch *policy.Spec.RateLimit.Type {
+		case egv1a1.GlobalRateLimitType:
+			return t.buildGlobalRateLimit(policy)
+		case egv1a1.LocalRateLimitType:
+			return t.buildLocalRateLimit(policy)
+		}
+		return nil, fmt.Errorf("invalid rateLimit type: %s", *policy.Spec.RateLimit.Type)
 	}
 
-	return nil, fmt.Errorf("invalid rateLimit type: %s", policy.Spec.RateLimit.Type)
+	return t.buildBothRateLimit(policy)
 }
 
 func (t *Translator) buildLocalRateLimit(policy *egv1a1.BackendTrafficPolicy) (*ir.RateLimit, error) {
@@ -1147,6 +1151,35 @@ func (t *Translator) buildGlobalRateLimit(policy *egv1a1.BackendTrafficPolicy) (
 	return rateLimit, nil
 }
 
+func (t *Translator) buildBothRateLimit(policy *egv1a1.BackendTrafficPolicy) (*ir.RateLimit, error) {
+	var (
+		localRateLimit  *ir.RateLimit
+		globalRateLimit *ir.RateLimit
+		err             error
+	)
+
+	if policy.Spec.RateLimit.Local != nil {
+		localRateLimit, err = t.buildLocalRateLimit(policy)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if policy.Spec.RateLimit.Global != nil {
+		globalRateLimit, err = t.buildGlobalRateLimit(policy)
+		if err != nil {
+			return nil, err
+		}
+	}
+	rl := &ir.RateLimit{}
+	if localRateLimit != nil && localRateLimit.Local != nil {
+		rl.Local = localRateLimit.Local
+	}
+	if globalRateLimit != nil && globalRateLimit.Global != nil {
+		rl.Global = globalRateLimit.Global
+	}
+	return rl, nil
+}
+
 func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) {
 	irRule := &ir.RateLimitRule{
 		Limit: ir.RateLimitValue{
@@ -1213,9 +1246,18 @@ func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) {
 		if match.Path != nil {
 			switch ptr.Deref(match.Path.Type, gwapiv1.PathMatchPathPrefix) {
 			case gwapiv1.PathMatchPathPrefix:
-				irRule.PathMatch = &ir.StringMatch{
-					Prefix: ptr.To(match.Path.Value),
-					Invert: match.Path.Invert,
+				if match.Path.Value == "/" {
+					irRule.PathMatch = &ir.StringMatch{
+						Prefix: ptr.To(match.Path.Value),
+						Invert: match.Path.Invert,
+					}
+				} else {
+					// envoy ratelimit HeaderMatcher doesn't support PathSeparatedPrefix like route matching,
+					// so we use regex to achieve the same path-separated prefix behavior.
+					irRule.PathMatch = &ir.StringMatch{
+						SafeRegex: ptr.To(regex.PathSeparatedPrefixRegex(match.Path.Value)),
+						Invert:    match.Path.Invert,
+					}
 				}
 			case gwapiv1.PathMatchExact:
 				irRule.PathMatch = &ir.StringMatch{
diff --git a/internal/gatewayapi/contexts.go b/internal/gatewayapi/contexts.go
index 0dd197cce..8ee05a241 100644
--- a/internal/gatewayapi/contexts.go
+++ b/internal/gatewayapi/contexts.go
@@ -44,8 +44,6 @@ func (g *GatewayContext) ResetListeners(resources *resource.Resources, epMap map
 			listenerStatusIdx: i,
 		}
 	}
-
-	g.attachEnvoyProxy(resources, epMap)
 }
 
 func (g *GatewayContext) attachEnvoyProxy(resources *resource.Resources, epMap map[types.NamespacedName]*egv1a1.EnvoyProxy) {
diff --git a/internal/gatewayapi/tls.go b/internal/gatewayapi/tls.go
index 591f245c2..92ffff1af 100644
--- a/internal/gatewayapi/tls.go
+++ b/internal/gatewayapi/tls.go
@@ -129,7 +129,7 @@ func validateCrl(data []byte) error {
 		return fmt.Errorf("failed to parse CRL: %w", err)
 	}
 	now := time.Now()
-	if now.After(crl.NextUpdate) {
+	if !crl.NextUpdate.IsZero() && now.After(crl.NextUpdate) {
 		return fmt.Errorf("CRL is expired (next update was due at %v)", crl.NextUpdate)
 	}
 	if now.Before(crl.ThisUpdate) {
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index 1a38a6ff2..e6bb75d35 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -386,6 +386,7 @@ func (t *Translator) GetRelevantGateways(resources *resource.Resources) (
 		gCtx := &GatewayContext{
 			Gateway: gateway,
 		}
+		gCtx.attachEnvoyProxy(resources, envoyproxyMap)
 
 		// Gateways that are not accepted by the controller because they reference an invalid EnvoyProxy.
 		if status.GatewayNotAccepted(gCtx.Gateway) {
@@ -394,7 +395,6 @@ func (t *Translator) GetRelevantGateways(resources *resource.Resources) (
 			continue
 		}
 
-		gCtx.ResetListeners(resources, envoyproxyMap)
 		if ep := gCtx.envoyProxy; ep != nil {
 			key := utils.NamespacedName(ep)
 			if err, exits := envoyproxyValidationErrorMap[key]; exits {
@@ -406,6 +406,8 @@ func (t *Translator) GetRelevantGateways(resources *resource.Resources) (
 			}
 		}
 
+		// we cannot do this early, otherwise there's an error when updating status.
+		gCtx.ResetListeners(resources, envoyproxyMap)
 		acceptedGateways = append(acceptedGateways, gCtx)
 	}
 	return
diff --git a/internal/infrastructure/runner/runner.go b/internal/infrastructure/runner/runner.go
index 1c5d1f1e6..07a9de815 100644
--- a/internal/infrastructure/runner/runner.go
+++ b/internal/infrastructure/runner/runner.go
@@ -106,7 +106,7 @@ func (r *Runner) updateProxyInfraFromSubscription(ctx context.Context, sub <-cha
 	// Subscribe to resources
 	message.HandleSubscription(message.Metadata{Runner: r.Name(), Message: message.InfraIRMessageName}, sub,
 		func(update message.Update[string, *ir.Infra], errChan chan error) {
-			r.Logger.Info("received an update")
+			r.Logger.Info("received an update", "key", update.Key, "delete", update.Delete)
 			val := update.Value
 
 			if update.Delete {
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index b471261f1..fac9d8d8b 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -1605,11 +1605,7 @@ func (r *gatewayAPIReconciler) processServiceClusterForGatewayClass(ep *egv1a1.E
 		}
 	}
 
-	resourceMap.insertBackendRef(gwapiv1.BackendObjectReference{
-		Kind:      ptr.To(gwapiv1.Kind("Service")),
-		Namespace: gatewayapi.NamespacePtr(proxySvcNamespace),
-		Name:      gwapiv1.ObjectName(proxySvcName),
-	})
+	r.insertProxyServiceIfExists(proxySvcName, proxySvcNamespace, resourceMap)
 }
 
 // Called on a Gateway when merged gateways mode is not enabled for its parent GatewayClass.
@@ -1634,10 +1630,24 @@ func (r *gatewayAPIReconciler) processServiceClusterForGateway(ep *egv1a1.EnvoyP
 		}
 	}
 
+	r.insertProxyServiceIfExists(proxySvcName, proxySvcNamespace, resourceMap)
+}
+
+func (r *gatewayAPIReconciler) insertProxyServiceIfExists(name, namespace string, resourceMap *resourceMappings) {
+	svcNN := types.NamespacedName{Name: name, Namespace: namespace}
+	svc := new(corev1.Service)
+	err := r.client.Get(context.Background(), svcNN, svc)
+	// Only insert if service exists
+	if err != nil {
+		if !kerrors.IsNotFound(err) {
+			r.log.Error(err, "failed to get proxy service", "namespace", namespace, "name", name)
+		}
+		return
+	}
 	resourceMap.insertBackendRef(gwapiv1.BackendObjectReference{
-		Kind:      ptr.To(gwapiv1.Kind("Service")),
-		Namespace: gatewayapi.NamespacePtr(proxySvcNamespace),
-		Name:      gwapiv1.ObjectName(proxySvcName),
+		Kind:      ptr.To(gwapiv1.Kind(resource.KindService)),
+		Namespace: gatewayapi.NamespacePtr(svc.Namespace),
+		Name:      gwapiv1.ObjectName(svc.Name),
 	})
 }
 
diff --git a/internal/utils/regex/regex.go b/internal/utils/regex/regex.go
index e4c686a6e..a3c6de269 100644
--- a/internal/utils/regex/regex.go
+++ b/internal/utils/regex/regex.go
@@ -8,6 +8,7 @@ package regex
 import (
 	"fmt"
 	"regexp"
+	"strings"
 )
 
 // Validate validates a regex string.
@@ -17,3 +18,19 @@ func Validate(regex string) error {
 	}
 	return nil
 }
+
+// PathSeparatedPrefixRegex creates a regex pattern that Envoy's PathSeparatedPrefix behavior.
+// The pattern matches paths that either exactly match the prefix or have the prefix followed by "/".
+// This ensures proper path separation (e.g., "/api" matches "/api" and "/api/v1" but not "/apiv1").
+//
+// References:
+// - Envoy 'path_separated_prefix' : https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-routematch
+func PathSeparatedPrefixRegex(prefix string) string {
+	// Remove trailing slash
+	trimmedPrefix := strings.TrimSuffix(prefix, "/")
+
+	// Escape special regex characters in the prefix
+	escapedPrefix := regexp.QuoteMeta(trimmedPrefix)
+
+	return "^" + escapedPrefix + "(/.*|\\?.*|#.*|;.*|$)"
+}
diff --git a/internal/xds/translator/jwt.go b/internal/xds/translator/jwt.go
index 423e31f1d..6a61eaad0 100644
--- a/internal/xds/translator/jwt.go
+++ b/internal/xds/translator/jwt.go
@@ -53,31 +53,39 @@ func (*jwt) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListen
 		return nil
 	}
 
-	// Return early if filter already exists.
-	for _, httpFilter := range mgr.HttpFilters {
+	var jwtAuthn jwtauthnv3.JwtAuthentication
+	jwtAuthnFilterIndex := -1
+	// Add providers to existing JwtAuthentication if present.
+	for index, httpFilter := range mgr.HttpFilters {
 		if httpFilter.Name == egv1a1.EnvoyFilterJWTAuthn.String() {
-			return nil
+			jwtAuthnFilterIndex = index
+			if err := httpFilter.GetTypedConfig().UnmarshalTo(&jwtAuthn); err != nil {
+				return err
+			}
 		}
 	}
 
-	jwtFilter, err := buildHCMJWTFilter(irListener)
+	if err := buildJWTAuthn(irListener, &jwtAuthn); err != nil {
+		return err
+	}
+
+	jwtFilter, err := buildHCMJWTFilter(&jwtAuthn)
 	if err != nil {
 		return err
 	}
 
-	mgr.HttpFilters = append([]*hcmv3.HttpFilter{jwtFilter}, mgr.HttpFilters...)
+	if exist := jwtAuthnFilterIndex != -1; exist {
+		mgr.HttpFilters[jwtAuthnFilterIndex] = jwtFilter
+	} else {
+		mgr.HttpFilters = append([]*hcmv3.HttpFilter{jwtFilter}, mgr.HttpFilters...)
+	}
 
 	return nil
 }
 
 // buildHCMJWTFilter returns a JWT authn HTTP filter from the provided IR listener.
-func buildHCMJWTFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) {
-	jwtAuthnProto, err := buildJWTAuthn(irListener)
-	if err != nil {
-		return nil, err
-	}
-
-	jwtAuthnAny, err := proto.ToAnyWithValidation(jwtAuthnProto)
+func buildHCMJWTFilter(jwtAuthn *jwtauthnv3.JwtAuthentication) (*hcmv3.HttpFilter, error) {
+	jwtAuthnAny, err := proto.ToAnyWithValidation(jwtAuthn)
 	if err != nil {
 		return nil, err
 	}
@@ -91,15 +99,19 @@ func buildHCMJWTFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) {
 }
 
 // buildJWTAuthn returns a JwtAuthentication based on the provided IR HTTPListener.
-func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, error) {
-	jwtProviders := make(map[string]*jwtauthnv3.JwtProvider)
-	reqMap := make(map[string]*jwtauthnv3.JwtRequirement)
-
+func buildJWTAuthn(irListener *ir.HTTPListener, jwtAuthn *jwtauthnv3.JwtAuthentication) error {
 	for _, route := range irListener.Routes {
 		if route == nil || !routeContainsJWTAuthn(route) {
 			continue
 		}
 
+		if jwtAuthn.Providers == nil {
+			jwtAuthn.Providers = make(map[string]*jwtauthnv3.JwtProvider, len(route.Security.JWT.Providers))
+		}
+		if jwtAuthn.RequirementMap == nil {
+			jwtAuthn.RequirementMap = make(map[string]*jwtauthnv3.JwtRequirement, len(route.Security.JWT.Providers))
+		}
+
 		var reqs []*jwtauthnv3.JwtRequirement
 		for i := range route.Security.JWT.Providers {
 			var (
@@ -144,7 +156,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
 				} else {
 					var cluster *urlCluster
 					if cluster, err = url2Cluster(jwks.URI); err != nil {
-						return nil, err
+						return err
 					}
 					jwksCluster = cluster.name
 				}
@@ -169,7 +181,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
 				if jwks.Traffic != nil && jwks.Traffic.Retry != nil {
 					var rp *corev3.RetryPolicy
 					if rp, err = buildNonRouteRetryPolicy(jwks.Traffic.Retry); err != nil {
-						return nil, err
+						return err
 					}
 					remote.RemoteJwks.RetryPolicy = rp
 				}
@@ -187,7 +199,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
 			}
 
 			providerKey := fmt.Sprintf("%s/%s", route.Name, irProvider.Name)
-			jwtProviders[providerKey] = jwtProvider
+			jwtAuthn.Providers[providerKey] = jwtProvider
 			reqs = append(reqs, &jwtauthnv3.JwtRequirement{
 				RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{
 					ProviderName: providerKey,
@@ -204,7 +216,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
 		}
 
 		if len(reqs) == 1 {
-			reqMap[route.Name] = reqs[0]
+			jwtAuthn.RequirementMap[route.Name] = reqs[0]
 		} else {
 			orListReqs := &jwtauthnv3.JwtRequirement{
 				RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{
@@ -213,14 +225,10 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
 					},
 				},
 			}
-			reqMap[route.Name] = orListReqs
+			jwtAuthn.RequirementMap[route.Name] = orListReqs
 		}
 	}
-
-	return &jwtauthnv3.JwtAuthentication{
-		RequirementMap: reqMap,
-		Providers:      jwtProviders,
-	}, nil
+	return nil
 }
 
 // buildXdsUpstreamTLSSocket returns an xDS TransportSocket that uses envoyTrustBundle
diff --git a/internal/xds/translator/local_ratelimit.go b/internal/xds/translator/local_ratelimit.go
index 369712e20..eaaf6e89e 100644
--- a/internal/xds/translator/local_ratelimit.go
+++ b/internal/xds/translator/local_ratelimit.go
@@ -136,7 +136,6 @@ func (*localRateLimit) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, h
 	local := irRoute.Traffic.RateLimit.Local
 
 	rateLimits, descriptors := buildRouteLocalRateLimits(local)
-	routeAction.RateLimits = rateLimits
 
 	filterCfg := route.GetTypedPerFilterConfig()
 	if _, ok := filterCfg[egv1a1.EnvoyFilterLocalRateLimit.String()]; ok {
@@ -177,6 +176,7 @@ func (*localRateLimit) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute, h
 		AlwaysConsumeDefaultTokenBucket: &wrapperspb.BoolValue{
 			Value: false,
 		},
+		RateLimits: rateLimits,
 	}
 	if httpListener.Headers != nil && httpListener.Headers.DisableRateLimitHeaders {
 		localRl.EnableXRatelimitHeaders = rlv3.XRateLimitHeadersRFCVersion_OFF
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index a38cd5f78..32c964f90 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -8,10 +8,14 @@ security updates: |
 
 # New features or capabilities added in this release.
 new features: |
+ Added support for both Global and Local rate limiting in BackendTrafficPolicy simultaneously.
  Added support for applying SecurityPolicy Authorization to TCPRoute (client IP / allow-deny list for TCP traffic).
+
 bug fixes: |
  - Fixed Listener port limit typo 65353 -> 65535.
  - Fixed issue where reloading invalid envoy gateway configuration.
+ - Fixed missing JWT provider configuration when JWT authentication is configured on multiple HTTP listeners sharing the same port.
+
 # Enhancements that improve performance.
 performance improvements: |
 



Test patch:
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.in.yaml
new file mode 100644
index 000000000..b7f7255b8
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.in.yaml
@@ -0,0 +1,62 @@
+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
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-grcp-route
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+    rateLimit:
+      global:
+        rules:
+        - clientSelectors:
+          - sourceCIDR:
+              type: "Distinct"
+              value: 192.168.0.0/16
+          limit:
+            requests: 20
+            unit: Hour
+      local:
+        rules:
+        - clientSelectors:
+          - headers:
+            - name: x-user-id
+              value: one
+            - name: x-org-id
+              value: foo
+          limit:
+            requests: 10
+            unit: Hour
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.out.yaml
new file mode 100644
index 000000000..27d7f6f7c
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit-both-type.out.yaml
@@ -0,0 +1,246 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-grcp-route
+    namespace: default
+  spec:
+    rateLimit:
+      global:
+        rules:
+        - clientSelectors:
+          - sourceCIDR:
+              type: Distinct
+              value: 192.168.0.0/16
+          limit:
+            requests: 20
+            unit: Hour
+      local:
+        rules:
+        - clientSelectors:
+          - headers:
+            - name: x-user-id
+              value: one
+            - name: x-org-id
+              value: foo
+          limit:
+            requests: 10
+            unit: Hour
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+  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
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+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
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        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
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      envoyClientCertificate:
+        certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+        name: envoy-gateway-system/envoy
+        privateKey: '[redacted]'
+      proxyServiceCluster:
+        metadata:
+          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:
+            name: envoy-envoy-gateway-gateway-1-196ae069
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-1
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 80
+      hostnames:
+      - '*'
+      isHTTP2: true
+      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: GRPCRoute
+            name: grpcroute-1
+            namespace: default
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: grpcroute/default/grpcroute-1/rule/0/backend/0
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+        traffic:
+          rateLimit:
+            global:
+              rules:
+              - cidrMatch:
+                  cidr: 192.168.0.0/16
+                  distinct: true
+                  ip: 192.168.0.0
+                  isIPv6: false
+                  maskLen: 16
+                headerMatches: []
+                limit:
+                  requests: 20
+                  unit: Hour
+                name: default/policy-for-grcp-route/rule/0
+            local:
+              default:
+                requests: 4294967295
+                unit: Second
+              rules:
+              - headerMatches:
+                - distinct: false
+                  exact: one
+                  name: x-user-id
+                - distinct: false
+                  exact: foo
+                  name: x-org-id
+                limit:
+                  requests: 10
+                  unit: Hour
+                name: default/policy-for-grcp-route/rule/0
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
index 74cae0fc6..ca70dbd11 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
@@ -114,6 +114,9 @@ backendTrafficPolicies:
             - name: x-org-id
               value: admin
               invert: true
+            path:
+              type: PathPrefix
+              value: "/user"
           limit:
             requests: 10
             unit: Hour
@@ -135,6 +138,9 @@ backendTrafficPolicies:
           - sourceCIDR:
               type: "Distinct"
               value: 192.168.0.0/16
+            path:
+              type: PathPrefix
+              value: "/"
           limit:
             requests: 20
             unit: Hour
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
index ca29c7923..b261a0dfd 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
@@ -9,7 +9,10 @@ backendTrafficPolicies:
       global:
         rules:
         - clientSelectors:
-          - sourceCIDR:
+          - path:
+              type: PathPrefix
+              value: /
+            sourceCIDR:
               type: Distinct
               value: 192.168.0.0/16
           cost:
@@ -101,6 +104,9 @@ backendTrafficPolicies:
             - invert: true
               name: x-org-id
               value: admin
+            path:
+              type: PathPrefix
+              value: /user
           limit:
             requests: 10
             unit: Hour
@@ -490,6 +496,10 @@ xdsIR:
                   requests: 10
                   unit: Hour
                 name: envoy-gateway/policy-for-gateway/rule/0
+                pathMatch:
+                  distinct: false
+                  name: ""
+                  safeRegex: ^/user(/.*|\?.*|#.*|;.*|$)
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
@@ -583,6 +593,10 @@ xdsIR:
                   requests: 20
                   unit: Hour
                 name: default/policy-for-route/rule/0
+                pathMatch:
+                  distinct: false
+                  name: ""
+                  prefix: /
                 requestCost:
                   number: 1
                 responseCost:
diff --git a/internal/gatewayapi/testdata/envoyproxy-gateway-accesslog-with-bad-sinks.out.yaml b/internal/gatewayapi/testdata/envoyproxy-gateway-accesslog-with-bad-sinks.out.yaml
index a6bb55e49..5da07c49a 100644
--- a/internal/gatewayapi/testdata/envoyproxy-gateway-accesslog-with-bad-sinks.out.yaml
+++ b/internal/gatewayapi/testdata/envoyproxy-gateway-accesslog-with-bad-sinks.out.yaml
@@ -27,10 +27,5 @@ gateways:
       reason: InvalidParameters
       status: "False"
       type: Accepted
-    listeners:
-    - attachedRoutes: 0
-      conditions: null
-      name: http
-      supportedKinds: null
 infraIR: {}
 xdsIR: {}
diff --git a/internal/provider/kubernetes/controller_test.go b/internal/provider/kubernetes/controller_test.go
index 9a978495a..c3d29519b 100644
--- a/internal/provider/kubernetes/controller_test.go
+++ b/internal/provider/kubernetes/controller_test.go
@@ -1249,32 +1249,43 @@ func TestProcessSecurityPolicyObjectRefs(t *testing.T) {
 }
 
 func TestProcessServiceClusterForGatewayClass(t *testing.T) {
+	gcName := "merged-gc"
+	nsName := "envoy-gateway-system"
 	testCases := []struct {
 		name            string
 		gatewayClass    *gwapiv1.GatewayClass
 		envoyProxy      *egv1a1.EnvoyProxy
 		expectedSvcName string
+		serviceCluster  []client.Object
 	}{
 		{
 			name: "when merged gateways and no hardcoded svc name is used",
 			gatewayClass: &gwapiv1.GatewayClass{
 				ObjectMeta: metav1.ObjectMeta{
-					Name: "merged-gc",
+					Name: gcName,
 				},
 			},
 			envoyProxy:      nil,
-			expectedSvcName: proxy.ExpectedResourceHashedName("merged-gc"),
+			expectedSvcName: proxy.ExpectedResourceHashedName(gcName),
+			serviceCluster: []client.Object{
+				&corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      proxy.ExpectedResourceHashedName(gcName),
+						Namespace: nsName,
+					},
+				},
+			},
 		},
 		{
 			name: "when merged gateways and a hardcoded svc name is used",
 			gatewayClass: &gwapiv1.GatewayClass{
 				ObjectMeta: metav1.ObjectMeta{
-					Name: "merged-gc",
+					Name: gcName,
 				},
 			},
 			envoyProxy: &egv1a1.EnvoyProxy{
 				ObjectMeta: metav1.ObjectMeta{
-					Name: "merged-gc",
+					Name: gcName,
 				},
 				Spec: egv1a1.EnvoyProxySpec{
 					Provider: &egv1a1.EnvoyProxyProvider{
@@ -1288,6 +1299,25 @@ func TestProcessServiceClusterForGatewayClass(t *testing.T) {
 				},
 			},
 			expectedSvcName: "merged-gc-svc",
+			serviceCluster: []client.Object{
+				&corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "merged-gc-svc",
+						Namespace: nsName,
+					},
+				},
+			},
+		},
+		{
+			name: "non-existent proxy service",
+			gatewayClass: &gwapiv1.GatewayClass{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: gcName,
+				},
+			},
+			envoyProxy:      nil,
+			expectedSvcName: proxy.ExpectedResourceHashedName(gcName),
+			serviceCluster:  nil,
 		},
 	}
 
@@ -1299,6 +1329,10 @@ func TestProcessServiceClusterForGatewayClass(t *testing.T) {
 			resourceMap := newResourceMapping()
 
 			r := newGatewayAPIReconciler(logger)
+			r.client = fakeclient.NewClientBuilder().
+				WithScheme(envoygateway.GetScheme()).
+				WithObjects(tc.serviceCluster...).
+				Build()
 			r.namespace = "envoy-gateway-system"
 
 			r.processServiceClusterForGatewayClass(tc.envoyProxy, tc.gatewayClass, resourceMap)
@@ -1309,8 +1343,12 @@ func TestProcessServiceClusterForGatewayClass(t *testing.T) {
 				Name:      gwapiv1.ObjectName(tc.expectedSvcName),
 			}
 			key := backendRefKey(&expectedRef)
-			require.Contains(t, resourceMap.allAssociatedBackendRefs, key)
-			require.Equal(t, expectedRef, resourceMap.allAssociatedBackendRefs[key])
+			if tc.serviceCluster != nil {
+				require.Contains(t, resourceMap.allAssociatedBackendRefs, key)
+				require.Equal(t, expectedRef, resourceMap.allAssociatedBackendRefs[key])
+			} else {
+				require.NotContains(t, resourceMap.allAssociatedBackendRefs, key)
+			}
 		})
 	}
 }
@@ -1324,6 +1362,7 @@ func TestProcessServiceClusterForGateway(t *testing.T) {
 		gatewayNamespacedMode  bool
 		expectedSvcName        string
 		expectedSvcNamespace   string
+		serviceCluster         []client.Object
 	}{
 		{
 			name: "no gateway namespaced mode with no hardcoded service name",
@@ -1377,6 +1416,14 @@ func TestProcessServiceClusterForGateway(t *testing.T) {
 			gatewayNamespacedMode: true,
 			expectedSvcName:       "my-gateway",
 			expectedSvcNamespace:  "app-namespace",
+			serviceCluster: []client.Object{
+				&corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-gateway",
+						Namespace: "app-namespace",
+					},
+				},
+			},
 		},
 		{
 			name: "gateway namespaced mode with hardcoded service name",
@@ -1404,6 +1451,14 @@ func TestProcessServiceClusterForGateway(t *testing.T) {
 			gatewayNamespacedMode: true,
 			expectedSvcName:       "my-gateway-svc",
 			expectedSvcNamespace:  "app-namespace",
+			serviceCluster: []client.Object{
+				&corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-gateway-svc",
+						Namespace: "app-namespace",
+					},
+				},
+			},
 		},
 		{
 			name: "no gateway namespaced mode with no hardcoded service name attached gatewayclass",
@@ -1443,6 +1498,10 @@ func TestProcessServiceClusterForGateway(t *testing.T) {
 			resourceMap := newResourceMapping()
 
 			r := newGatewayAPIReconciler(logger)
+			r.client = fakeclient.NewClientBuilder().
+				WithScheme(envoygateway.GetScheme()).
+				WithObjects(tc.serviceCluster...).
+				Build()
 			r.namespace = "envoy-gateway-system"
 			r.gatewayNamespaceMode = tc.gatewayNamespacedMode
 
@@ -1466,8 +1525,12 @@ func TestProcessServiceClusterForGateway(t *testing.T) {
 				Name:      gwapiv1.ObjectName(tc.expectedSvcName),
 			}
 			key := backendRefKey(&expectedRef)
-			require.Contains(t, resourceMap.allAssociatedBackendRefs, key)
-			require.Equal(t, expectedRef, resourceMap.allAssociatedBackendRefs[key])
+			if tc.serviceCluster != nil {
+				require.Contains(t, resourceMap.allAssociatedBackendRefs, key)
+				require.Equal(t, expectedRef, resourceMap.allAssociatedBackendRefs[key])
+			} else {
+				require.NotContains(t, resourceMap.allAssociatedBackendRefs, key)
+			}
 		})
 	}
 }
diff --git a/internal/utils/regex/regex_test.go b/internal/utils/regex/regex_test.go
index b3dfaf4c4..351a13ff5 100644
--- a/internal/utils/regex/regex_test.go
+++ b/internal/utils/regex/regex_test.go
@@ -5,7 +5,10 @@
 
 package regex
 
-import "testing"
+import (
+	"regexp"
+	"testing"
+)
 
 func TestValidate(t *testing.T) {
 	tests := []struct {
@@ -32,3 +35,129 @@ func TestValidate(t *testing.T) {
 		})
 	}
 }
+
+func TestPathSeparatedPrefixRegex(t *testing.T) {
+	tests := []struct {
+		name     string
+		prefix   string
+		testPath string
+		want     bool
+	}{
+		// Tests for prefix "/api/v1" - what should match
+		{
+			name:     "exact match",
+			prefix:   "/api/v1",
+			testPath: "/api/v1",
+			want:     true,
+		},
+		{
+			name:     "with trailing slash",
+			prefix:   "/api/v1",
+			testPath: "/api/v1/",
+			want:     true,
+		},
+		{
+			name:     "with sub-path",
+			prefix:   "/api/v1",
+			testPath: "/api/v1/users",
+			want:     true,
+		},
+		{
+			name:     "with deep sub-path",
+			prefix:   "/api/v1",
+			testPath: "/api/v1/users/123/profile",
+			want:     true,
+		},
+		{
+			name:     "with query params",
+			prefix:   "/api/v1",
+			testPath: "/api/v1?version=latest",
+			want:     true,
+		},
+		{
+			name:     "with complex query",
+			prefix:   "/api/v1",
+			testPath: "/api/v1?param1=value1&param2=value2",
+			want:     true,
+		},
+		{
+			name:     "with fragment",
+			prefix:   "/api/v1",
+			testPath: "/api/v1#section",
+			want:     true,
+		},
+		{
+			name:     "with semicolon parameter",
+			prefix:   "/api/v1",
+			testPath: "/api/v1;sessionid=123",
+			want:     true,
+		},
+		{
+			name:     "with semicolon and sub-path",
+			prefix:   "/api/v1",
+			testPath: "/api/v1;sessionid=123/profile",
+			want:     true,
+		},
+
+		// Tests for prefix "/api/v1" - what should NOT match
+		{
+			name:     "alphanumeric continuation",
+			prefix:   "/api/v1",
+			testPath: "/api/v1abc",
+			want:     false,
+		},
+		{
+			name:     "underscore continuation",
+			prefix:   "/api/v1",
+			testPath: "/api/v1_test",
+			want:     false,
+		},
+		{
+			name:     "dash continuation",
+			prefix:   "/api/v1",
+			testPath: "/api/v1-beta",
+			want:     false,
+		},
+		{
+			name:     "dot continuation",
+			prefix:   "/api/v1",
+			testPath: "/api/v1.1",
+			want:     false,
+		},
+		{
+			name:     "different path completely",
+			prefix:   "/api/v1",
+			testPath: "/api/v2",
+			want:     false,
+		},
+		{
+			name:     "prefix longer than path",
+			prefix:   "/api/v1",
+			testPath: "/api",
+			want:     false,
+		},
+		{
+			name:     "similar but different",
+			prefix:   "/api/v1",
+			testPath: "/api/v10",
+			want:     false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			pattern := PathSeparatedPrefixRegex(tt.prefix)
+
+			regex, err := regexp.Compile(pattern)
+			if err != nil {
+				t.Fatalf("Failed to compile regex pattern %q: %v", pattern, err)
+			}
+
+			got := regex.MatchString(tt.testPath)
+			if got != tt.want {
+				t.Errorf("PathSeparatedPrefixRegex(%q).MatchString(%q) = %v, want %v (pattern: %q)",
+					tt.prefix, tt.testPath, got, tt.want, pattern)
+			}
+		})
+	}
+}
diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-from-multiple-listeners.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-from-multiple-listeners.yaml
new file mode 100644
index 000000000..2edc562af
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/jwt-from-multiple-listeners.yaml
@@ -0,0 +1,121 @@
+# This file tests JWT configuration from multiple HTTP listeners sharing the same port won't overlap.
+http:
+  - address: 0.0.0.0
+    externalPort: 80
+    hostnames:
+      - domain1.example.com
+    isHTTP2: false
+    metadata:
+      kind: Gateway
+      name: external-gateway
+      namespace: envoy-gateway-system
+      sectionName: domain1-example-com-http
+    name: envoy-gateway-system/external-gateway/domain1-example-com-http
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    port: 10080
+    routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: domain1
+            namespace: ns1
+          name: httproute/ns1/domain1/rule/0
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 7.7.7.7
+                  port: 80
+              metadata:
+                kind: Service
+                name: app1
+                namespace: ns1
+                sectionName: "80"
+              name: httproute/ns1/domain1/rule/0/backend/0
+              protocol: HTTP
+              weight: 1
+        hostname: domain1.example.com
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: domain1
+          namespace: ns1
+        name: httproute/ns1/domain1/rule/0/match/0/domain1_example_com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        security:
+          jwt:
+            allowMissing: true
+            providers:
+              - extractFrom:
+                  cookies:
+                    - AccessTokenDomain1
+                issuer: https://accounts.google.com
+                name: jwt1
+                remoteJWKS:
+                  uri: https://www.googleapis.com/oauth2/v3/certs
+  - address: 0.0.0.0
+    externalPort: 80
+    hostnames:
+      - domain2.example.com
+    isHTTP2: false
+    metadata:
+      kind: Gateway
+      name: external-gateway
+      namespace: envoy-gateway-system
+      sectionName: domain2-example-com-http
+    name: envoy-gateway-system/external-gateway/domain2-example-com-http
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    port: 10080
+    routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: domain2
+            namespace: ns2
+          name: httproute/ns2/domain2/rule/0
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 9.9.9.9
+                  port: 80
+              metadata:
+                kind: Service
+                name: app2
+                namespace: ns2
+                sectionName: "80"
+              name: httproute/ns2/domain2/rule/0/backend/0
+              protocol: HTTP
+              weight: 1
+        hostname: domain2.example.com
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: domain2
+          namespace: ns2
+        name: httproute/ns2/domain2/rule/0/match/0/domain2_example_com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        security:
+          jwt:
+            allowMissing: true
+            providers:
+              - extractFrom:
+                  cookies:
+                    - AccessTokenDomain2
+                issuer: https://accounts.google.com
+                name: jwt2
+                remoteJWKS:
+                  uri: https://www.googleapis.com/oauth2/v3/certs
+readyListener:
+  address: 0.0.0.0
+  ipFamily: IPv4
+  path: /ready
+  port: 19003
diff --git a/internal/xds/translator/testdata/in/xds-ir/ratelimit-both-type.yaml b/internal/xds/translator/testdata/in/xds-ir/ratelimit-both-type.yaml
new file mode 100644
index 000000000..47c75eaae
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit-both-type.yaml
@@ -0,0 +1,68 @@
+globalResources:
+  envoyClientCertificate:
+    name: envoy-gateway-system/envoy
+    privateKey: [107, 101, 121, 45, 100, 97, 116, 97]
+    certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97]
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route"
+    hostname: "*"
+    traffic:
+      rateLimit:
+        global:
+          rules:
+          - headerMatches:
+            - name: "x-user-id"
+              exact: "one"
+            limit:
+              requests: 5
+              unit: second
+    pathMatch:
+      exact: "foo/bar"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+        name: "first-route-dest/backend/0"
+  - name: "second-route"
+    hostname: "*"
+    traffic:
+      rateLimit:
+        global:
+          rules:
+          - headerMatches:
+            - name: "x-user-id"
+              distinct: true
+            limit:
+              requests: 5
+              unit: second
+        local:
+          default:
+            requests: 10
+            unit: Minute
+          rules:
+          - headerMatches:
+            - name: x-user-id
+              exact: one
+            limit:
+              requests: 10
+              unit: Hour
+    pathMatch:
+      exact: "example"
+    destination:
+      name: "second-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+        name: "second-route-dest/backend/0"
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.clusters.yaml
new file mode 100644
index 000000000..578e27762
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.clusters.yaml
@@ -0,0 +1,105 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/ns1/domain1/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  metadata:
+    filterMetadata:
+      envoy-gateway:
+        resources:
+        - kind: HTTPRoute
+          name: domain1
+          namespace: ns1
+  name: httproute/ns1/domain1/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  dnsRefreshRate: 30s
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadAssignment:
+    clusterName: www_googleapis_com_443
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: www.googleapis.com
+              portValue: 443
+        loadBalancingWeight: 1
+      loadBalancingWeight: 1
+      locality:
+        region: www_googleapis_com_443/backend/-1
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: www_googleapis_com_443
+  perConnectionBufferLimitBytes: 32768
+  respectDnsTtl: true
+  transportSocket:
+    name: envoy.transport_sockets.tls
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+      commonTlsContext:
+        validationContext:
+          trustedCa:
+            filename: /etc/ssl/certs/ca-certificates.crt
+      sni: www.googleapis.com
+  type: STRICT_DNS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/ns2/domain2/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  metadata:
+    filterMetadata:
+      envoy-gateway:
+        resources:
+        - kind: HTTPRoute
+          name: domain2
+          namespace: ns2
+  name: httproute/ns2/domain2/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.endpoints.yaml
new file mode 100644
index 000000000..e3eba9863
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.endpoints.yaml
@@ -0,0 +1,40 @@
+- clusterName: httproute/ns1/domain1/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 80
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/ns1/domain1/rule/0/backend/0
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Service
+            name: app1
+            namespace: ns1
+            sectionName: "80"
+- clusterName: httproute/ns2/domain2/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 9.9.9.9
+            portValue: 80
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/ns2/domain2/rule/0/backend/0
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Service
+            name: app2
+            namespace: ns2
+            sectionName: "80"
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.listeners.yaml
new file mode 100644
index 000000000..7cf5e6201
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.listeners.yaml
@@ -0,0 +1,116 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 19003
+  bypassOverloadManager: true
+  filterChains:
+  - filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        httpFilters:
+        - name: envoy.filters.http.health_check
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck
+            headers:
+            - name: :path
+              stringMatch:
+                exact: /ready
+            passThroughMode: false
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        routeConfig:
+          name: ready_route
+          virtualHosts:
+          - domains:
+            - '*'
+            name: ready_route
+            routes:
+            - directResponse:
+                status: 500
+              match:
+                prefix: /
+        statPrefix: eg-ready-http
+  name: envoy-gateway-proxy-ready-0.0.0.0-19003
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.jwt_authn
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
+            providers:
+              httproute/ns1/domain1/rule/0/match/0/domain1_example_com/jwt1:
+                forward: true
+                fromCookies:
+                - AccessTokenDomain1
+                issuer: https://accounts.google.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: jwt1
+                remoteJwks:
+                  asyncFetch: {}
+                  httpUri:
+                    cluster: www_googleapis_com_443
+                    timeout: 10s
+                    uri: https://www.googleapis.com/oauth2/v3/certs
+              httproute/ns2/domain2/rule/0/match/0/domain2_example_com/jwt2:
+                forward: true
+                fromCookies:
+                - AccessTokenDomain2
+                issuer: https://accounts.google.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: jwt2
+                remoteJwks:
+                  asyncFetch: {}
+                  httpUri:
+                    cluster: www_googleapis_com_443
+                    timeout: 10s
+                    uri: https://www.googleapis.com/oauth2/v3/certs
+            requirementMap:
+              httproute/ns1/domain1/rule/0/match/0/domain1_example_com:
+                requiresAny:
+                  requirements:
+                  - providerName: httproute/ns1/domain1/rule/0/match/0/domain1_example_com/jwt1
+                  - allowMissing: {}
+              httproute/ns2/domain2/rule/0/match/0/domain2_example_com:
+                requiresAny:
+                  requirements:
+                  - providerName: httproute/ns2/domain2/rule/0/match/0/domain2_example_com/jwt2
+                  - allowMissing: {}
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: envoy-gateway-system/external-gateway/domain1-example-com-http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: envoy-gateway-system/external-gateway/domain1-example-com-http
+  maxConnectionsToAcceptPerSocketEvent: 1
+  name: envoy-gateway-system/external-gateway/domain1-example-com-http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.routes.yaml
new file mode 100644
index 000000000..e13818e70
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-from-multiple-listeners.routes.yaml
@@ -0,0 +1,63 @@
+- ignorePortInHostMatching: true
+  name: envoy-gateway-system/external-gateway/domain1-example-com-http
+  virtualHosts:
+  - domains:
+    - domain1.example.com
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: external-gateway
+            namespace: envoy-gateway-system
+            sectionName: domain1-example-com-http
+    name: envoy-gateway-system/external-gateway/domain1-example-com-http/domain1_example_com
+    routes:
+    - match:
+        prefix: /
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: domain1
+              namespace: ns1
+      name: httproute/ns1/domain1/rule/0/match/0/domain1_example_com
+      route:
+        cluster: httproute/ns1/domain1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.jwt_authn:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig
+          requirementName: httproute/ns1/domain1/rule/0/match/0/domain1_example_com
+  - domains:
+    - domain2.example.com
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: external-gateway
+            namespace: envoy-gateway-system
+            sectionName: domain2-example-com-http
+    name: envoy-gateway-system/external-gateway/domain2-example-com-http/domain2_example_com
+    routes:
+    - match:
+        prefix: /
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: domain2
+              namespace: ns2
+      name: httproute/ns2/domain2/rule/0/match/0/domain2_example_com
+      route:
+        cluster: httproute/ns2/domain2/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.jwt_authn:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig
+          requirementName: httproute/ns2/domain2/rule/0/match/0/domain2_example_com
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.routes.yaml
index 65cce8400..cd84f7825 100644
--- a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.routes.yaml
@@ -10,11 +10,6 @@
       name: first-route-ratelimit-distinct-ip
       route:
         cluster: first-route-dest
-        rateLimits:
-        - actions:
-          - requestHeaders:
-              descriptorKey: rule-0-match-0
-              headerName: x-user-id
         upgradeConfigs:
         - upgradeType: websocket
       typedPerFilterConfig:
@@ -35,6 +30,11 @@
           filterEnforced:
             defaultValue:
               numerator: 100
+          rateLimits:
+          - actions:
+            - requestHeaders:
+                descriptorKey: rule-0-match-0
+                headerName: x-user-id
           statPrefix: http_local_rate_limiter
           tokenBucket:
             fillInterval: 60s
@@ -45,44 +45,6 @@
       name: second-route-ratelimit-multiple-rules
       route:
         cluster: second-route-dest
-        rateLimits:
-        - actions:
-          - headerValueMatch:
-              descriptorKey: rule-0-match-0
-              descriptorValue: rule-0-match-0
-              expectMatch: true
-              headers:
-              - name: x-user-id
-                stringMatch:
-                  exact: one
-          - headerValueMatch:
-              descriptorKey: rule-0-match-1
-              descriptorValue: rule-0-match-1
-              expectMatch: true
-              headers:
-              - name: x-org-id
-                stringMatch:
-                  exact: foo
-        - actions:
-          - headerValueMatch:
-              descriptorKey: rule-1-match-0
-              descriptorValue: rule-1-match-0
-              expectMatch: true
-              headers:
-              - name: x-user-id
-                stringMatch:
-                  exact: two
-          - headerValueMatch:
-              descriptorKey: rule-1-match-1
-              descriptorValue: rule-1-match-1
-              expectMatch: true
-              headers:
-              - name: x-org-id
-                stringMatch:
-                  exact: bar
-          - maskedRemoteAddress:
-              v4PrefixMaskLen: 16
-          - remoteAddress: {}
         upgradeConfigs:
         - upgradeType: websocket
       typedPerFilterConfig:
@@ -118,6 +80,44 @@
           filterEnforced:
             defaultValue:
               numerator: 100
+          rateLimits:
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-0-match-0
+                descriptorValue: rule-0-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: one
+            - headerValueMatch:
+                descriptorKey: rule-0-match-1
+                descriptorValue: rule-0-match-1
+                expectMatch: true
+                headers:
+                - name: x-org-id
+                  stringMatch:
+                    exact: foo
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-1-match-0
+                descriptorValue: rule-1-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: two
+            - headerValueMatch:
+                descriptorKey: rule-1-match-1
+                descriptorValue: rule-1-match-1
+                expectMatch: true
+                headers:
+                - name: x-org-id
+                  stringMatch:
+                    exact: bar
+            - maskedRemoteAddress:
+                v4PrefixMaskLen: 16
+            - remoteAddress: {}
           statPrefix: http_local_rate_limiter
           tokenBucket:
             fillInterval: 60s
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml
index 7fd497923..c898c4e4d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.routes.yaml
@@ -10,24 +10,6 @@
       name: first-route-ratelimit-single-rule
       route:
         cluster: first-route-dest
-        rateLimits:
-        - actions:
-          - headerValueMatch:
-              descriptorKey: rule-0-match-0
-              descriptorValue: rule-0-match-0
-              expectMatch: true
-              headers:
-              - name: x-user-id
-                stringMatch:
-                  exact: one
-          - headerValueMatch:
-              descriptorKey: rule-0-match-1
-              descriptorValue: rule-0-match-1
-              expectMatch: true
-              headers:
-              - name: x-org-id
-                stringMatch:
-                  exact: foo
         upgradeConfigs:
         - upgradeType: websocket
       typedPerFilterConfig:
@@ -51,6 +33,24 @@
           filterEnforced:
             defaultValue:
               numerator: 100
+          rateLimits:
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-0-match-0
+                descriptorValue: rule-0-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: one
+            - headerValueMatch:
+                descriptorKey: rule-0-match-1
+                descriptorValue: rule-0-match-1
+                expectMatch: true
+                headers:
+                - name: x-org-id
+                  stringMatch:
+                    exact: foo
           statPrefix: http_local_rate_limiter
           tokenBucket:
             fillInterval: 60s
@@ -61,43 +61,6 @@
       name: second-route-ratelimit-multiple-rules
       route:
         cluster: second-route-dest
-        rateLimits:
-        - actions:
-          - headerValueMatch:
-              descriptorKey: rule-0-match-0
-              descriptorValue: rule-0-match-0
-              expectMatch: true
-              headers:
-              - name: x-user-id
-                stringMatch:
-                  exact: one
-          - headerValueMatch:
-              descriptorKey: rule-0-match-1
-              descriptorValue: rule-0-match-1
-              expectMatch: true
-              headers:
-              - name: x-org-id
-                stringMatch:
-                  exact: foo
-        - actions:
-          - headerValueMatch:
-              descriptorKey: rule-1-match-0
-              descriptorValue: rule-1-match-0
-              expectMatch: true
-              headers:
-              - name: x-user-id
-                stringMatch:
-                  exact: two
-          - headerValueMatch:
-              descriptorKey: rule-1-match-1
-              descriptorValue: rule-1-match-1
-              expectMatch: true
-              headers:
-              - name: x-org-id
-                stringMatch:
-                  exact: bar
-          - maskedRemoteAddress:
-              v4PrefixMaskLen: 16
         upgradeConfigs:
         - upgradeType: websocket
       typedPerFilterConfig:
@@ -132,6 +95,43 @@
           filterEnforced:
             defaultValue:
               numerator: 100
+          rateLimits:
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-0-match-0
+                descriptorValue: rule-0-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: one
+            - headerValueMatch:
+                descriptorKey: rule-0-match-1
+                descriptorValue: rule-0-match-1
+                expectMatch: true
+                headers:
+                - name: x-org-id
+                  stringMatch:
+                    exact: foo
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-1-match-0
+                descriptorValue: rule-1-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: two
+            - headerValueMatch:
+                descriptorKey: rule-1-match-1
+                descriptorValue: rule-1-match-1
+                expectMatch: true
+                headers:
+                - name: x-org-id
+                  stringMatch:
+                    exact: bar
+            - maskedRemoteAddress:
+                v4PrefixMaskLen: 16
           statPrefix: http_local_rate_limiter
           tokenBucket:
             fillInterval: 60s
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.clusters.yaml
new file mode 100644
index 000000000..39e632c5b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.clusters.yaml
@@ -0,0 +1,104 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: first-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: second-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: second-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  dnsRefreshRate: 30s
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadAssignment:
+    clusterName: ratelimit_cluster
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: envoy-ratelimit.envoy-gateway-system.svc.cluster.local
+              portValue: 8081
+        loadBalancingWeight: 1
+      loadBalancingWeight: 1
+      locality:
+        region: ratelimit_cluster/backend/-1
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: ratelimit_cluster
+  perConnectionBufferLimitBytes: 32768
+  respectDnsTtl: true
+  transportSocket:
+    name: envoy.transport_sockets.tls
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+      commonTlsContext:
+        tlsCertificateSdsSecretConfigs:
+        - name: envoy-gateway-system/envoy
+          sdsConfig:
+            ads: {}
+            resourceApiVersion: V3
+        tlsParams:
+          tlsMaximumProtocolVersion: TLSv1_3
+        validationContext:
+          trustedCa:
+            filename: /certs/ca.crt
+  type: STRICT_DNS
+  typedExtensionProtocolOptions:
+    envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+      '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+      explicitHttpConfig:
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.endpoints.yaml
new file mode 100644
index 000000000..de95bf555
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.endpoints.yaml
@@ -0,0 +1,24 @@
+- clusterName: first-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: first-route-dest/backend/0
+- clusterName: second-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: second-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.listeners.yaml
new file mode 100644
index 000000000..862ea444b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.listeners.yaml
@@ -0,0 +1,51 @@
+- address:
+    socketAddress:
+      address: '::'
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.local_ratelimit
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
+            maxDynamicDescriptors: 10000
+            statPrefix: http_local_rate_limiter
+        - name: envoy.filters.http.ratelimit
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
+            disableXEnvoyRatelimitedHeader: true
+            domain: first-listener
+            enableXRatelimitHeaders: DRAFT_VERSION_03
+            rateLimitService:
+              grpcService:
+                envoyGrpc:
+                  clusterName: ratelimit_cluster
+              transportApiVersion: V3
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  maxConnectionsToAcceptPerSocketEvent: 1
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.routes.yaml
new file mode 100644
index 000000000..45ad315c4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.routes.yaml
@@ -0,0 +1,76 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        path: foo/bar
+      name: first-route
+      route:
+        cluster: first-route-dest
+        rateLimits:
+        - actions:
+          - genericKey:
+              descriptorKey: first-route
+              descriptorValue: first-route
+          - headerValueMatch:
+              descriptorKey: rule-0-match-0
+              descriptorValue: rule-0-match-0
+              expectMatch: true
+              headers:
+              - name: x-user-id
+                stringMatch:
+                  exact: one
+        upgradeConfigs:
+        - upgradeType: websocket
+    - match:
+        path: example
+      name: second-route
+      route:
+        cluster: second-route-dest
+        rateLimits:
+        - actions:
+          - genericKey:
+              descriptorKey: second-route
+              descriptorValue: second-route
+          - requestHeaders:
+              descriptorKey: rule-0-match-0
+              headerName: x-user-id
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.local_ratelimit:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
+          alwaysConsumeDefaultTokenBucket: false
+          descriptors:
+          - entries:
+            - key: rule-0-match-0
+              value: rule-0-match-0
+            tokenBucket:
+              fillInterval: 3600s
+              maxTokens: 10
+              tokensPerFill: 10
+          enableXRatelimitHeaders: DRAFT_VERSION_03
+          filterEnabled:
+            defaultValue:
+              numerator: 100
+          filterEnforced:
+            defaultValue:
+              numerator: 100
+          rateLimits:
+          - actions:
+            - headerValueMatch:
+                descriptorKey: rule-0-match-0
+                descriptorValue: rule-0-match-0
+                expectMatch: true
+                headers:
+                - name: x-user-id
+                  stringMatch:
+                    exact: one
+          statPrefix: http_local_rate_limiter
+          tokenBucket:
+            fillInterval: 60s
+            maxTokens: 10
+            tokensPerFill: 10
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.secrets.yaml
new file mode 100644
index 000000000..fb0891511
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-both-type.secrets.yaml
@@ -0,0 +1,6 @@
+- name: envoy-gateway-system/envoy
+  tlsCertificate:
+    certificateChain:
+      inlineBytes: Y2VydC1kYXRh
+    privateKey:
+      inlineBytes: a2V5LWRhdGE=
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 7ab4e62ee..c7dc6811c 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -4319,7 +4319,7 @@ _Appears in:_
 
 | Field | Type | Required | Default | Description |
 | ---   | ---  | ---      | ---     | ---         |
-| `type` | _[RateLimitType](#ratelimittype)_ |  true  |  | Type decides the scope for the RateLimits.<br />Valid RateLimitType values are "Global" or "Local". |
+| `type` | _[RateLimitType](#ratelimittype)_ |  false  |  | Type decides the scope for the RateLimits.<br />Valid RateLimitType values are "Global" or "Local".<br />Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting. |
 | `global` | _[GlobalRateLimit](#globalratelimit)_ |  false  |  | Global defines global rate limit configuration. |
 | `local` | _[LocalRateLimit](#localratelimit)_ |  false  |  | Local defines local rate limit configuration. |
 
diff --git a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
index a395d3b8d..daa6fa1ab 100644
--- a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
+++ b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
@@ -61,7 +61,6 @@ spec:
       matchLabels:
         app: payment-service
   rateLimit:
-    type: Local
     local:
       requests: 10
       unit: Second
@@ -173,7 +172,6 @@ metadata:
   name: global-backendtrafficpolicy
 spec:
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -198,7 +196,6 @@ metadata:
 spec:
   mergeType: StrategicMerge  # Enables merging with gateway policy
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
diff --git a/site/content/en/latest/concepts/rate-limiting.md b/site/content/en/latest/concepts/rate-limiting.md
index 6573197b7..2b308f981 100644
--- a/site/content/en/latest/concepts/rate-limiting.md
+++ b/site/content/en/latest/concepts/rate-limiting.md
@@ -55,7 +55,6 @@ spec:
     kind: HTTPRoute
     name: my-api
   rateLimit:
-    type: Global
     global:
       rules:
       - limit:
@@ -91,7 +90,6 @@ spec:
     kind: HTTPRoute
     name: my-api
   rateLimit:
-    type: Local
     local:
       rules:
       - limit:
@@ -101,6 +99,57 @@ spec:
 ```
 This configuration limits traffic to 50 requests per minute per Envoy instance for the my-api route. If there are two Envoy replicas, up to 100 total requests per minute may be allowed (50 per replica).
 
+---
+
+## Combining Global and Local Rate Limiting
+
+Envoy Gateway supports configuring both Global and Local rate limits simultaneously on the same route. This layered approach provides comprehensive protection by combining the benefits of both strategies.
+
+**How it works:**
+
+When both rate limits are configured:
+1. **Local rate limits** is evaluated first at each Envoy instance
+2. **Global rate limits** is then evaluated using the shared rate limit service
+3. A request must pass both checks to be allowed through
+
+**Benefits of combined rate limiting:**
+
+- Local limits provide immediate protection against sudden traffic spikes at each instance without external service calls
+- Reduces load on the global rate limit service
+
+### Example
+
+```yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: combined-ratelimit
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: my-api
+  rateLimit:
+    local:
+      rules:
+      - limit:
+          requests: 50
+          unit: Minute
+    global:
+      rules:
+      - limit:
+          requests: 100
+          unit: Minute
+```
+
+This configuration applies both rate limiting strategies to the my-api route:
+- **Local limiting**: Each Envoy instance independently allows up to 50 requests per second. If you have 3 Envoy replicas, the theoretical maximum would be 150 requests per second (50 per replica) assuming perfect load distribution.
+- **Global limiting**: All requests across all Envoy instances are limited to 100 requests per minute total. This is enforced by the shared rate limit service, ensuring the combined traffic from all replicas never exceeds this limit.
+
+In practice, a request will be rejected if it exceeds either limit. For example:
+- If one Envoy instance receives 51 requests in a second, the 51st request is rejected locally (local limit exceeded)
+- If the system has already processed 100 requests in the current minute across all instances, the 101st request is rejected globally (global limit exceeded)
+
 ## Related Resources
 - [BackendTrafficPolicy](gateway_api_extensions/backend-traffic-policy.md)
 - [Task: Global Rate Limit](../tasks/traffic/global-rate-limit.md)
diff --git a/site/content/en/latest/tasks/traffic/global-rate-limit.md b/site/content/en/latest/tasks/traffic/global-rate-limit.md
index e4a295459..90ccae442 100644
--- a/site/content/en/latest/tasks/traffic/global-rate-limit.md
+++ b/site/content/en/latest/tasks/traffic/global-rate-limit.md
@@ -211,7 +211,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -240,7 +239,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -429,7 +427,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -461,7 +458,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -677,7 +673,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - limit:
@@ -702,7 +697,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - limit:
@@ -833,7 +827,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -882,7 +875,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -973,7 +965,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1022,7 +1013,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1160,7 +1150,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1208,7 +1197,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1358,7 +1346,6 @@ spec:
     kind: HTTPRoute
     name: example
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1426,7 +1413,6 @@ spec:
     kind: HTTPRoute
     name: example
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1576,7 +1562,6 @@ metadata:
   name: global-backendtrafficpolicy
 spec:
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1599,7 +1584,6 @@ metadata:
 spec:
   mergeType: StrategicMerge
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1649,7 +1633,6 @@ metadata:
   name: global-backendtrafficpolicy
 spec:
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
@@ -1672,7 +1655,6 @@ metadata:
 spec:
   mergeType: StrategicMerge
   rateLimit:
-    type: Global
     global:
       rules:
       - clientSelectors:
diff --git a/site/content/en/latest/tasks/traffic/local-rate-limit.md b/site/content/en/latest/tasks/traffic/local-rate-limit.md
index c5492b823..fdf842d7a 100644
--- a/site/content/en/latest/tasks/traffic/local-rate-limit.md
+++ b/site/content/en/latest/tasks/traffic/local-rate-limit.md
@@ -48,7 +48,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -77,7 +76,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -265,7 +263,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -297,7 +294,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -485,7 +481,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - limit:
@@ -510,7 +505,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - limit:
@@ -639,7 +633,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -668,7 +661,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -844,7 +836,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
@@ -872,7 +863,6 @@ spec:
     kind: HTTPRoute
     name: http-ratelimit
   rateLimit:
-    type: Local
     local:
       rules:
       - clientSelectors:
diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go
index 213c00620..ca95df0bf 100644
--- a/test/cel-validation/backendtrafficpolicy_test.go
+++ b/test/cel-validation/backendtrafficpolicy_test.go
@@ -1136,7 +1136,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Global: &egv1a1.GlobalRateLimit{
 							Rules: rules,
 						},
@@ -1170,7 +1169,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Global: &egv1a1.GlobalRateLimit{
 							Rules: rules,
 						},
@@ -1930,7 +1928,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Global: &egv1a1.GlobalRateLimit{
 							Rules: rules,
 						},
@@ -1953,7 +1950,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Global: &egv1a1.GlobalRateLimit{
 							Rules: []egv1a1.RateLimitRule{
 								{
@@ -1989,7 +1985,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Local: &egv1a1.LocalRateLimit{
 							Rules: []egv1a1.RateLimitRule{
 								{
@@ -2020,7 +2015,6 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 						},
 					},
 					RateLimit: &egv1a1.RateLimitSpec{
-						Type: egv1a1.GlobalRateLimitType,
 						Global: &egv1a1.GlobalRateLimit{
 							Rules: []egv1a1.RateLimitRule{
 								{
diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go
index 8d5b1cefa..12583031f 100644
--- a/test/e2e/tests/ratelimit.go
+++ b/test/e2e/tests/ratelimit.go
@@ -258,6 +258,36 @@ var RateLimitPathMatchTest = suite.ConformanceTest{
 			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
 				t.Errorf("failed to get expected response for the last (fourth) request: %v", err)
 			}
+
+			// Subpath should be rate limited due to prefix matching.
+			expectLimitResp = http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get/specific-path/subpath",
+				},
+				Response: http.Response{
+					StatusCode: 429,
+				},
+				Namespace: ns,
+			}
+			expectLimitReq = http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http")
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
+				t.Errorf("failed to get expected response for the last (fourth) request: %v", err)
+			}
+
+			// Different path (contains the path prefix) should not be rate limited.
+			expectOkResp = http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get/specific-path2",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			expectOkReq = http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+				t.Errorf("failed to get expected response for the last (fourth) request: %v", err)
+			}
 		})
 
 		t.Run("not matched path cannot got limited", func(t *testing.T) {
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 854b28329..c5e4dfd33 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -22677,12 +22677,12 @@ spec:
                     description: |-
                       Type decides the scope for the RateLimits.
                       Valid RateLimitType values are "Global" or "Local".
+
+                      Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting.
                     enum:
                     - Global
                     - Local
                     type: string
-                required:
-                - type
                 type: object
               requestBuffer:
                 description: |-
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index 6f7f5b540..a05ba9f7f 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -2021,12 +2021,12 @@ spec:
                     description: |-
                       Type decides the scope for the RateLimits.
                       Valid RateLimitType values are "Global" or "Local".
+
+                      Deprecated: Use Global and/or Local fields directly instead. Both can be specified simultaneously for combined rate limiting.
                     enum:
                     - Global
                     - Local
                     type: string
-                required:
-                - type
                 type: object
               requestBuffer:
                 description: |-