Repository: envoyproxy/gateway
Pull request #5391 (2025-03-03 05:04:31)

chore: fix dependabot
xref: https://github.com/envoyproxy/gateway/runs/37897802670

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5391?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 65.09%. Comparing base [(`52a7970`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/52a7970d960a381a2f63f56f449911b0a245dbc5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`76aa594`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/76aa5947aa7e30bdd9faff0e130a92953d0dc9b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #5391   +/-   ##
=======================================
  Coverage   65.09%   65.09%           
=======================================
  Files         213      213           
  Lines       33588    33588           
=======================================
+ Hits        21863    21864    +1     
+ Misses      10398    10396    -2     
- Partials     1327     1328    +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5391?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c36c9f62a..d3e8f96a0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -22,13 +22,16 @@ updates:
       - dependency-name: "envoyproxy/toolshed/gh-actions/retest"
     groups:
       github-actions:
-        - "actions*"
+        patterns:
+          - "actions*"
   - package-ecosystem: gomod
     directories:
       - "/"
-      - "examples/extension-server"
     schedule:
       interval: weekly
+    ignore:
+      # Ignore cel-go, since it should be updated alongside k8s.io/apiserver
+      - dependency-name: "github.com/google/cel-go"
     groups:
       github.com:
         patterns:
diff --git a/VERSION b/VERSION
index 18fa8e74f..757407982 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-v1.3.0
+v1.3.1
diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go
index 5c52d5373..815aca794 100644
--- a/api/v1alpha1/ratelimit_types.go
+++ b/api/v1alpha1/ratelimit_types.go
@@ -202,7 +202,6 @@ const (
 	SourceMatchExact SourceMatchType = "Exact"
 	// SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector
 	// and uses a separate rate limit bucket/counter.
-	// Note: This is only supported for Global Rate Limits.
 	SourceMatchDistinct SourceMatchType = "Distinct"
 )
 
@@ -270,7 +269,6 @@ const (
 	// HeaderMatchDistinct matches any and all possible unique values encountered in the
 	// specified HTTP Header. Note that each unique value will receive its own rate limit
 	// bucket.
-	// Note: This is only supported for Global Rate Limits.
 	HeaderMatchDistinct HeaderMatchType = "Distinct"
 )
 
diff --git a/cmd/envoy-gateway/main.go b/cmd/envoy-gateway/main.go
index e7c83fb7c..ae6922adc 100644
--- a/cmd/envoy-gateway/main.go
+++ b/cmd/envoy-gateway/main.go
@@ -9,11 +9,13 @@ import (
 	"fmt"
 	"os"
 
+	ctrl "sigs.k8s.io/controller-runtime"
+
 	"github.com/envoyproxy/gateway/cmd/envoy-gateway/root"
 )
 
 func main() {
-	if err := root.GetRootCommand().Execute(); err != nil {
+	if err := root.GetRootCommand().ExecuteContext(ctrl.SetupSignalHandler()); err != nil {
 		_, _ = fmt.Fprintln(os.Stderr, err)
 		os.Exit(1)
 	}
diff --git a/examples/extension-server/go.mod b/examples/extension-server/go.mod
index ed38b67c3..52d92e8b6 100644
--- a/examples/extension-server/go.mod
+++ b/examples/extension-server/go.mod
@@ -4,8 +4,8 @@ go 1.24.0
 
 require (
 	github.com/envoyproxy/gateway v1.3.0
-	github.com/envoyproxy/go-control-plane v0.13.4
-	github.com/envoyproxy/go-control-plane/envoy v1.32.4
+	github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb
+	github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb
 	github.com/urfave/cli/v2 v2.27.5
 	google.golang.org/grpc v1.70.0
 	google.golang.org/protobuf v1.36.5
diff --git a/examples/extension-server/go.sum b/examples/extension-server/go.sum
index 73f1c30c8..72842d102 100644
--- a/examples/extension-server/go.sum
+++ b/examples/extension-server/go.sum
@@ -8,10 +8,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
-github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb h1:6PnPVZ2oLLX6cV28/823mJA1djE4X3kzJXkigkHNNP0=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb/go.mod h1:UbsdUt4yQprC2gTbH9AaLhB/RAAajyoD55sjUAfzKUA=
+github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb h1:4oJf7zkbbNoBRGkeA0O8jB48FjM78qfYwGL6Q68AaH4=
+github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb/go.mod h1:vggMdJSZaAGnQ5sf1pBl1OV7w3IFqt8qo3id+asttJw=
 github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
diff --git a/go.mod b/go.mod
index 53818831a..950428b84 100644
--- a/go.mod
+++ b/go.mod
@@ -10,12 +10,12 @@ require (
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
-	github.com/docker/cli v27.5.1+incompatible
+	github.com/docker/cli v28.0.1+incompatible
 	github.com/docker/docker v27.5.1+incompatible
 	github.com/dominikbraun/graph v0.23.0
-	github.com/envoyproxy/go-control-plane v0.13.4
-	github.com/envoyproxy/go-control-plane/contrib v1.32.4
-	github.com/envoyproxy/go-control-plane/envoy v1.32.4
+	github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb
+	github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250303232259-0c5cb90447bb
+	github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb
 	github.com/envoyproxy/go-control-plane/ratelimit v0.1.0
 	github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7
 	github.com/evanphx/json-patch v5.9.11+incompatible
diff --git a/go.sum b/go.sum
index 315793206..0d5d83f0c 100644
--- a/go.sum
+++ b/go.sum
@@ -311,8 +311,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
-github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
+github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@@ -352,12 +352,12 @@ github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
-github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
-github.com/envoyproxy/go-control-plane/contrib v1.32.4 h1:/udV6s9xkDGe13WfrT2MHAxXTNDMBYBPxI1GkleCrmM=
-github.com/envoyproxy/go-control-plane/contrib v1.32.4/go.mod h1:gkGYoY7plfQg7FPBDhyKtP1cDA9frFR/3YsCx8taRvI=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb h1:6PnPVZ2oLLX6cV28/823mJA1djE4X3kzJXkigkHNNP0=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20250303232259-0c5cb90447bb/go.mod h1:UbsdUt4yQprC2gTbH9AaLhB/RAAajyoD55sjUAfzKUA=
+github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250303232259-0c5cb90447bb h1:rQwW655S4Bvkp/jdkoPhG6kWPfsSaK3xiDLgmSJMVio=
+github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250303232259-0c5cb90447bb/go.mod h1:dnsjEXShSTkYKebBHJng2tMCYQJhJhlP0CDtMpLRQqQ=
+github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb h1:4oJf7zkbbNoBRGkeA0O8jB48FjM78qfYwGL6Q68AaH4=
+github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250303232259-0c5cb90447bb/go.mod h1:vggMdJSZaAGnQ5sf1pBl1OV7w3IFqt8qo3id+asttJw=
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
diff --git a/internal/cmd/certgen.go b/internal/cmd/certgen.go
index b16062d03..6b34158a8 100644
--- a/internal/cmd/certgen.go
+++ b/internal/cmd/certgen.go
@@ -12,7 +12,6 @@ import (
 	"path"
 
 	"github.com/spf13/cobra"
-	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	clicfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
@@ -37,7 +36,7 @@ func GetCertGenCommand() *cobra.Command {
 		Use:   "certgen",
 		Short: "Generate Control Plane Certificates",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return certGen(local)
+			return certGen(cmd.Context(), local)
 		},
 	}
 
@@ -49,7 +48,7 @@ func GetCertGenCommand() *cobra.Command {
 }
 
 // certGen generates control plane certificates.
-func certGen(local bool) error {
+func certGen(ctx context.Context, local bool) error {
 	cfg, err := config.New()
 	if err != nil {
 		return err
@@ -68,7 +67,7 @@ func certGen(local bool) error {
 			return fmt.Errorf("failed to create controller-runtime client: %w", err)
 		}
 
-		if err = outputCertsForKubernetes(ctrl.SetupSignalHandler(), cli, cfg, overwriteControlPlaneCerts, certs); err != nil {
+		if err = outputCertsForKubernetes(ctx, cli, cfg, overwriteControlPlaneCerts, certs); err != nil {
 			return fmt.Errorf("failed to output certificates: %w", err)
 		}
 	} else {
diff --git a/internal/cmd/server.go b/internal/cmd/server.go
index 492742b4a..528170fb8 100644
--- a/internal/cmd/server.go
+++ b/internal/cmd/server.go
@@ -9,7 +9,6 @@ import (
 	"context"
 
 	"github.com/spf13/cobra"
-	ctrl "sigs.k8s.io/controller-runtime"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/admin"
@@ -38,7 +37,7 @@ func GetServerCommand() *cobra.Command {
 		Aliases: []string{"serve"},
 		Short:   "Serve Envoy Gateway",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return server()
+			return server(cmd.Context())
 		},
 	}
 	cmd.PersistentFlags().StringVarP(&cfgPath, "config-path", "c", "",
@@ -48,13 +47,12 @@ func GetServerCommand() *cobra.Command {
 }
 
 // server serves Envoy Gateway.
-func server() error {
+func server(ctx context.Context) error {
 	cfg, err := getConfig()
 	if err != nil {
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
 	hook := func(c context.Context, cfg *config.Server) error {
 		cfg.Logger.Info("Setup runners")
 		if err := setupRunners(c, cfg); err != nil {
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 16d00920c..4d3c95c49 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -676,16 +676,6 @@ func (t *Translator) buildLocalRateLimit(policy *egv1a1.BackendTrafficPolicy) (*
 		if err != nil {
 			return nil, err
 		}
-
-		if irRule.CIDRMatch != nil && irRule.CIDRMatch.Distinct {
-			return nil, fmt.Errorf("local rateLimit does not support distinct CIDRMatch")
-		}
-
-		for _, match := range irRule.HeaderMatches {
-			if match.Distinct {
-				return nil, fmt.Errorf("local rateLimit does not support distinct HeaderMatch")
-			}
-		}
 		irRules = append(irRules, irRule)
 	}
 
diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go
index 044c98ada..ec53b29ae 100644
--- a/internal/gatewayapi/listener.go
+++ b/internal/gatewayapi/listener.go
@@ -586,7 +586,7 @@ func destinationSettingFromHostAndPort(name, host string, port uint32) []*ir.Des
 			Name:      name,
 			Weight:    ptr.To[uint32](1),
 			Protocol:  ir.GRPC,
-			Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(host, port, false)},
+			Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(host, port, false, nil)},
 		},
 	}
 }
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 8c16cb7fe..f32cfeb1e 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -1336,7 +1336,7 @@ func (t *Translator) processDestination(name string, backendRefContext BackendRe
 		} else {
 			backendIps := resources.GetServiceImport(backendNamespace, string(backendRef.Name)).Spec.IPs
 			for _, ip := range backendIps {
-				ep := ir.NewDestEndpoint(ip, uint32(*backendRef.Port), false)
+				ep := ir.NewDestEndpoint(ip, uint32(*backendRef.Port), false, nil)
 				endpoints = append(endpoints, ep)
 			}
 		}
@@ -1372,7 +1372,10 @@ func (t *Translator) processDestination(name string, backendRefContext BackendRe
 		if err != nil {
 			return nil, err
 		}
-		ds.IPFamily = getServiceIPFamily(resources.GetService(backendNamespace, string(backendRef.Name)))
+		svc := resources.GetService(backendNamespace, string(backendRef.Name))
+		ds.IPFamily = getServiceIPFamily(svc)
+		ds.ZoneAwareRoutingEnabled = isZoneAwareRoutingEnabled(svc)
+
 	case egv1a1.KindBackend:
 		ds = t.processBackendDestinationSetting(name, backendRef.BackendObjectReference, backendNamespace, protocol, resources)
 		ds.TLS, err = t.applyBackendTLSSetting(
@@ -1468,15 +1471,16 @@ func (t *Translator) processServiceDestinationSetting(
 		endpoints, addrType = getIREndpointsFromEndpointSlices(endpointSlices, servicePort.Name, servicePort.Protocol)
 	} else {
 		// Fall back to Service ClusterIP routing
-		ep := ir.NewDestEndpoint(service.Spec.ClusterIP, uint32(*backendRef.Port), false)
+		ep := ir.NewDestEndpoint(service.Spec.ClusterIP, uint32(*backendRef.Port), false, nil)
 		endpoints = append(endpoints, ep)
 	}
 
 	return &ir.DestinationSetting{
-		Name:        name,
-		Protocol:    protocol,
-		Endpoints:   endpoints,
-		AddressType: addrType,
+		Name:                    name,
+		Protocol:                protocol,
+		Endpoints:               endpoints,
+		AddressType:             addrType,
+		ZoneAwareRoutingEnabled: isZoneAwareRoutingEnabled(service),
 	}
 }
 
@@ -1496,6 +1500,22 @@ func getBackendFilters(routeType gwapiv1.Kind, backendRefContext BackendRefConte
 	return nil
 }
 
+func isZoneAwareRoutingEnabled(svc *corev1.Service) bool {
+	if trafficDist := svc.Spec.TrafficDistribution; trafficDist != nil {
+		return *trafficDist == corev1.ServiceTrafficDistributionPreferClose
+	}
+
+	// Allows annotation values that align with Kubernetes defaults.
+	// Ref:
+	// https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/#enabling-topology-aware-routing
+	// https://github.com/kubernetes/kubernetes/blob/9d9e1afdf78bce0a517cc22557457f942040ca19/staging/src/k8s.io/endpointslice/utils.go#L355-L368
+	if val, ok := svc.Annotations[corev1.AnnotationTopologyMode]; ok {
+		return val == "Auto" || val == "auto"
+	}
+
+	return false
+}
+
 func (t *Translator) processDestinationFilters(routeType gwapiv1.Kind, backendRefContext BackendRefContext, parentRef *RouteParentContext, route RouteContext, resources *resource.Resources) (*ir.DestinationFilters, error) {
 	backendFilters := getBackendFilters(routeType, backendRefContext)
 	if backendFilters == nil {
@@ -1693,12 +1713,12 @@ func getIREndpointsFromEndpointSlice(endpointSlice *discoveryv1.EndpointSlice, p
 				// Drain the endpoint if it is being terminated
 				draining := *conditions.Terminating
 				for _, address := range endpoint.Addresses {
-					ep := ir.NewDestEndpoint(address, uint32(*endpointPort.Port), draining)
+					ep := ir.NewDestEndpoint(address, uint32(*endpointPort.Port), draining, endpoint.Zone)
 					endpoints = append(endpoints, ep)
 				}
 			} else if conditions.Ready == nil || *conditions.Ready {
 				for _, address := range endpoint.Addresses {
-					ep := ir.NewDestEndpoint(address, uint32(*endpointPort.Port), false)
+					ep := ir.NewDestEndpoint(address, uint32(*endpointPort.Port), false, endpoint.Zone)
 					endpoints = append(endpoints, ep)
 				}
 			}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index acefaf9c2..368e7d5db 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -1474,6 +1474,9 @@ type DestinationSetting struct {
 	IPFamily *egv1a1.IPFamily    `json:"ipFamily,omitempty" yaml:"ipFamily,omitempty"`
 	TLS      *TLSUpstreamConfig  `json:"tls,omitempty" yaml:"tls,omitempty"`
 	Filters  *DestinationFilters `json:"filters,omitempty" yaml:"filters,omitempty"`
+	// ZoneAwareRoutingEnabled specifies whether to enable Zone Aware Routing for this destination's endpoints.
+	// This is derived from the backend service and depends on having Kubernetes Topology Aware Routing or Traffic Distribution enabled.
+	ZoneAwareRoutingEnabled bool `json:"zoneAwareRoutingEnabled,omitempty" yaml:"zoneAwareRoutingEnabled,omitempty"`
 }
 
 // Validate the fields within the DestinationSetting structure
@@ -1508,6 +1511,8 @@ type DestinationEndpoint struct {
 	Path *string `json:"path,omitempty" yaml:"path,omitempty"`
 	// Draining is true if this endpoint should be drained
 	Draining bool `json:"draining,omitempty" yaml:"draining,omitempty"`
+	// Zone refers to the topology zone the Endpoint resides in
+	Zone *string `json:"zone,omitempty" yaml:"zone,omitempty"`
 }
 
 // Validate the fields within the DestinationEndpoint structure
@@ -1539,11 +1544,12 @@ func (d DestinationEndpoint) Validate() error {
 }
 
 // NewDestEndpoint creates a new DestinationEndpoint.
-func NewDestEndpoint(host string, port uint32, draining bool) *DestinationEndpoint {
+func NewDestEndpoint(host string, port uint32, draining bool, zone *string) *DestinationEndpoint {
 	return &DestinationEndpoint{
 		Host:     host,
 		Port:     port,
 		Draining: draining,
+		Zone:     zone,
 	}
 }
 
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 611978906..4735b2e73 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -774,6 +774,11 @@ func (in *DestinationEndpoint) DeepCopyInto(out *DestinationEndpoint) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.Zone != nil {
+		in, out := &in.Zone, &out.Zone
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestinationEndpoint.
diff --git a/internal/xds/extensions/extensions.gen.go b/internal/xds/extensions/extensions.gen.go
index 052eb2216..5df9ca1bb 100644
--- a/internal/xds/extensions/extensions.gen.go
+++ b/internal/xds/extensions/extensions.gen.go
@@ -204,6 +204,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/local_response_policy/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/redirect_policy/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_validators/envoy_default/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/injected_credentials/generic/v3"
diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go
index a380d7ecd..b3b8368ef 100644
--- a/internal/xds/translator/cluster.go
+++ b/internal/xds/translator/cluster.go
@@ -158,6 +158,12 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
 	}
 
 	for i, ds := range args.settings {
+
+		// If zone aware routing is enabled we update the cluster lb config
+		if ds.ZoneAwareRoutingEnabled {
+			cluster.CommonLbConfig.LocalityConfigSpecifier = &clusterv3.Cluster_CommonLbConfig_ZoneAwareLbConfig_{ZoneAwareLbConfig: &clusterv3.Cluster_CommonLbConfig_ZoneAwareLbConfig{}}
+		}
+
 		if ds.TLS != nil {
 			socket, err := buildXdsUpstreamTLSSocketWthCert(ds.TLS)
 			if err != nil {
@@ -446,8 +452,6 @@ func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.Destin
 	localities := make([]*endpointv3.LocalityLbEndpoints, 0, len(destSettings))
 	for i, ds := range destSettings {
 
-		endpoints := make([]*endpointv3.LbEndpoint, 0, len(ds.Endpoints))
-
 		var metadata *corev3.Metadata
 		if ds.TLS != nil {
 			metadata = &corev3.Metadata{
@@ -461,45 +465,101 @@ func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.Destin
 			}
 		}
 
-		for _, irEp := range ds.Endpoints {
-			healthStatus := corev3.HealthStatus_UNKNOWN
-			if irEp.Draining {
-				healthStatus = corev3.HealthStatus_DRAINING
-			}
-			lbEndpoint := &endpointv3.LbEndpoint{
-				Metadata: metadata,
-				HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
-					Endpoint: &endpointv3.Endpoint{
-						Address: buildAddress(irEp),
-					},
+		// If zone aware routing is enabled for a backendRefs we include endpoint zone info in localities.
+		// Note: The locality.LoadBalancingWeight field applies only when using locality-weighted LB config
+		// so in order to support traffic splitting we rely on weighted clusters defined at the route level
+		// if multiple backendRefs exist. This pushes part of the routing logic higher up the stack which can
+		// limit host selection controls during retries and session affinity.
+		// For more details see https://github.com/envoyproxy/gateway/issues/5307#issuecomment-2688767482
+		if ds.ZoneAwareRoutingEnabled {
+			localities = append(localities, buildZonalLocalities(metadata, ds)...)
+		} else {
+			localities = append(localities, buildWeightedLocalities(metadata, ds))
+		}
+	}
+	return &endpointv3.ClusterLoadAssignment{ClusterName: clusterName, Endpoints: localities}
+}
+
+func buildZonalLocalities(metadata *corev3.Metadata, ds *ir.DestinationSetting) []*endpointv3.LocalityLbEndpoints {
+	var localities []*endpointv3.LocalityLbEndpoints
+	zonalEndpoints := make(map[string][]*endpointv3.LbEndpoint)
+	for _, irEp := range ds.Endpoints {
+		healthStatus := corev3.HealthStatus_UNKNOWN
+		if irEp.Draining {
+			healthStatus = corev3.HealthStatus_DRAINING
+		}
+		lbEndpoint := &endpointv3.LbEndpoint{
+			Metadata: metadata,
+			HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
+				Endpoint: &endpointv3.Endpoint{
+					Address: buildAddress(irEp),
 				},
-				HealthStatus: healthStatus,
-			}
-			// Set default weight of 1 for all endpoints.
-			lbEndpoint.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: 1}
-			endpoints = append(endpoints, lbEndpoint)
+			},
+			LoadBalancingWeight: wrapperspb.UInt32(1),
+			HealthStatus:        healthStatus,
 		}
 
+		zone := ptr.Deref(irEp.Zone, "")
+		zonalEndpoints[zone] = append(zonalEndpoints[zone], lbEndpoint)
+	}
+
+	for zone, endPts := range zonalEndpoints {
 		locality := &endpointv3.LocalityLbEndpoints{
 			Locality: &corev3.Locality{
-				Region: ds.Name,
+				Zone: zone,
 			},
-			LbEndpoints: endpoints,
-			Priority:    0,
+			LbEndpoints: endPts,
+			Priority:    ptr.Deref(ds.Priority, 0),
 		}
+		localities = append(localities, locality)
+	}
+	// Sort localities by zone, so that the order is deterministic.
+	sort.Slice(localities, func(i, j int) bool {
+		return localities[i].Locality.Zone < localities[j].Locality.Zone
+	})
 
-		// Set locality weight
-		var weight uint32
-		if ds.Weight != nil {
-			weight = *ds.Weight
-		} else {
-			weight = 1
+	return localities
+}
+
+func buildWeightedLocalities(metadata *corev3.Metadata, ds *ir.DestinationSetting) *endpointv3.LocalityLbEndpoints {
+	endpoints := make([]*endpointv3.LbEndpoint, 0, len(ds.Endpoints))
+
+	for _, irEp := range ds.Endpoints {
+		healthStatus := corev3.HealthStatus_UNKNOWN
+		if irEp.Draining {
+			healthStatus = corev3.HealthStatus_DRAINING
 		}
-		locality.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: weight}
-		locality.Priority = ptr.Deref(ds.Priority, 0)
-		localities = append(localities, locality)
+		lbEndpoint := &endpointv3.LbEndpoint{
+			Metadata: metadata,
+			HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
+				Endpoint: &endpointv3.Endpoint{
+					Address: buildAddress(irEp),
+				},
+			},
+			HealthStatus: healthStatus,
+		}
+		// Set default weight of 1 for all endpoints.
+		lbEndpoint.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: 1}
+		endpoints = append(endpoints, lbEndpoint)
 	}
-	return &endpointv3.ClusterLoadAssignment{ClusterName: clusterName, Endpoints: localities}
+	locality := &endpointv3.LocalityLbEndpoints{
+		Locality: &corev3.Locality{
+			Region: ds.Name,
+		},
+		LbEndpoints: endpoints,
+		Priority:    0,
+	}
+
+	// Set locality weight
+	var weight uint32
+	if ds.Weight != nil {
+		weight = *ds.Weight
+	} else {
+		weight = 1
+	}
+	locality.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: weight}
+	locality.Priority = ptr.Deref(ds.Priority, 0)
+	return locality
 }
 
 func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) map[string]*anypb.Any {
diff --git a/internal/xds/translator/local_ratelimit.go b/internal/xds/translator/local_ratelimit.go
index ea3c43516..1fb7272f7 100644
--- a/internal/xds/translator/local_ratelimit.go
+++ b/internal/xds/translator/local_ratelimit.go
@@ -27,6 +27,7 @@ import (
 const (
 	localRateLimitFilterStatPrefix = "http_local_rate_limiter"
 	descriptorMaskedRemoteAddress  = "masked_remote_address"
+	descriptorRemoteAddress        = "remote_address"
 )
 
 func init() {
@@ -59,6 +60,11 @@ func (*localRateLimit) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir
 
 	localRl := &localrlv3.LocalRateLimit{
 		StatPrefix: localRateLimitFilterStatPrefix,
+		MaxDynamicDescriptors: &wrapperspb.UInt32Value{
+			Value: 10000,
+			// Default to 10k, assuming a listener has 10k unique active users to be rate limited.
+			// We can make this configurable in the API if needed.
+		},
 	}
 
 	localRlAny, err := anypb.New(localRl)
@@ -129,10 +135,7 @@ func (*localRateLimit) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) e
 
 	local := irRoute.Traffic.RateLimit.Local
 
-	rateLimits, descriptors, err := buildRouteLocalRateLimits(local)
-	if err != nil {
-		return err
-	}
+	rateLimits, descriptors := buildRouteLocalRateLimits(local)
 	routeAction.RateLimits = rateLimits
 
 	filterCfg := route.GetTypedPerFilterConfig()
@@ -189,7 +192,7 @@ func (*localRateLimit) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) e
 }
 
 func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
-	[]*routev3.RateLimit, []*rlv3.LocalRateLimitDescriptor, error,
+	[]*routev3.RateLimit, []*rlv3.LocalRateLimitDescriptor,
 ) {
 	var rateLimits []*routev3.RateLimit
 	var descriptors []*rlv3.LocalRateLimitDescriptor
@@ -201,42 +204,57 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
 
 		// HeaderMatches
 		for mIdx, match := range rule.HeaderMatches {
+			var action *routev3.RateLimit_Action
+			var entry *rlv3.RateLimitDescriptor_Entry
+
 			if match.Distinct {
-				// This is a sanity check. This should never happen because Gateway
-				// API translator should have already validated this.
-				if rule.CIDRMatch.Distinct {
-					return nil, nil, errors.New("local rateLimit does not support distinct HeaderMatch")
+				// For distinct matches, we only check if the header exists using the RequestHeaders action.
+				descriptorKey := getRouteRuleDescriptor(rIdx, mIdx)
+				action = &routev3.RateLimit_Action{
+					ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{
+						RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{
+							HeaderName:    match.Name,
+							DescriptorKey: descriptorKey,
+						},
+					},
 				}
-			}
-
-			// Setup HeaderValueMatch actions
-			descriptorKey := getRouteRuleDescriptor(rIdx, mIdx)
-			descriptorVal := getRouteRuleDescriptor(rIdx, mIdx)
-			headerMatcher := &routev3.HeaderMatcher{
-				Name: match.Name,
-				HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
-					StringMatch: buildXdsStringMatcher(match),
-				},
-			}
-			expectMatch := true
-			if match.Invert != nil && *match.Invert {
-				expectMatch = false
-			}
-			action := &routev3.RateLimit_Action{
-				ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
-					HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
-						DescriptorKey:   descriptorKey,
-						DescriptorValue: descriptorVal,
-						ExpectMatch: &wrapperspb.BoolValue{
-							Value: expectMatch,
+				// The descriptor entry value is not set for distinct matches, which means that each distinct
+				// value of the matched header will be counted separately.
+				entry = &rlv3.RateLimitDescriptor_Entry{
+					Key: descriptorKey,
+				}
+			} else {
+				// For exact matches, we check if there is an existing header with the matching value using the
+				// HeaderValueMatch action.
+				descriptorKey := getRouteRuleDescriptor(rIdx, mIdx)
+				descriptorVal := getRouteRuleDescriptor(rIdx, mIdx)
+				headerMatcher := &routev3.HeaderMatcher{
+					Name: match.Name,
+					HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+						StringMatch: buildXdsStringMatcher(match),
+					},
+				}
+				expectMatch := true
+				if match.Invert != nil && *match.Invert {
+					expectMatch = false
+				}
+				action = &routev3.RateLimit_Action{
+					ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+						HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+							DescriptorKey:   descriptorKey,
+							DescriptorValue: descriptorVal,
+							ExpectMatch: &wrapperspb.BoolValue{
+								Value: expectMatch,
+							},
+							Headers: []*routev3.HeaderMatcher{headerMatcher},
 						},
-						Headers: []*routev3.HeaderMatcher{headerMatcher},
 					},
-				},
-			}
-			entry := &rlv3.RateLimitDescriptor_Entry{
-				Key:   descriptorKey,
-				Value: descriptorVal,
+				}
+				// For exact matches, the descriptor entry value is set to the generated descriptor value.
+				entry = &rlv3.RateLimitDescriptor_Entry{
+					Key:   descriptorKey,
+					Value: descriptorVal,
+				}
 			}
 			rlActions = append(rlActions, action)
 			descriptorEntries = append(descriptorEntries, entry)
@@ -244,13 +262,8 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
 
 		// Source IP CIDRMatch
 		if rule.CIDRMatch != nil {
-			// This is a sanity check. This should never happen because Gateway
-			// API translator should have already validated this.
-			if rule.CIDRMatch.Distinct {
-				return nil, nil, errors.New("local rateLimit does not support distinct CIDRMatch")
-			}
-
-			// Setup MaskedRemoteAddress action
+			// For CIDR matches, we first need to check if the source IP matches the CIDR range using
+			// the MaskedRemoteAddress action.
 			mra := &routev3.RateLimit_Action_MaskedRemoteAddress{}
 			maskLen := &wrapperspb.UInt32Value{Value: rule.CIDRMatch.MaskLen}
 			if rule.CIDRMatch.IsIPv6 {
@@ -269,6 +282,23 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
 			}
 			descriptorEntries = append(descriptorEntries, entry)
 			rlActions = append(rlActions, action)
+
+			if rule.CIDRMatch.Distinct {
+				// If the CIDRMatch is distinct, we also need to use the RemoteAddress action to get the client IP.
+				action = &routev3.RateLimit_Action{
+					ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{
+						RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{},
+					},
+				}
+
+				// If the CIDRMatch is distinct, we use the built-in remote address descriptor key without a value.
+				// This means that each distinct client IP will be counted separately.
+				entry = &rlv3.RateLimitDescriptor_Entry{
+					Key: descriptorRemoteAddress,
+				}
+				descriptorEntries = append(descriptorEntries, entry)
+				rlActions = append(rlActions, action)
+			}
 		}
 
 		rateLimit := &routev3.RateLimit{Actions: rlActions}
@@ -287,5 +317,5 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
 		descriptors = append(descriptors, descriptor)
 	}
 
-	return rateLimits, descriptors, nil
+	return rateLimits, descriptors
 }
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index 4c1417e0f..fc1814f2d 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -348,7 +348,7 @@ func createOAuth2TokenEndpointCluster(tCtx *types.ResourceVersionTable,
 	ds = &ir.DestinationSetting{
 		Weight: ptr.To[uint32](1),
 		Endpoints: []*ir.DestinationEndpoint{
-			ir.NewDestEndpoint(cluster.hostname, cluster.port, false),
+			ir.NewDestEndpoint(cluster.hostname, cluster.port, false, nil),
 		},
 		Name: destinationSettingName(cluster.name),
 	}
diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go
index 2ca4ae37e..464be9cdb 100644
--- a/internal/xds/translator/ratelimit.go
+++ b/internal/xds/translator/ratelimit.go
@@ -526,7 +526,7 @@ func (t *Translator) createRateLimitServiceCluster(tCtx *types.ResourceVersionTa
 	ds := &ir.DestinationSetting{
 		Weight:    ptr.To[uint32](1),
 		Protocol:  ir.GRPC,
-		Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(host, port, false)},
+		Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(host, port, false, nil)},
 		Name:      destinationSettingName(clusterName),
 	}
 
diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go
index 662036cc3..2bff31761 100644
--- a/internal/xds/translator/route.go
+++ b/internal/xds/translator/route.go
@@ -224,7 +224,7 @@ func buildXdsStringMatcher(irMatch *ir.StringMatch) *matcherv3.StringMatcher {
 
 func buildXdsRouteAction(backendWeights *ir.BackendWeights, settings []*ir.DestinationSetting) *routev3.RouteAction {
 	// only use weighted cluster when there are invalid weights
-	if hasFiltersInSettings(settings) || backendWeights.Invalid != 0 {
+	if needsClusterPerSetting(settings) || backendWeights.Invalid != 0 {
 		return buildXdsWeightedRouteAction(backendWeights, settings)
 	}
 
@@ -708,6 +708,13 @@ func buildRetryOn(triggers []ir.TriggerEnum) (string, error) {
 	return b.String(), nil
 }
 
+func needsClusterPerSetting(settings []*ir.DestinationSetting) bool {
+	if hasFiltersInSettings(settings) || hasZoneAwareRouting(settings) {
+		return true
+	}
+	return false
+}
+
 func hasFiltersInSettings(settings []*ir.DestinationSetting) bool {
 	for _, setting := range settings {
 		filters := setting.Filters
@@ -717,3 +724,16 @@ func hasFiltersInSettings(settings []*ir.DestinationSetting) bool {
 	}
 	return false
 }
+
+func hasZoneAwareRouting(settings []*ir.DestinationSetting) bool {
+	// Only use weighted clusters if more than one setting
+	if len(settings) < 2 {
+		return false
+	}
+	for _, setting := range settings {
+		if setting.ZoneAwareRoutingEnabled {
+			return true
+		}
+	}
+	return false
+}
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 61ac4f891..f9cd6d90b 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -585,7 +585,7 @@ func (t *Translator) addRouteToRouteConfig(
 			var err error
 			// If there are no filters in the destination settings we create
 			// a regular xds Cluster
-			if !hasFiltersInSettings(httpRoute.Destination.Settings) {
+			if !needsClusterPerSetting(httpRoute.Destination.Settings) {
 				err = processXdsCluster(
 					tCtx,
 					httpRoute.Destination.Name,
diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go
index 2016a92f6..a4567ba5b 100644
--- a/internal/xds/translator/utils.go
+++ b/internal/xds/translator/utils.go
@@ -186,7 +186,7 @@ func addClusterFromURL(url string, tCtx *types.ResourceVersionTable) error {
 
 	ds = &ir.DestinationSetting{
 		Weight:    ptr.To[uint32](1),
-		Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(uc.hostname, uc.port, false)},
+		Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(uc.hostname, uc.port, false, nil)},
 		Name:      destinationSettingName(uc.name),
 	}
 
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 41078a15f..3579855d9 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -18,7 +18,10 @@ new features: |
   Added support for defining Lua EnvoyExtensionPolicies
   Added RequestID field in ClientTrafficPolicy.HeaderSettings to configure Envoy X-Request-ID behavior.
   Added support for HorizontalPodAutoscaler to helm chart
+  Added support for distinct header and distinct source CIDR based local rate limiting
   Added support for forwarding the authenticated username to the backend via a configurable header in BasicAuth
+  Added support for zone aware routing
+
 
 bug fixes: |
   Fix traffic splitting when filters are attached to the backendRef.
@@ -36,4 +39,3 @@ deprecations: |
 
 # Other notable changes not covered by the above sections.
 Other changes: |
-
diff --git a/release-notes/v1.3.1.yaml b/release-notes/v1.3.1.yaml
new file mode 100644
index 000000000..60abd6cb3
--- /dev/null
+++ b/release-notes/v1.3.1.yaml
@@ -0,0 +1,29 @@
+date: March 4, 2025
+
+# Changes that are expected to cause an incompatibility with previous versions, such as deletions or modifications to existing APIs.
+breaking changes: |
+  Use the envoy JSON formatter for the default access log instead of text formatter.
+
+# Updates addressing vulnerabilities, security flaws, or compliance requirements.
+security updates: |
+  Fixed CVE-2025-25294: log injection vulnerability in Envoy Gateway when using default access log.
+
+# New features or capabilities added in this release.
+new features: |
+  Add defaulter for gateway-api resources loading from file to be able to set default values.
+
+bug fixes: |
+  Added support for Secret and ConfigMap parsing in Standalone mode.
+  Fix translating backendSettings for extAuth.
+  Fix allowing weights to be zero on endpoints for backendRefs in TCPRoute and UDPRoute.
+  Fix validation of all xDS resources.
+  Fix support for empty values in fields with default values in Standalone.
+
+# Other notable changes not covered by the above sections.
+Other changes: |
+  Bump golang to 1.23.6.
+  Bump k8s-io to 1.32.1.
+  Bump ratelimit to ae4cee11.
+  Bump golang.org/x/oauth2 to v0.27.0.
+  Bump golang.org/x/crypto to v0.35.0.
+
diff --git a/site/content/en/news/releases/notes/v1.3.1.md b/site/content/en/news/releases/notes/v1.3.1.md
new file mode 100644
index 000000000..866e0cddf
--- /dev/null
+++ b/site/content/en/news/releases/notes/v1.3.1.md
@@ -0,0 +1,30 @@
+---
+title: "v1.3.1"
+publishdate: 2025-03-04
+---
+
+Date: March 4, 2025
+
+## Breaking changes
+- Use the envoy JSON formatter for the default access log instead of text formatter.
+
+## Security updates
+- Fixed CVE-2025-25294: log injection vulnerability in Envoy Gateway when using default access log.
+
+## New features
+- Add defaulter for gateway-api resources loading from file to be able to set default values.
+
+## Bug fixes
+- Added support for Secret and ConfigMap parsing in Standalone mode.
+- Fix translating backendSettings for extAuth.
+- Fix allowing weights to be zero on endpoints for backendRefs in TCPRoute and UDPRoute.
+- Fix validation of all xDS resources.
+- Fix support for empty values in fields with default values in Standalone.
+
+## Other changes
+- Bump golang to 1.23.6.
+- Bump k8s-io to 1.32.1.
+- Bump ratelimit to ae4cee11.
+- Bump golang.org/x/oauth2 to v0.27.0.
+- Bump golang.org/x/crypto to v0.35.0.
+
diff --git a/site/layouts/shortcodes/helm-version.html b/site/layouts/shortcodes/helm-version.html
index 992bf751e..4d37257aa 100644
--- a/site/layouts/shortcodes/helm-version.html
+++ b/site/layouts/shortcodes/helm-version.html
@@ -9,8 +9,8 @@
 {{- "v1.2.6" -}}
 {{- end -}}
 {{- with (strings.HasPrefix $pagePrefix "v1.3") -}}
-{{- "v1.3.0" -}}
+{{- "v1.3.1" -}}
 {{- end -}}
 {{- with (strings.HasPrefix $pagePrefix "doc") -}}
-{{- "v1.3.0" -}}
+{{- "v1.3.1" -}}
 {{- end -}}
diff --git a/site/layouts/shortcodes/yaml-version.html b/site/layouts/shortcodes/yaml-version.html
index cd2aded56..811d5dada 100644
--- a/site/layouts/shortcodes/yaml-version.html
+++ b/site/layouts/shortcodes/yaml-version.html
@@ -9,8 +9,8 @@
 {{- "v1.2.6" -}}
 {{- end -}}
 {{- with (strings.HasPrefix $pagePrefix "v1.3") -}}
-{{- "v1.3.0" -}}
+{{- "v1.3.1" -}}
 {{- end -}}
 {{- with (strings.HasPrefix $pagePrefix "doc") -}}
-{{- "v1.3.0" -}}
+{{- "v1.3.1" -}}
 {{- end -}}
diff --git a/site/static/logos/kubermatic.svg b/site/static/logos/kubermatic.svg
index 7536ba747..ad1a9df4b 100644
--- a/site/static/logos/kubermatic.svg
+++ b/site/static/logos/kubermatic.svg
@@ -1,56 +1,60 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 viewBox="0 0 800 240" style="enable-background:new 0 0 800 240;" xml:space="preserve">
+	 viewBox="0 0 800 400" style="enable-background:new 0 0 800 400;" xml:space="preserve">
 <style type="text/css">
 	.st0{fill:#00517D;}
 </style>
 <g>
-	<polygon class="st0" points="165,64.8 165,95.1 125,114.8 125,84.6 	"/>
-	<polygon class="st0" points="215,84.6 215,114.8 175,95.1 175,64.8 	"/>
-	<polygon class="st0" points="165,144 165,174.3 125,154.6 125,124.3 	"/>
-	<polygon class="st0" points="215,124.3 215,154.6 175,174.3 175,144 	"/>
-	<polygon class="st0" points="115,84.8 87,65.2 87,175.2 115,155.5 	"/>
+	<g>
+		<polygon class="st0" points="424,53.4 424,105.4 355.4,139.2 355.4,87.4 		"/>
+		<polygon class="st0" points="509.8,87.4 509.8,139.2 441.2,105.4 441.2,53.4 		"/>
+		<polygon class="st0" points="424,189.3 424,241.3 355.4,207.5 355.4,155.5 		"/>
+		<polygon class="st0" points="509.8,155.5 509.8,207.5 441.2,241.3 441.2,189.3 		"/>
+		<polygon class="st0" points="338.2,87.8 290.2,54.1 290.2,242.8 338.2,209.1 		"/>
+	</g>
+	<g>
+		<path d="M208.6,345.5c-1-1.6-2.2-3.3-3.6-5c-1.4-1.8-2.8-3.5-4.3-5.1s-3.1-3.2-4.7-4.7c-1.6-1.5-3.2-2.8-4.7-3.8v18.7h-7.6V304
+			h7.6v17.2c1.3-1.3,2.7-2.7,4.1-4.2s2.9-3,4.3-4.6c1.4-1.5,2.8-3,4-4.5c1.3-1.4,2.4-2.7,3.5-3.9h9.1c-1.3,1.5-2.7,3.1-4.2,4.7
+			s-3,3.3-4.6,5s-3.1,3.3-4.7,4.9c-1.6,1.6-3.1,3.2-4.6,4.7c1.7,1.3,3.4,2.8,5.1,4.5c1.7,1.7,3.5,3.5,5.1,5.5c1.7,1.9,3.3,4,4.8,6.1
+			s2.9,4.2,4.2,6.3h-8.8V345.5z"/>
+		<path d="M244.4,346c-2.8,0-5.3-0.4-7.3-1.2s-3.7-2-5.1-3.5c-1.3-1.5-2.3-3.2-3-5.2c-0.6-2-1-4.2-1-6.6v-26h7.6v25.3
+			c0,1.9,0.2,3.5,0.6,4.8c0.4,1.3,1,2.4,1.8,3.3s1.7,1.5,2.8,1.9s2.3,0.6,3.6,0.6s2.5-0.2,3.6-0.6s2-1,2.8-1.9
+			c0.8-0.8,1.4-1.9,1.8-3.3c0.4-1.3,0.6-2.9,0.6-4.8v-25.3h7.6v26c0,2.4-0.3,4.6-1,6.6s-1.7,3.7-3,5.2c-1.3,1.5-3.1,2.6-5.1,3.5
+			C249.7,345.6,247.3,346,244.4,346z"/>
+		<path d="M289.9,346c-2.2,0-4.4-0.1-6.7-0.2c-2.2-0.1-4.2-0.4-5.9-0.9v-40.4c0.9-0.2,1.8-0.3,2.8-0.4s2-0.3,3.1-0.3
+			c1-0.1,2.1-0.1,3.1-0.2c1,0,2-0.1,2.9-0.1c2.5,0,4.9,0.2,7,0.6c2.2,0.4,4,1,5.6,1.9s2.8,2,3.7,3.5c0.9,1.4,1.3,3.2,1.3,5.3
+			c0,2-0.5,3.6-1.4,5s-2.3,2.5-3.9,3.4c2.5,0.8,4.4,2.1,5.6,3.7s1.8,3.7,1.8,6.2c0,4.3-1.6,7.5-4.7,9.6
+			C301.1,344.9,296.3,346,289.9,346z M284.8,320.8h5.3c2.9,0,5.1-0.4,6.8-1.2c1.6-0.8,2.5-2.3,2.5-4.4c0-2-0.9-3.4-2.5-4.3
+			c-1.7-0.8-3.9-1.3-6.6-1.3c-1.2,0-2.2,0-3.1,0.1s-1.7,0.1-2.3,0.2L284.8,320.8L284.8,320.8z M284.8,326.8v12.7
+			c0.8,0.1,1.7,0.1,2.7,0.2c1,0,1.8,0.1,2.6,0.1c1.6,0,3-0.1,4.4-0.3c1.3-0.2,2.5-0.5,3.5-1s1.8-1.2,2.3-2c0.6-0.8,0.9-1.9,0.9-3.2
+			c0-2.4-0.9-4-2.6-4.9s-4.1-1.4-7.1-1.4h-6.7V326.8z"/>
+		<path d="M323.6,345.5v-41.6h26.7v6.4h-19.1v10.3h17v6.3h-17V339h20.6v6.4L323.6,345.5L323.6,345.5z"/>
+		<path d="M378.3,303.7c6,0,10.6,1.1,13.8,3.3c3.2,2.2,4.8,5.6,4.8,10.1c0,5.6-2.8,9.5-8.3,11.5c0.8,0.9,1.6,2,2.6,3.4
+			c1,1.3,1.9,2.8,2.9,4.3s2,3.1,2.9,4.7s1.7,3.2,2.5,4.8H391c-0.8-1.4-1.6-2.9-2.5-4.3c-0.9-1.5-1.8-2.9-2.7-4.3
+			c-0.9-1.4-1.8-2.7-2.7-3.9c-0.9-1.2-1.7-2.3-2.5-3.3c-0.6,0-1,0.1-1.4,0.1s-0.8,0-1.1,0h-3.7v15.7h-7.6v-41
+			c1.8-0.4,3.8-0.7,5.9-0.8C374.8,303.8,376.6,303.7,378.3,303.7z M378.8,310.2c-1.6,0-3.1,0.1-4.4,0.2v13.4h3.3
+			c1.8,0,3.5-0.1,4.9-0.3c1.4-0.2,2.6-0.6,3.5-1.1s1.7-1.2,2.1-2.1c0.5-0.9,0.7-2,0.7-3.4c0-1.3-0.2-2.4-0.7-3.2
+			c-0.5-0.9-1.2-1.6-2.1-2.1s-2-0.9-3.2-1.1S380.3,310.2,378.8,310.2z"/>
+		<path d="M423.7,303.9c0.9,1.5,1.9,3.4,2.9,5.6c1.1,2.2,2.2,4.5,3.3,6.9c1.1,2.4,2.2,4.9,3.3,7.4c1.1,2.5,2.1,4.8,3,6.9
+			c0.9-2.1,1.9-4.4,3-6.9s2.2-5,3.3-7.4c1.1-2.4,2.2-4.7,3.3-6.9c1.1-2.2,2.1-4.1,2.9-5.6h6.8c0.4,3,0.7,6.3,1,9.7
+			c0.3,3.4,0.6,6.9,0.9,10.5s0.5,7.2,0.7,10.8s0.4,7.2,0.6,10.6h-7.4c-0.2-4.8-0.4-9.8-0.6-14.9s-0.6-10.1-1-14.9
+			c-0.4,0.9-1,2-1.6,3.3s-1.2,2.7-1.9,4.2c-0.7,1.5-1.4,3-2.1,4.6s-1.4,3.2-2,4.7c-0.7,1.5-1.3,2.9-1.8,4.2s-1,2.4-1.4,3.3h-5.6
+			c-0.4-0.9-0.9-2-1.4-3.4c-0.6-1.3-1.2-2.7-1.8-4.2s-1.3-3-2-4.6s-1.4-3.1-2.1-4.6s-1.3-2.9-1.9-4.2c-0.6-1.3-1.1-2.4-1.6-3.3
+			c-0.4,4.8-0.8,9.8-1,14.9s-0.4,10.1-0.6,14.9h-7.4c0.2-3.4,0.4-7,0.6-10.7s0.5-7.3,0.7-11s0.6-7.1,0.9-10.5s0.7-6.5,1-9.4H423.7z"
+			/>
+		<path d="M502.1,345.5c-0.6-1.6-1.1-3.3-1.7-4.9s-1.2-3.2-1.7-4.9H481c-0.6,1.7-1.1,3.3-1.7,5s-1.2,3.2-1.7,4.8h-7.9
+			c1.6-4.4,3-8.5,4.4-12.3s2.8-7.3,4.1-10.7c1.3-3.4,2.7-6.6,4-9.6s2.7-6,4.1-9h7.2c1.4,3,2.8,6,4.1,9c1.3,3,2.6,6.2,4,9.6
+			c1.3,3.4,2.7,6.9,4.1,10.7c1.4,3.8,2.9,7.9,4.5,12.3H502.1z M489.8,312c-0.9,2.1-2,4.7-3.2,7.7s-2.4,6.3-3.7,9.9h13.7
+			c-1.3-3.6-2.5-6.9-3.7-10C491.7,316.6,490.7,314,489.8,312z"/>
+		<path d="M547.9,303.9v6.5h-12.8v35h-7.6v-35h-12.8v-6.5H547.9z"/>
+		<path d="M560.5,303.9h7.6v41.6h-7.6V303.9z"/>
+		<path d="M603.1,346.5c-3.1,0-5.9-0.5-8.4-1.4s-4.6-2.4-6.3-4.2c-1.7-1.9-3-4.1-4-6.8c-0.9-2.7-1.4-5.8-1.4-9.2
+			c0-3.5,0.5-6.6,1.6-9.2c1.1-2.7,2.5-5,4.4-6.8c1.8-1.9,4-3.3,6.5-4.2c2.5-1,5.1-1.4,8-1.4c1.7,0,3.3,0.1,4.7,0.4s2.6,0.6,3.7,0.9
+			c1,0.3,1.9,0.7,2.6,1c0.7,0.4,1.2,0.6,1.4,0.8l-2.2,6.2c-1-0.6-2.3-1.2-4.1-1.7s-3.7-0.8-5.8-0.8c-1.8,0-3.5,0.3-5,0.9
+			c-1.6,0.6-2.9,1.5-4,2.8c-1.1,1.2-2,2.8-2.7,4.7c-0.6,1.9-1,4.1-1,6.5c0,2.2,0.3,4.2,0.8,6.1c0.5,1.8,1.3,3.4,2.3,4.8
+			c1,1.3,2.4,2.4,4,3.1s3.5,1.1,5.8,1.1c2.7,0,4.9-0.3,6.5-0.8s3-1,3.9-1.5l2,6.2c-0.5,0.3-1.1,0.6-1.9,1c-0.8,0.3-1.8,0.6-2.9,0.9
+			c-1.1,0.3-2.4,0.5-3.8,0.7C606.2,346.4,604.7,346.5,603.1,346.5z"/>
+	</g>
 </g>
-<path d="M298.8,140.8c-1-1.6-2.2-3.3-3.6-5c-1.4-1.8-2.8-3.5-4.3-5.1c-1.5-1.6-3.1-3.2-4.7-4.7c-1.6-1.5-3.2-2.8-4.7-3.8v18.7H274
-	V99.2h7.6v17.2c1.3-1.3,2.7-2.7,4.1-4.2s2.9-3,4.3-4.6c1.4-1.5,2.8-3,4-4.5c1.3-1.4,2.4-2.7,3.5-3.9h9.1c-1.3,1.5-2.7,3.1-4.2,4.7
-	c-1.5,1.6-3,3.3-4.6,5c-1.6,1.7-3.1,3.3-4.7,4.9c-1.6,1.6-3.1,3.2-4.6,4.7c1.7,1.3,3.4,2.8,5.1,4.5c1.7,1.7,3.5,3.5,5.1,5.5
-	c1.7,1.9,3.3,4,4.8,6.1c1.5,2.1,2.9,4.2,4.2,6.3H298.8z"/>
-<path d="M334.7,141.2c-2.8,0-5.3-0.4-7.3-1.2s-3.7-2-5.1-3.5c-1.3-1.5-2.3-3.2-3-5.2c-0.6-2-1-4.2-1-6.6v-26h7.6V124
-	c0,1.9,0.2,3.5,0.6,4.8c0.4,1.3,1,2.4,1.8,3.3s1.7,1.5,2.8,1.9c1.1,0.4,2.3,0.6,3.6,0.6s2.5-0.2,3.6-0.6c1.1-0.4,2-1,2.8-1.9
-	c0.8-0.8,1.4-1.9,1.8-3.3c0.4-1.3,0.6-2.9,0.6-4.8V98.8h7.6v26c0,2.4-0.3,4.6-1,6.6c-0.7,2-1.7,3.7-3,5.2c-1.3,1.5-3.1,2.6-5.1,3.5
-	C340,140.8,337.5,141.2,334.7,141.2z"/>
-<path d="M380.2,141.2c-2.2,0-4.4-0.1-6.7-0.2c-2.2-0.1-4.2-0.4-5.9-0.9V99.8c0.9-0.2,1.8-0.3,2.8-0.4c1-0.1,2-0.3,3.1-0.3
-	c1-0.1,2.1-0.1,3.1-0.2c1,0,2-0.1,2.9-0.1c2.5,0,4.9,0.2,7,0.6c2.2,0.4,4,1,5.6,1.9c1.6,0.9,2.8,2,3.7,3.5c0.9,1.4,1.3,3.2,1.3,5.3
-	c0,2-0.5,3.6-1.4,5c-0.9,1.4-2.3,2.5-3.9,3.4c2.5,0.8,4.4,2.1,5.6,3.7c1.2,1.6,1.8,3.7,1.8,6.2c0,4.3-1.6,7.5-4.7,9.6
-	C391.4,140.2,386.6,141.2,380.2,141.2z M375.1,116.1h5.3c2.9,0,5.1-0.4,6.8-1.2c1.6-0.8,2.5-2.3,2.5-4.4c0-2-0.9-3.4-2.5-4.3
-	c-1.7-0.8-3.9-1.3-6.6-1.3c-1.2,0-2.2,0-3.1,0.1s-1.7,0.1-2.3,0.2V116.1z M375.1,122v12.7c0.8,0.1,1.7,0.1,2.7,0.2
-	c1,0,1.8,0.1,2.6,0.1c1.6,0,3-0.1,4.4-0.3c1.3-0.2,2.5-0.5,3.5-1c1-0.5,1.8-1.2,2.3-2c0.6-0.8,0.9-1.9,0.9-3.2c0-2.4-0.9-4-2.6-4.9
-	c-1.7-0.9-4.1-1.4-7.1-1.4H375.1z"/>
-<path d="M413.9,140.8V99.2h26.7v6.4h-19.1v10.3h17v6.3h-17v12.2H442v6.4H413.9z"/>
-<path d="M468.5,99c6,0,10.6,1.1,13.8,3.3c3.2,2.2,4.8,5.6,4.8,10.1c0,5.6-2.8,9.5-8.3,11.5c0.8,0.9,1.6,2,2.6,3.4
-	c1,1.3,1.9,2.8,2.9,4.3c1,1.5,2,3.1,2.9,4.7c0.9,1.6,1.7,3.2,2.5,4.8h-8.5c-0.8-1.4-1.6-2.9-2.5-4.3c-0.9-1.5-1.8-2.9-2.7-4.3
-	c-0.9-1.4-1.8-2.7-2.7-3.9c-0.9-1.2-1.7-2.3-2.5-3.3c-0.6,0-1,0.1-1.4,0.1c-0.4,0-0.8,0-1.1,0h-3.7V141h-7.6v-41
-	c1.8-0.4,3.8-0.7,5.9-0.8C465,99.1,466.9,99,468.5,99z M469.1,105.5c-1.6,0-3.1,0.1-4.4,0.2v13.4h3.3c1.8,0,3.5-0.1,4.9-0.3
-	c1.4-0.2,2.6-0.6,3.5-1.1c0.9-0.5,1.7-1.2,2.1-2.1c0.5-0.9,0.7-2,0.7-3.4c0-1.3-0.2-2.4-0.7-3.2c-0.5-0.9-1.2-1.6-2.1-2.1
-	c-0.9-0.5-2-0.9-3.2-1.1S470.5,105.5,469.1,105.5z"/>
-<path d="M513.9,99.2c0.9,1.5,1.9,3.4,2.9,5.6c1.1,2.2,2.2,4.5,3.3,6.9c1.1,2.4,2.2,4.9,3.3,7.4c1.1,2.5,2.1,4.8,3,6.9
-	c0.9-2.1,1.9-4.4,3-6.9c1.1-2.5,2.2-5,3.3-7.4c1.1-2.4,2.2-4.7,3.3-6.9c1.1-2.2,2.1-4.1,2.9-5.6h6.8c0.4,3,0.7,6.3,1,9.7
-	c0.3,3.4,0.6,6.9,0.9,10.5c0.3,3.6,0.5,7.2,0.7,10.8s0.4,7.2,0.6,10.6h-7.4c-0.2-4.8-0.4-9.8-0.6-14.9c-0.2-5.1-0.6-10.1-1-14.9
-	c-0.4,0.9-1,2-1.6,3.3s-1.2,2.7-1.9,4.2c-0.7,1.5-1.4,3-2.1,4.6c-0.7,1.6-1.4,3.2-2,4.7c-0.7,1.5-1.3,2.9-1.8,4.2s-1,2.4-1.4,3.3
-	h-5.6c-0.4-0.9-0.9-2-1.4-3.4c-0.6-1.3-1.2-2.7-1.8-4.2s-1.3-3-2-4.6c-0.7-1.6-1.4-3.1-2.1-4.6c-0.7-1.5-1.3-2.9-1.9-4.2
-	c-0.6-1.3-1.1-2.4-1.6-3.3c-0.4,4.8-0.8,9.8-1,14.9c-0.2,5.1-0.4,10.1-0.6,14.9h-7.4c0.2-3.4,0.4-7,0.6-10.7c0.2-3.7,0.5-7.3,0.7-11
-	s0.6-7.1,0.9-10.5c0.3-3.4,0.7-6.5,1-9.4H513.9z"/>
-<path d="M592.3,140.8c-0.6-1.6-1.1-3.3-1.7-4.9c-0.6-1.6-1.2-3.2-1.7-4.9h-17.6c-0.6,1.7-1.1,3.3-1.7,5s-1.2,3.2-1.7,4.8H560
-	c1.6-4.4,3-8.5,4.4-12.3s2.8-7.3,4.1-10.7c1.3-3.4,2.7-6.6,4-9.6c1.3-3,2.7-6,4.1-9h7.2c1.4,3,2.8,6,4.1,9c1.3,3,2.6,6.2,4,9.6
-	c1.3,3.4,2.7,6.9,4.1,10.7c1.4,3.8,2.9,7.9,4.5,12.3H592.3z M580,107.3c-0.9,2.1-2,4.7-3.2,7.7s-2.4,6.3-3.7,9.9h13.7
-	c-1.3-3.6-2.5-6.9-3.7-10C582,111.9,580.9,109.3,580,107.3z"/>
-<path d="M638.2,99.2v6.5h-12.8v35h-7.6v-35H605v-6.5H638.2z"/>
-<path d="M650.8,99.2h7.6v41.6h-7.6V99.2z"/>
-<path d="M693.4,141.7c-3.1,0-5.9-0.5-8.4-1.4s-4.6-2.4-6.3-4.2c-1.7-1.9-3-4.1-4-6.8c-0.9-2.7-1.4-5.8-1.4-9.2
-	c0-3.5,0.5-6.6,1.6-9.2c1.1-2.7,2.5-5,4.4-6.8c1.8-1.9,4-3.3,6.5-4.2c2.5-1,5.1-1.4,8-1.4c1.7,0,3.3,0.1,4.7,0.4
-	c1.4,0.3,2.6,0.6,3.7,0.9c1,0.3,1.9,0.7,2.6,1c0.7,0.4,1.2,0.6,1.4,0.8l-2.2,6.2c-1-0.6-2.3-1.2-4.1-1.7c-1.8-0.5-3.7-0.8-5.8-0.8
-	c-1.8,0-3.5,0.3-5,0.9c-1.6,0.6-2.9,1.5-4,2.8c-1.1,1.2-2,2.8-2.7,4.7c-0.6,1.9-1,4.1-1,6.5c0,2.2,0.3,4.2,0.8,6.1
-	c0.5,1.8,1.3,3.4,2.3,4.8c1,1.3,2.4,2.4,4,3.1c1.6,0.7,3.5,1.1,5.8,1.1c2.7,0,4.9-0.3,6.5-0.8s3-1,3.9-1.5l2,6.2
-	c-0.5,0.3-1.1,0.6-1.9,1c-0.8,0.3-1.8,0.6-2.9,0.9c-1.1,0.3-2.4,0.5-3.8,0.7C696.5,141.6,695,141.7,693.4,141.7z"/>
 </svg>
diff --git a/tools/make/lint.mk b/tools/make/lint.mk
index 54880be6d..a02bb1139 100644
--- a/tools/make/lint.mk
+++ b/tools/make/lint.mk
@@ -94,4 +94,9 @@ latest-release-check: ## Check if latest release and tag are created properly.
 
 .PHONY: lint.markdown
 lint.markdown:
-	markdownlint -c .github/markdown_lint_config.json site/content/*
\ No newline at end of file
+	markdownlint -c .github/markdown_lint_config.json site/content/*
+
+.PHONY: lint.dependabot
+lint: lint.dependabot
+lint.dependabot: ## Check if dependabot configuration is valid
+	@npx @bugron/validate-dependabot-yaml .github/dependabot.yml


Test patch:
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index 497c6ff48..2f0e223d6 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -55,7 +55,7 @@ jobs:
     - name: Run Coverage Tests
       run: make go.test.coverage
     - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3  # v5.3.1
+      uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574  # v5.4.0
       with:
         fail_ci_if_error: true
         files: ./coverage.xml
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-distinct-match-type.in.yaml
similarity index 100%
rename from internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.in.yaml
rename to internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-distinct-match-type.in.yaml
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-distinct-match-type.out.yaml
similarity index 82%
rename from internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml
rename to internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-distinct-match-type.out.yaml
index c4952e3cb..2ada61ce5 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-invalid-match-type.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-distinct-match-type.out.yaml
@@ -43,9 +43,9 @@ backendTrafficPolicies:
         namespace: envoy-gateway
       conditions:
       - lastTransitionTime: null
-        message: 'RateLimit: local rateLimit does not support distinct HeaderMatch.'
-        reason: Invalid
-        status: "False"
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
         type: Accepted
       controllerName: gateway.envoyproxy.io/gatewayclass-controller
 gateways:
@@ -175,8 +175,6 @@ xdsIR:
             name: httproute/default/httproute-1/rule/0/backend/0
             protocol: HTTP
             weight: 1
-        directResponse:
-          statusCode: 500
         hostname: gateway.envoyproxy.io
         isHTTP2: false
         metadata:
@@ -188,6 +186,38 @@ xdsIR:
           distinct: false
           name: ""
           prefix: /
+        traffic:
+          rateLimit:
+            local:
+              default:
+                requests: 4294967295
+                unit: Second
+              rules:
+              - headerMatches:
+                - distinct: false
+                  exact: one
+                  name: x-user-id
+                - distinct: true
+                  name: x-org-id
+                limit:
+                  requests: 10
+                  unit: Hour
+              - cidrMatch:
+                  cidr: 192.168.0.0/16
+                  distinct: false
+                  ip: 192.168.0.0
+                  isIPv6: false
+                  maskLen: 16
+                headerMatches:
+                - distinct: false
+                  exact: two
+                  name: x-user-id
+                - distinct: false
+                  exact: bar
+                  name: x-org-id
+                limit:
+                  requests: 10
+                  unit: Minute
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing-weighted-clusters.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing-weighted-clusters.yaml
new file mode 100644
index 000000000..4e1138342
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing-weighted-clusters.yaml
@@ -0,0 +1,32 @@
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  routes:
+  - name: "first-route"
+    hostname: "*"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 8080
+          zone: us-east-1a
+        - host: "2.3.4.5"
+          port: 8080
+          zone: us-east-1b
+        name: "first-route-dest/backend/0"
+        weight: 30
+        zoneAwareRoutingEnabled: true
+      - endpoints:
+        - host: "3.4.5.6"
+          port: 9090
+          zone: us-east-1c
+        - host: "4.5.6.7"
+          port: 9090
+          zone: us-east-1d
+        name: "first-route-dest/backend/1"
+        weight: 70
+        zoneAwareRoutingEnabled: false
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing.yaml
new file mode 100644
index 000000000..1500304bb
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-zonal-routing.yaml
@@ -0,0 +1,22 @@
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  routes:
+  - name: "first-route"
+    hostname: "*"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 8080
+          zone: us-east-1a
+        - host: "2.3.4.5"
+          port: 8080
+          zone: us-east-1b
+        name: "first-route-dest/backend/0"
+        weight: 30
+        zoneAwareRoutingEnabled: true
diff --git a/internal/xds/translator/testdata/in/xds-ir/local-ratelimit-distinct.yaml b/internal/xds/translator/testdata/in/xds-ir/local-ratelimit-distinct.yaml
new file mode 100644
index 000000000..aece51968
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/local-ratelimit-distinct.yaml
@@ -0,0 +1,70 @@
+http:
+- name: "first-listener"
+  address: "::"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route-ratelimit-distinct-ip"
+    hostname: "*"
+    traffic:
+      rateLimit:
+        local:
+          default:
+            requests: 10
+            unit: Minute
+          rules:
+          - headerMatches:
+            - name: "x-user-id"
+              distinct: true
+            limit:
+              requests: 5
+              unit: second
+    pathMatch:
+      exact: "foo/bar"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+  - name: "second-route-ratelimit-multiple-rules"
+    hostname: "*"
+    traffic:
+      rateLimit:
+        local:
+          default:
+            requests: 10
+            unit: Minute
+          rules:
+          - headerMatches:
+            - name: x-user-id
+              exact: one
+            - name: x-org-id
+              exact: foo
+            limit:
+              requests: 10
+              unit: Hour
+          - cidrMatch:
+              cidr: 192.168.0.0/16
+              maskLen: 16
+              distinct: true
+            headerMatches:
+            - name: x-user-id
+              exact: two
+            - name: x-org-id
+              exact: bar
+            limit:
+              requests: 10
+              unit: Minute
+    pathMatch:
+      exact: "example"
+    destination:
+      name: "second-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.clusters.yaml
new file mode 100644
index 000000000..7f0d1201c
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.clusters.yaml
@@ -0,0 +1,34 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    zoneAwareLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest/backend/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: first-route-dest/backend/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest/backend/1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: first-route-dest/backend/1
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.endpoints.yaml
new file mode 100644
index 000000000..c6c23b8c1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.endpoints.yaml
@@ -0,0 +1,38 @@
+- clusterName: first-route-dest/backend/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 8080
+      loadBalancingWeight: 1
+    locality:
+      zone: us-east-1a
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.3.4.5
+            portValue: 8080
+      loadBalancingWeight: 1
+    locality:
+      zone: us-east-1b
+- clusterName: first-route-dest/backend/1
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 3.4.5.6
+            portValue: 9090
+      loadBalancingWeight: 1
+    - endpoint:
+        address:
+          socketAddress:
+            address: 4.5.6.7
+            portValue: 9090
+      loadBalancingWeight: 1
+    loadBalancingWeight: 70
+    locality:
+      region: first-route-dest/backend/1
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.listeners.yaml
new file mode 100644
index 000000000..ec43e34f3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.listeners.yaml
@@ -0,0 +1,32 @@
+- 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.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        normalizePath: true
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.routes.yaml
new file mode 100644
index 000000000..d2d5e3aef
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing-weighted-clusters.routes.yaml
@@ -0,0 +1,20 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: first-route
+      route:
+        clusterNotFoundResponseCode: INTERNAL_SERVER_ERROR
+        upgradeConfigs:
+        - upgradeType: websocket
+        weightedClusters:
+          clusters:
+          - name: first-route-dest/backend/0
+            weight: 30
+          - name: first-route-dest/backend/1
+            weight: 70
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.clusters.yaml
new file mode 100644
index 000000000..d78191adf
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.clusters.yaml
@@ -0,0 +1,17 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    zoneAwareLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: first-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.endpoints.yaml
new file mode 100644
index 000000000..4d20294a0
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.endpoints.yaml
@@ -0,0 +1,20 @@
+- clusterName: first-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 8080
+      loadBalancingWeight: 1
+    locality:
+      zone: us-east-1a
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.3.4.5
+            portValue: 8080
+      loadBalancingWeight: 1
+    locality:
+      zone: us-east-1b
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.listeners.yaml
new file mode 100644
index 000000000..ec43e34f3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.listeners.yaml
@@ -0,0 +1,32 @@
+- 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.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        normalizePath: true
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.routes.yaml
new file mode 100644
index 000000000..0b5b4bee7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-zonal-routing.routes.yaml
@@ -0,0 +1,14 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: first-route
+      route:
+        cluster: first-route-dest
+        upgradeConfigs:
+        - upgradeType: websocket
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.clusters.yaml
new file mode 100644
index 000000000..ba595752d
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.clusters.yaml
@@ -0,0 +1,34 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: first-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: second-route-dest
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: second-route-dest
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.endpoints.yaml
new file mode 100644
index 000000000..997ca24ad
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.endpoints.yaml
@@ -0,0 +1,22 @@
+- clusterName: first-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality: {}
+- clusterName: second-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.listeners.yaml
new file mode 100644
index 000000000..4f2355a56
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.listeners.yaml
@@ -0,0 +1,39 @@
+- 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.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
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
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
new file mode 100644
index 000000000..02f71fc1d
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit-distinct.routes.yaml
@@ -0,0 +1,123 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        path: foo/bar
+      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:
+        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
+            tokenBucket:
+              fillInterval: 0s
+              maxTokens: 5
+              tokensPerFill: 5
+          filterEnabled:
+            defaultValue:
+              numerator: 100
+          filterEnforced:
+            defaultValue:
+              numerator: 100
+          statPrefix: http_local_rate_limiter
+          tokenBucket:
+            fillInterval: 60s
+            maxTokens: 10
+            tokensPerFill: 10
+    - match:
+        path: example
+      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:
+        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
+            - key: rule-0-match-1
+              value: rule-0-match-1
+            tokenBucket:
+              fillInterval: 3600s
+              maxTokens: 10
+              tokensPerFill: 10
+          - entries:
+            - key: rule-1-match-0
+              value: rule-1-match-0
+            - key: rule-1-match-1
+              value: rule-1-match-1
+            - key: masked_remote_address
+              value: 192.168.0.0/16
+            - key: remote_address
+            tokenBucket:
+              fillInterval: 60s
+              maxTokens: 10
+              tokensPerFill: 10
+          filterEnabled:
+            defaultValue:
+              numerator: 100
+          filterEnforced:
+            defaultValue:
+              numerator: 100
+          statPrefix: http_local_rate_limiter
+          tokenBucket:
+            fillInterval: 60s
+            maxTokens: 10
+            tokensPerFill: 10
diff --git a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml
index aff3d89b5..4f2355a56 100644
--- a/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/local-ratelimit.listeners.yaml
@@ -17,6 +17,7 @@
         - 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.router
           typedConfig:
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 729a011ee..1134cf454 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -2251,7 +2251,7 @@ _Appears in:_
 | ----- | ----------- |
 | `Exact` | HeaderMatchExact matches the exact value of the Value field against the value of<br />the specified HTTP Header.<br /> | 
 | `RegularExpression` | HeaderMatchRegularExpression matches a regular expression against the value of the<br />specified HTTP Header. The regex string must adhere to the syntax documented in<br />https://github.com/google/re2/wiki/Syntax.<br /> | 
-| `Distinct` | HeaderMatchDistinct matches any and all possible unique values encountered in the<br />specified HTTP Header. Note that each unique value will receive its own rate limit<br />bucket.<br />Note: This is only supported for Global Rate Limits.<br /> | 
+| `Distinct` | HeaderMatchDistinct matches any and all possible unique values encountered in the<br />specified HTTP Header. Note that each unique value will receive its own rate limit<br />bucket.<br /> | 
 
 
 #### HeaderSettings
@@ -4151,7 +4151,7 @@ _Appears in:_
 | Value | Description |
 | ----- | ----------- |
 | `Exact` | SourceMatchExact All IP Addresses within the specified Source IP CIDR are treated as a single client selector<br />and share the same rate limit bucket.<br /> | 
-| `Distinct` | SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector<br />and uses a separate rate limit bucket/counter.<br />Note: This is only supported for Global Rate Limits.<br /> | 
+| `Distinct` | SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector<br />and uses a separate rate limit bucket/counter.<br /> | 
 
 
 #### StatefulTLSSessionResumption
diff --git a/test/e2e/testdata/local-ratelimit-distinct-cidr.yaml b/test/e2e/testdata/local-ratelimit-distinct-cidr.yaml
new file mode 100644
index 000000000..1469f0f23
--- /dev/null
+++ b/test/e2e/testdata/local-ratelimit-distinct-cidr.yaml
@@ -0,0 +1,102 @@
+# This is a client traffic policy that enables client IP detection using the XFF header.
+# So, the client IP can be detected from the XFF header and used for rate limiting.
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: ClientTrafficPolicy
+metadata:
+  name: enable-client-ip-detection-trusted-cidr
+  namespace: gateway-conformance-infra
+spec:
+  clientIPDetection:
+    xForwardedFor:
+      numTrustedHops: 1
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: Gateway
+      name: same-namespace
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-distinct-cidr
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-ratelimit-distinct-cidr
+  rateLimit:
+    type: Local
+    local:
+      rules:
+        - limit:
+            requests: 10
+            unit: Hour
+        - clientSelectors:
+            - sourceCIDR:
+                value: 0.0.0.0/0
+                type: Distinct
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-distinct-cidr-and-exact-header
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-ratelimit-distinct-cidr-and-exact-header
+  rateLimit:
+    type: Local
+    local:
+      rules:
+        - limit:
+            requests: 10
+            unit: Hour
+        - clientSelectors:
+            - sourceCIDR:
+                value: 0.0.0.0/0
+                type: Distinct
+            - headers:
+                - name: x-org-id
+                  value: foo
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-distinct-cidr
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+      matches:
+        - path:
+            type: Exact
+            value: /ratelimit-distinct-cidr
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-distinct-cidr-and-exact-header
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+      matches:
+        - path:
+            type: Exact
+            value: /ratelimit-distinct-cidr-and-exact-header
diff --git a/test/e2e/testdata/local-ratelimit-distinct-header.yaml b/test/e2e/testdata/local-ratelimit-distinct-header.yaml
new file mode 100644
index 000000000..47ffee00a
--- /dev/null
+++ b/test/e2e/testdata/local-ratelimit-distinct-header.yaml
@@ -0,0 +1,85 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-distinct-header
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit-distinct-header
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - limit:
+          requests: 10
+          unit: Hour
+      - clientSelectors:
+        - headers:
+          - name: x-user-id
+            type: Distinct
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-distinct-header-and-exact-header
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit-distinct-header-and-exact-header
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - limit:
+          requests: 10
+          unit: Hour
+      - clientSelectors:
+        - headers:
+          - name: x-user-id
+            type: Distinct
+          - name: x-org-id
+            value: foo
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-distinct-header
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  rules:
+  - backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+    matches:
+    - path:
+        type: Exact
+        value: /ratelimit-distinct-header
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-distinct-header-and-exact-header
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  rules:
+  - backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+    matches:
+    - path:
+        type: Exact
+        value: /ratelimit-distinct-header-and-exact-header
diff --git a/test/e2e/tests/local_ratelimit_distinct_cidr.go b/test/e2e/tests/local_ratelimit_distinct_cidr.go
new file mode 100644
index 000000000..b82650fd6
--- /dev/null
+++ b/test/e2e/tests/local_ratelimit_distinct_cidr.go
@@ -0,0 +1,136 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, LocalRateLimitDistinctCIDRTest)
+}
+
+var LocalRateLimitDistinctCIDRTest = suite.ConformanceTest{
+	ShortName:   "LocalRateLimitDistinctCIDR",
+	Description: "Test that local rate limit filter works with distinct cidr",
+	Manifests:   []string{"testdata/local-ratelimit-distinct-cidr.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "http-ratelimit-distinct-cidr", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+		t.Run("requests with x-forwarded-for header should be limited per IP", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-cidr", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-cidr"
+			testDistinctCIDRRatelimit(t, "192.168.1.1", "", ns, gwAddr, path, true, suite)
+			testDistinctCIDRRatelimit(t, "192.168.1.2", "", ns, gwAddr, path, true, suite)
+		})
+
+		t.Run("requests with x-forwarded-for header and matching x-org-id header should be limited per IP", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-cidr-and-exact-header", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-cidr-and-exact-header"
+			testDistinctCIDRRatelimit(t, "192.168.1.1", "foo", ns, gwAddr, path, true, suite)
+			testDistinctCIDRRatelimit(t, "192.168.1.2", "foo", ns, gwAddr, path, true, suite)
+		})
+
+		t.Run("requests with with x-forwarded-for header but no matching x-org-id header should not be limited", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-cidr-and-exact-header", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-cidr-and-exact-header"
+			testDistinctCIDRRatelimit(t, "192.168.1.1", "bar", ns, gwAddr, path, false, suite)
+		})
+	},
+}
+
+func testDistinctCIDRRatelimit(t *testing.T, clientIP, org, ns, gwAddr, path string, limited bool, suite *suite.ConformanceTestSuite) {
+	expectOkResp := http.ExpectedResponse{
+		Request: http.Request{
+			Path: path,
+			Headers: map[string]string{
+				"X-Forwarded-For": clientIP,
+				"x-org-id":        org,
+			},
+		},
+		ExpectedRequest: &http.ExpectedRequest{
+			Request: http.Request{
+				Path:    path,
+				Headers: nil, // don't check headers since Envoy will append the client IP to the X-Forwarded-For header
+			},
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+
+	expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+	expectLimitResp := http.ExpectedResponse{
+		Request: http.Request{
+			Path: path,
+			Headers: map[string]string{
+				"X-Forwarded-For": clientIP,
+				"x-org-id":        org,
+			},
+		},
+		Response: http.Response{
+			StatusCode: 429,
+		},
+		Namespace: ns,
+	}
+	expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http")
+
+	// should just send exactly 4 requests, and expect 429
+
+	// keep sending requests till get 200 first, that will cost one 200
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+	// fire the rest request
+	if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+		t.Errorf("fail to get expected response at first three request: %v", err)
+	}
+
+	if limited {
+		// this request should be limited because the limit is 3
+		if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
+			t.Errorf("fail to get expected response at the fourth request: %v", err)
+		}
+	} else {
+		if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectOkResp); err != nil {
+			t.Errorf("fail to get expected response at the fourth request: %v", err)
+		}
+	}
+}
diff --git a/test/e2e/tests/local_ratelimit_distinct_header.go b/test/e2e/tests/local_ratelimit_distinct_header.go
new file mode 100644
index 000000000..2c032c655
--- /dev/null
+++ b/test/e2e/tests/local_ratelimit_distinct_header.go
@@ -0,0 +1,130 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, LocalRateLimitDistinctHeaderTest)
+}
+
+var LocalRateLimitDistinctHeaderTest = suite.ConformanceTest{
+	ShortName:   "LocalRateLimitDistinctHeader",
+	Description: "Test that local rate limit filter works with distinct header",
+	Manifests:   []string{"testdata/local-ratelimit-distinct-header.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "http-ratelimit-distinct-header", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+		t.Run("requests with x-user-id header should be limited per user", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-header", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-header"
+			testDistinctHeaderRatelimit(t, "john", "", ns, gwAddr, path, true, suite)
+			testDistinctHeaderRatelimit(t, "alice", "", ns, gwAddr, path, true, suite)
+		})
+
+		t.Run("requests with x-user-id header and matching x-org-id header should be limited per user", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-header-and-exact-header", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-header-and-exact-header"
+			testDistinctHeaderRatelimit(t, "john", "foo", ns, gwAddr, path, true, suite)
+			testDistinctHeaderRatelimit(t, "alice", "foo", ns, gwAddr, path, true, suite)
+		})
+
+		t.Run("requests with x-user-id header but no matching x-org-id header should not be limited", func(t *testing.T) {
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-distinct-header-and-exact-header", Namespace: ns}, suite.ControllerName, ancestorRef)
+			path := "/ratelimit-distinct-header-and-exact-header"
+			testDistinctHeaderRatelimit(t, "john", "bar", ns, gwAddr, path, false, suite)
+		})
+	},
+}
+
+func testDistinctHeaderRatelimit(t *testing.T, user, org, ns, gwAddr, path string, limited bool, suite *suite.ConformanceTestSuite) {
+	expectOkResp := http.ExpectedResponse{
+		Request: http.Request{
+			Path: path,
+			Headers: map[string]string{
+				"x-user-id": user,
+				"x-org-id":  org,
+			},
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+
+	expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+	expectLimitResp := http.ExpectedResponse{
+		Request: http.Request{
+			Path: path,
+			Headers: map[string]string{
+				"x-user-id": user,
+				"x-org-id":  org,
+			},
+		},
+		Response: http.Response{
+			StatusCode: 429,
+		},
+		Namespace: ns,
+	}
+	expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http")
+
+	// should just send exactly 4 requests, and expect 429
+
+	// keep sending requests till get 200 first, that will cost one 200
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+	// fire the rest request
+	if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+		t.Errorf("fail to get expected response at first three request: %v", err)
+	}
+
+	if limited {
+		// this request should be limited because the limit is 3
+		if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
+			t.Errorf("fail to get expected response at the fourth request: %v", err)
+		}
+	} else {
+		if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectOkResp); err != nil {
+			t.Errorf("fail to get expected response at the fourth request: %v", err)
+		}
+	}
+}