Repository: envoyproxy/gateway

Issue:
Expired service account projected token
*Description*:
Envoy bootstrap configuration should automatically re-read the service account token file when the token is renewed.  
Currently, once the token expires, Envoy continues using the old one and cannot recover without manual intervention.

*Repro steps*:
1. Deploy Envoy Gateway in **GatewayNamespace** mode.  
2. Run any gateway instance.  
3. Wait ~1 hour until the service account token expires.  
4. Delete the Envoy Gateway controller Pod(s).  
5. Observe that the Envoy proxy fails to reconnect to the control plane. The logs contain:  
```the token is not a service account```
(from [tokenreview.go#L49](https://github.com/envoyproxy/gateway/blob/3e5d1aa9bf453dcb8064a451e6913b9fe9799bd4/internal/xds/server/kubejwt/tokenreview.go#L49))  

At this point, Envoy is still using the expired token.  
Instead of surfacing the correct error, the implementation ignores the `error` section of the response, and the token response contains a `nil` value in the `Groups` field.

*Note*:
A possible solution is to configure Envoy to watch the token file and reload it automatically, e.g. by using:  
```yaml
watched_directory:
  path: /var/run/secrets/token
```
(as referenced [here](https://github.com/envoyproxy/gateway/blob/320df80ee6a75d739d29db93b79307c794a85c67/internal/xds/bootstrap/bootstrap.yaml.tpl#L259))

*Environment*:
v1.5.0

Discussion:
@arkodg: cc @cnvergence @anasinnyk: I found [comment](https://github.com/envoyproxy/envoy/blob/v1.35.3/api/envoy/config/core/v3/base.proto#L525-L527) on envoy codebase about WatchedDirectory. My solution don't fix this issue :(@cnvergence: thanks @anasinnyk, I will check up on this issue@anasinnyk: https://github.com/anasinnyk/gateway/commit/6fe952ca5ef6ea994293511c8429bdfcffd42d87 - fix it with those changes. I'm not good at envoy configuration, but changes re-read token file for me.

<img width="3430" height="1334" alt="Image" src="https://github.com/user-attachments/assets/0fe8a806-4076-41de-8960-4cb18c9e6752" />
with projected expiration time 600s

Solution patch:
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index 4728272ba..516626c98 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -86,6 +86,17 @@ jobs:
         verbose: true
         use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}
 
+  go-benchmark-test:
+    runs-on: ubuntu-latest
+    needs:
+    - changes
+    if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.run_test_workflow == 'true' }}
+    steps:
+    - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
+    - uses: ./tools/github-actions/setup-deps
+    - name: Run Go Benchmark Tests
+      run: make go-benchmark
+
   build:
     runs-on: ubuntu-latest
     needs: [changes, lint, gen-check, license-check, coverage-test]
diff --git a/internal/gatewayapi/status/policy.go b/internal/gatewayapi/status/policy.go
index f96f44c47..87bbbfce1 100644
--- a/internal/gatewayapi/status/policy.go
+++ b/internal/gatewayapi/status/policy.go
@@ -9,7 +9,6 @@ import (
 	"sort"
 	"time"
 
-	gocmp "github.com/google/go-cmp/cmp"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
@@ -48,7 +47,7 @@ func SetAcceptedForPolicyAncestors(policyStatus *gwapiv1a2.PolicyStatus, ancesto
 func setAcceptedForPolicyAncestor(policyStatus *gwapiv1a2.PolicyStatus, ancestorRef gwapiv1a2.ParentReference, controllerName string, generation int64) {
 	// Return early if Accepted condition is already set for specific ancestor.
 	for _, ancestor := range policyStatus.Ancestors {
-		if string(ancestor.ControllerName) == controllerName && gocmp.Equal(ancestor.AncestorRef, ancestorRef) {
+		if string(ancestor.ControllerName) == controllerName && ancestorRefsEqual(ancestor.AncestorRef, ancestorRef) {
 			for _, c := range ancestor.Conditions {
 				if c.Type == string(gwapiv1a2.PolicyConditionAccepted) {
 					return
@@ -77,17 +76,29 @@ func SetConditionForPolicyAncestor(policyStatus *gwapiv1a2.PolicyStatus, ancesto
 		policyStatus.Ancestors = []gwapiv1a2.PolicyAncestorStatus{}
 	}
 
-	cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), generation)
-
-	// Add condition for exist PolicyAncestorStatus.
+	// Find existing ancestor first
 	for i, ancestor := range policyStatus.Ancestors {
-		if string(ancestor.ControllerName) == controllerName && gocmp.Equal(ancestor.AncestorRef, ancestorRef) {
+		if string(ancestor.ControllerName) == controllerName && ancestorRefsEqual(ancestor.AncestorRef, ancestorRef) {
+			// if condition already exists and is unchanged, exit early
+			for _, existingCond := range ancestor.Conditions {
+				if existingCond.Type == string(conditionType) &&
+					existingCond.Status == status &&
+					existingCond.Reason == string(reason) &&
+					existingCond.Message == message &&
+					existingCond.ObservedGeneration == generation {
+					return
+				}
+			}
+
+			// Only create condition and merge if needed
+			cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), generation)
 			policyStatus.Ancestors[i].Conditions = MergeConditions(policyStatus.Ancestors[i].Conditions, cond)
 			return
 		}
 	}
 
-	// Add condition for new PolicyAncestorStatus.
+	// Add condition for new PolicyAncestorStatus
+	cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), generation)
 	policyStatus.Ancestors = append(policyStatus.Ancestors, gwapiv1a2.PolicyAncestorStatus{
 		AncestorRef:    ancestorRef,
 		ControllerName: gwapiv1a2.GatewayController(controllerName),
@@ -95,6 +106,47 @@ func SetConditionForPolicyAncestor(policyStatus *gwapiv1a2.PolicyStatus, ancesto
 	})
 }
 
+func ancestorRefsEqual(a, b gwapiv1a2.ParentReference) bool {
+	// Compare non-pointer fields first (fastest)
+	if a.Name != b.Name {
+		return false
+	}
+
+	// Compare Group pointers
+	if (a.Group == nil) != (b.Group == nil) {
+		return false
+	}
+	if a.Group != nil && *a.Group != *b.Group {
+		return false
+	}
+
+	// Compare Kind pointers
+	if (a.Kind == nil) != (b.Kind == nil) {
+		return false
+	}
+	if a.Kind != nil && *a.Kind != *b.Kind {
+		return false
+	}
+
+	// Compare Namespace pointers
+	if (a.Namespace == nil) != (b.Namespace == nil) {
+		return false
+	}
+	if a.Namespace != nil && *a.Namespace != *b.Namespace {
+		return false
+	}
+
+	// Compare SectionName pointers
+	if (a.SectionName == nil) != (b.SectionName == nil) {
+		return false
+	}
+	if a.SectionName != nil && *a.SectionName != *b.SectionName {
+		return false
+	}
+
+	return true
+}
+
 // TruncatePolicyAncestors trims PolicyStatus.Ancestors down to at most 16 entries.
 // The first 15 ancestors are shown as is.
 // The last 16th ancestor is shown as is and add Aggregated condition.
diff --git a/internal/infrastructure/common/proxy_jwt.go b/internal/infrastructure/common/proxy_jwt.go
new file mode 100644
index 000000000..edb3b0041
--- /dev/null
+++ b/internal/infrastructure/common/proxy_jwt.go
@@ -0,0 +1,20 @@
+// 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.
+
+package common
+
+import "fmt"
+
+const (
+	// #nosec G101 - This is a file path, not a credential
+	SdsServiceAccountTokenFilename = "xds_service_account_token.json"
+)
+
+func GetSdsServiceAccountTokenConfigMapData(tokenPath string) string {
+	return fmt.Sprintf(`{"resources":[{`+
+		`"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",`+
+		`"name":"jwt-sa-bearer",`+
+		`"generic_secret":{"secret":{"filename":"%s"}}}]}`, tokenPath)
+}
diff --git a/internal/infrastructure/kubernetes/proxy/resource.go b/internal/infrastructure/kubernetes/proxy/resource.go
index 285b5bb5d..96901a3ea 100644
--- a/internal/infrastructure/kubernetes/proxy/resource.go
+++ b/internal/infrastructure/kubernetes/proxy/resource.go
@@ -115,6 +115,10 @@ func expectedProxyContainers(infra *ir.ProxyInfra,
 		TopologyInjectorDisabled: topologyInjectorDisabled,
 	}
 
+	if gatewayNamespaceMode {
+		bootstrapConfigOptions.SdsConfig.ServiceAccountToken = filepath.Join("/sds", common.SdsServiceAccountTokenFilename)
+	}
+
 	args, err := common.BuildProxyArgs(infra, shutdownConfig, bootstrapConfigOptions, fmt.Sprintf("$(%s)", envoyPodEnvVar), gatewayNamespaceMode)
 	if err != nil {
 		return nil, err
@@ -400,6 +404,10 @@ func sdsConfigMapItems(gatewayNamespaceMode bool) []corev1.KeyToPath {
 				Key:  common.SdsCAFilename,
 				Path: common.SdsCAFilename,
 			},
+			{
+				Key:  common.SdsServiceAccountTokenFilename,
+				Path: common.SdsServiceAccountTokenFilename,
+			},
 		}
 	}
 
diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider.go b/internal/infrastructure/kubernetes/proxy/resource_provider.go
index a46f44dd6..29955fb57 100644
--- a/internal/infrastructure/kubernetes/proxy/resource_provider.go
+++ b/internal/infrastructure/kubernetes/proxy/resource_provider.go
@@ -34,6 +34,10 @@ import (
 )
 
 const (
+	// XdsServiceAccountTokenFilepath is the fully qualified path of the file containing
+	// the service account token used for authentication in GatewayNamespaceMode.
+	// #nosec G101 - This is a file path, not a credential
+	XdsServiceAccountTokenFilepath = "/var/run/secrets/token/sa-token"
 	// XdsTLSCertFilepath is the fully qualified path of the file containing Envoy's
 	// xDS server TLS certificate.
 	XdsTLSCertFilepath = "/certs/tls.crt"
@@ -319,6 +323,11 @@ func (r *ResourceRender) ConfigMap(cert string) (*corev1.ConfigMap, error) {
 		common.SdsCAFilename:   common.GetSdsCAConfigMapData(XdsTLSCaFilepath),
 		common.SdsCertFilename: common.GetSdsCertConfigMapData(XdsTLSCertFilepath, XdsTLSKeyFilepath),
 	}
+
+	if r.GatewayNamespaceMode {
+		data[common.SdsServiceAccountTokenFilename] = common.GetSdsServiceAccountTokenConfigMapData(XdsServiceAccountTokenFilepath)
+	}
+
 	if cert != "" {
 		data[XdsTLSCaFileName] = cert
 	}
diff --git a/internal/xds/bootstrap/bootstrap.go b/internal/xds/bootstrap/bootstrap.go
index 65d989a40..36845fcd8 100644
--- a/internal/xds/bootstrap/bootstrap.go
+++ b/internal/xds/bootstrap/bootstrap.go
@@ -45,6 +45,8 @@ const (
 
 	defaultSdsTrustedCAPath   = "/sds/xds-trusted-ca.json"
 	defaultSdsCertificatePath = "/sds/xds-certificate.json"
+	// #nosec G101 - This is a file path, not a credential
+	defaultServiceAccountTokenPath = "/sds/xds-service-account-token.json"
 
 	defaultServiceClusterName = "local_cluster"
 )
@@ -101,6 +103,8 @@ type bootstrapParameters struct {
 	ServiceClusterName string
 	// TopologyInjectorDisabled controls whether to render the local cluster for use with zone aware routing
 	TopologyInjectorDisabled bool
+	// ServiceAccountTokenPath is the path to the service account token file used for authentication in GatewayNamespaceMode.
+	ServiceAccountTokenPath string
 }
 
 type serverParameters struct {
@@ -149,11 +153,14 @@ type RenderBootstrapConfigOptions struct {
 	MaxHeapSizeBytes         uint64
 	GatewayNamespaceMode     bool
 	TopologyInjectorDisabled bool
+	ServiceAccountTokenPath  string
 }
 
 type SdsConfigPath struct {
 	Certificate string
 	TrustedCA   string
+	// ServiceAccountToken is the path to the service account token file used for authentication in GatewayNamespaceMode.
+	ServiceAccountToken string
 }
 
 // render the stringified bootstrap config in yaml format.
@@ -263,6 +270,7 @@ func GetRenderedBootstrapConfig(opts *RenderBootstrapConfigOptions) (string, err
 			PrometheusCompressionLibrary: prometheusCompressionLibrary,
 			OtelMetricSinks:              metricSinks,
 			ServiceClusterName:           defaultServiceClusterName,
+			ServiceAccountTokenPath:      defaultServiceAccountTokenPath,
 		},
 	}
 
@@ -306,6 +314,10 @@ func GetRenderedBootstrapConfig(opts *RenderBootstrapConfigOptions) (string, err
 			}
 		}
 		cfg.parameters.GatewayNamespaceMode = opts.GatewayNamespaceMode
+		if opts.GatewayNamespaceMode && len(opts.SdsConfig.ServiceAccountToken) > 0 {
+			cfg.parameters.ServiceAccountTokenPath = opts.SdsConfig.ServiceAccountToken
+		}
+
 		cfg.parameters.OverloadManager.MaxHeapSizeBytes = opts.MaxHeapSizeBytes
 		if opts.ServiceClusterName != nil {
 			cfg.parameters.ServiceClusterName = *opts.ServiceClusterName
diff --git a/internal/xds/bootstrap/bootstrap.yaml.tpl b/internal/xds/bootstrap/bootstrap.yaml.tpl
index e14a3d70b..c9db6499d 100644
--- a/internal/xds/bootstrap/bootstrap.yaml.tpl
+++ b/internal/xds/bootstrap/bootstrap.yaml.tpl
@@ -223,6 +223,10 @@ static_resources:
                 "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                 credential:
                   name: jwt-sa-bearer
+                  sds_config:
+                    path_config_source:
+                      path: {{ .ServiceAccountTokenPath }}
+                    resource_api_version: V3
             overwrite: true
         - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
           typed_config:
@@ -251,13 +255,6 @@ static_resources:
               path_config_source:
                 path: {{ .SdsTrustedCAPath }}
               resource_api_version: V3
-  {{- if .GatewayNamespaceMode }}
-  secrets:
-  - name: jwt-sa-bearer
-    generic_secret:
-      secret:
-        filename: "/var/run/secrets/token/sa-token"
-  {{- end }}
 overload_manager:
   refresh_interval: 0.25s
   resource_monitors:
diff --git a/internal/xds/server/kubejwt/tokenreview.go b/internal/xds/server/kubejwt/tokenreview.go
index 1f448550d..4bef90668 100644
--- a/internal/xds/server/kubejwt/tokenreview.go
+++ b/internal/xds/server/kubejwt/tokenreview.go
@@ -45,6 +45,10 @@ func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token, nodeID
 		return fmt.Errorf("failed to call TokenReview API to verify service account JWT: %w", err)
 	}
 
+	if tokenReview.Status.Error != "" {
+		return fmt.Errorf("token review found error: %s", tokenReview.Status.Error)
+	}
+
 	if !slices.Contains(tokenReview.Status.User.Groups, "system:serviceaccounts") {
 		return fmt.Errorf("the token is not a service account")
 	}
diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go
index 5cda9f063..9007fa7d5 100644
--- a/internal/xds/translator/route.go
+++ b/internal/xds/translator/route.go
@@ -71,7 +71,7 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute, httpListener *ir.HTTPListener) (*rou
 	case httpRoute.Redirect != nil:
 		router.Action = &routev3.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute)}
 	case httpRoute.URLRewrite != nil:
-		routeAction := buildXdsURLRewriteAction(httpRoute.Destination.Name, httpRoute.URLRewrite, httpRoute.PathMatch)
+		routeAction := buildXdsURLRewriteAction(httpRoute, httpRoute.URLRewrite, httpRoute.PathMatch)
 		routeAction.IdleTimeout = idleTimeout(httpRoute)
 		if httpRoute.Mirrors != nil {
 			routeAction.RequestMirrorPolicies = buildXdsRequestMirrorPolicies(httpRoute.Mirrors)
@@ -446,11 +446,18 @@ func prefix2RegexRewrite(prefix string) *matcherv3.RegexMatchAndSubstitute {
 	}
 }
 
-func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMatch *ir.StringMatch) *routev3.RouteAction {
-	routeAction := &routev3.RouteAction{
-		ClusterSpecifier: &routev3.RouteAction_Cluster{
-			Cluster: destName,
-		},
+func buildXdsURLRewriteAction(route *ir.HTTPRoute, urlRewrite *ir.URLRewrite, pathMatch *ir.StringMatch) *routev3.RouteAction {
+	backendWeights := route.Destination.ToBackendWeights()
+	// only use weighted cluster when there are invalid weights
+	var routeAction *routev3.RouteAction
+	if route.NeedsClusterPerSetting() || backendWeights.Invalid != 0 {
+		routeAction = buildXdsWeightedRouteAction(backendWeights, route.Destination.Settings)
+	} else {
+		routeAction = &routev3.RouteAction{
+			ClusterSpecifier: &routev3.RouteAction_Cluster{
+				Cluster: backendWeights.Name,
+			},
+		}
 	}
 
 	if urlRewrite.Path != nil {
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 4fec24f71..da978b9a0 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -26,6 +26,7 @@ bug fixes: |
   Fixed Lua validator log level to be suppressed by default.
   Fixed ProxyTopologyInjector cache sync race condition that caused injection failures
   Fixed validation for grpc routes with extension ref filters.
+  Fixed service account token handling in GatewayNamespaceMode to use SDS for properly refreshing expired token.
 
 # Enhancements that improve performance.
 performance improvements: |
diff --git a/tools/make/golang.mk b/tools/make/golang.mk
index b126e1b25..0060a3e25 100644
--- a/tools/make/golang.mk
+++ b/tools/make/golang.mk
@@ -81,6 +81,11 @@ go.test.cel: manifests # Run the CEL validation tests
          go test ./test/cel-validation --tags celvalidation -race || exit 1; \
     done
 
+.PHONY: go.test.benchmark
+go.test.benchmark: ## Run benchmark tests for translation performance
+	@$(LOG_TARGET)
+	go test -timeout=10m -run='^$$' -bench=. -benchmem -benchtime=1x -count=3 ./test/gobench
+
 .PHONY: go.clean
 go.clean: ## Clean the building output files
 	@$(LOG_TARGET)
@@ -139,3 +144,7 @@ clean: go.clean
 .PHONY: testdata
 testdata: ## Override the testdata with new configurations.
 testdata: go.testdata.complete
+
+.PHONY: go-benchmark
+go-benchmark: ## Run benchmark tests for translation performance.
+go-benchmark: go.test.benchmark



Test patch:
diff --git a/internal/infrastructure/kubernetes/proxy/testdata/configmap/gateway-namespace-mode.yaml b/internal/infrastructure/kubernetes/proxy/testdata/configmap/gateway-namespace-mode.yaml
index ee9b7a28c..09b6937f7 100644
--- a/internal/infrastructure/kubernetes/proxy/testdata/configmap/gateway-namespace-mode.yaml
+++ b/internal/infrastructure/kubernetes/proxy/testdata/configmap/gateway-namespace-mode.yaml
@@ -2,6 +2,7 @@ apiVersion: v1
 data:
   xds-certificate.json: '{"resources":[{"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret","name":"xds_certificate","tls_certificate":{"certificate_chain":{"filename":"/certs/tls.crt"},"private_key":{"filename":"/certs/tls.key"}}}]}'
   xds-trusted-ca.json: '{"resources":[{"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret","name":"xds_trusted_ca","validation_context":{"trusted_ca":{"filename":"/certs/ca.crt"},"match_typed_subject_alt_names":[{"san_type":"DNS","matcher":{"exact":"envoy-gateway"}}]}}]}'
+  xds_service_account_token.json: '{"resources":[{"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret","name":"jwt-sa-bearer","generic_secret":{"secret":{"filename":"/var/run/secrets/token/sa-token"}}}]}'
 kind: ConfigMap
 metadata:
   creationTimestamp: null
diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/gateway-namespace-mode.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/gateway-namespace-mode.yaml
index d9346bcd6..a8ef5bf67 100644
--- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/gateway-namespace-mode.yaml
+++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/gateway-namespace-mode.yaml
@@ -178,6 +178,10 @@ spec:
                           "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                           credential:
                             name: jwt-sa-bearer
+                            sds_config:
+                              path_config_source:
+                                path: /sds/xds_service_account_token.json
+                              resource_api_version: V3
                       overwrite: true
                   - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
                     typed_config:
@@ -197,11 +201,6 @@ spec:
                         path_config_source:
                           path: /sds/xds-trusted-ca.json
                         resource_api_version: V3
-            secrets:
-            - name: jwt-sa-bearer
-              generic_secret:
-                secret:
-                  filename: "/var/run/secrets/token/sa-token"
           overload_manager:
             refresh_interval: 0.25s
             resource_monitors:
@@ -403,6 +402,8 @@ spec:
           items:
           - key: xds-trusted-ca.json
             path: xds-trusted-ca.json
+          - key: xds_service_account_token.json
+            path: xds_service_account_token.json
           name: gateway-1
           optional: false
         name: sds
diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom-sa.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom-sa.yaml
index 2f68d8a3d..d99b81d06 100644
--- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom-sa.yaml
+++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom-sa.yaml
@@ -182,6 +182,10 @@ spec:
                           "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                           credential:
                             name: jwt-sa-bearer
+                            sds_config:
+                              path_config_source:
+                                path: /sds/xds_service_account_token.json
+                              resource_api_version: V3
                       overwrite: true
                   - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
                     typed_config:
@@ -201,11 +205,6 @@ spec:
                         path_config_source:
                           path: /sds/xds-trusted-ca.json
                         resource_api_version: V3
-            secrets:
-            - name: jwt-sa-bearer
-              generic_secret:
-                secret:
-                  filename: "/var/run/secrets/token/sa-token"
           overload_manager:
             refresh_interval: 0.25s
             resource_monitors:
@@ -407,6 +406,8 @@ spec:
           items:
           - key: xds-trusted-ca.json
             path: xds-trusted-ca.json
+          - key: xds_service_account_token.json
+            path: xds_service_account_token.json
           name: gateway-1
           optional: false
         name: sds
diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/gateway-namespace-mode.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/gateway-namespace-mode.yaml
index ac2ca511a..434cf7c59 100644
--- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/gateway-namespace-mode.yaml
+++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/gateway-namespace-mode.yaml
@@ -182,6 +182,10 @@ spec:
                           "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                           credential:
                             name: jwt-sa-bearer
+                            sds_config:
+                              path_config_source:
+                                path: /sds/xds_service_account_token.json
+                              resource_api_version: V3
                       overwrite: true
                   - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
                     typed_config:
@@ -201,11 +205,6 @@ spec:
                         path_config_source:
                           path: /sds/xds-trusted-ca.json
                         resource_api_version: V3
-            secrets:
-            - name: jwt-sa-bearer
-              generic_secret:
-                secret:
-                  filename: "/var/run/secrets/token/sa-token"
           overload_manager:
             refresh_interval: 0.25s
             resource_monitors:
@@ -407,6 +406,8 @@ spec:
           items:
           - key: xds-trusted-ca.json
             path: xds-trusted-ca.json
+          - key: xds_service_account_token.json
+            path: xds_service_account_token.json
           name: gateway-1
           optional: false
         name: sds
diff --git a/internal/infrastructure/kubernetes/proxy/testdata/gateway-namespace-mode/deployment.yaml b/internal/infrastructure/kubernetes/proxy/testdata/gateway-namespace-mode/deployment.yaml
index 5d2fa9b9d..f4c8c812d 100644
--- a/internal/infrastructure/kubernetes/proxy/testdata/gateway-namespace-mode/deployment.yaml
+++ b/internal/infrastructure/kubernetes/proxy/testdata/gateway-namespace-mode/deployment.yaml
@@ -182,6 +182,10 @@ spec:
                           "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                           credential:
                             name: jwt-sa-bearer
+                            sds_config:
+                              path_config_source:
+                                path: /sds/xds_service_account_token.json
+                              resource_api_version: V3
                       overwrite: true
                   - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
                     typed_config:
@@ -201,11 +205,6 @@ spec:
                         path_config_source:
                           path: /sds/xds-trusted-ca.json
                         resource_api_version: V3
-            secrets:
-            - name: jwt-sa-bearer
-              generic_secret:
-                secret:
-                  filename: "/var/run/secrets/token/sa-token"
           overload_manager:
             refresh_interval: 0.25s
             resource_monitors:
@@ -407,6 +406,8 @@ spec:
           items:
           - key: xds-trusted-ca.json
             path: xds-trusted-ca.json
+          - key: xds_service_account_token.json
+            path: xds_service_account_token.json
           name: gateway-1
           optional: false
         name: sds
@@ -596,6 +597,10 @@ spec:
                           "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
                           credential:
                             name: jwt-sa-bearer
+                            sds_config:
+                              path_config_source:
+                                path: /sds/xds_service_account_token.json
+                              resource_api_version: V3
                       overwrite: true
                   - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
                     typed_config:
@@ -615,11 +620,6 @@ spec:
                         path_config_source:
                           path: /sds/xds-trusted-ca.json
                         resource_api_version: V3
-            secrets:
-            - name: jwt-sa-bearer
-              generic_secret:
-                secret:
-                  filename: "/var/run/secrets/token/sa-token"
           overload_manager:
             refresh_interval: 0.25s
             resource_monitors:
@@ -821,6 +821,8 @@ spec:
           items:
           - key: xds-trusted-ca.json
             path: xds-trusted-ca.json
+          - key: xds_service_account_token.json
+            path: xds_service_account_token.json
           name: gateway-2
           optional: false
         name: sds
diff --git a/internal/infrastructure/kubernetes/proxy_configmap_test.go b/internal/infrastructure/kubernetes/proxy_configmap_test.go
index d35b2dc0f..3eba6373c 100644
--- a/internal/infrastructure/kubernetes/proxy_configmap_test.go
+++ b/internal/infrastructure/kubernetes/proxy_configmap_test.go
@@ -195,8 +195,9 @@ func TestCreateOrUpdateProxyConfigMap(t *testing.T) {
 					},
 				},
 				Data: map[string]string{
-					common.SdsCAFilename:   common.GetSdsCAConfigMapData(proxy.XdsTLSCaFilepath),
-					common.SdsCertFilename: common.GetSdsCertConfigMapData(proxy.XdsTLSCertFilepath, proxy.XdsTLSKeyFilepath),
+					common.SdsCAFilename:                  common.GetSdsCAConfigMapData(proxy.XdsTLSCaFilepath),
+					common.SdsCertFilename:                common.GetSdsCertConfigMapData(proxy.XdsTLSCertFilepath, proxy.XdsTLSKeyFilepath),
+					common.SdsServiceAccountTokenFilename: common.GetSdsServiceAccountTokenConfigMapData(proxy.XdsServiceAccountTokenFilepath),
 				},
 			},
 		},
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.yaml
new file mode 100644
index 000000000..5a7c27cf2
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.yaml
@@ -0,0 +1,68 @@
+http:
+  - name: "first-listener"
+    address: "::"
+    hostnames:
+      - '*'
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    port: 10080
+    routes:
+      - destination:
+          name: "first-route-dest"
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 1.1.1.1
+                  port: 8080
+              name: "first-route-dest/backend/0"
+              filters:
+                addRequestHeaders:
+                  - append: false
+                    name: add-header-3
+                    value:
+                      - some-value
+              protocol: HTTP
+              weight: 1
+        hostname: '*'
+        name: "first-route"
+        pathMatch:
+          prefix: /foo
+        urlRewrite:
+          path:
+            prefixMatchReplace: /
+      - destination:
+          name: "second-route-dest"
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 2.2.2.2
+                  port: 8080
+              name: "second-route-dest/backend/0"
+              filters:
+                addRequestHeaders:
+                  - append: true
+                    name: add-header-1
+                    value:
+                      - some-value
+                  - append: true
+                    name: add-header-2
+                    value:
+                      - some-value
+              protocol: HTTP
+              weight: 8
+            - addressType: IP
+              endpoints:
+                - host: 3.3.3.3
+                  port: 8080
+              filters: {}
+              protocol: HTTP
+              weight: 2
+              name: "second-route-dest/backend/1"
+        hostname: '*'
+        name: "second-route"
+        pathMatch:
+          prefix: /bar
+        urlRewrite:
+          path:
+            prefixMatchReplace: /
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.clusters.yaml
new file mode 100644
index 000000000..f4fa08ccb
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.clusters.yaml
@@ -0,0 +1,72 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest/backend/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: first-route-dest/backend/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: second-route-dest/backend/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: second-route-dest/backend/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: second-route-dest/backend/1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: second-route-dest/backend/1
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.endpoints.yaml
new file mode 100644
index 000000000..e0b91a520
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.endpoints.yaml
@@ -0,0 +1,36 @@
+- clusterName: first-route-dest/backend/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.1.1.1
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: first-route-dest/backend/0
+- clusterName: second-route-dest/backend/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.2.2.2
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 8
+    locality:
+      region: second-route-dest/backend/0
+- clusterName: second-route-dest/backend/1
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 3.3.3.3
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 2
+    locality:
+      region: second-route-dest/backend/1
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.listeners.yaml
new file mode 100644
index 000000000..5dd5e46e3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.listeners.yaml
@@ -0,0 +1,35 @@
+- 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
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  maxConnectionsToAcceptPerSocketEvent: 1
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.routes.yaml
new file mode 100644
index 000000000..22e035362
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix-with-weighted-backend.routes.yaml
@@ -0,0 +1,51 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        pathSeparatedPrefix: /foo
+      name: first-route
+      route:
+        clusterNotFoundResponseCode: INTERNAL_SERVER_ERROR
+        regexRewrite:
+          pattern:
+            regex: ^/foo\/*
+          substitution: /
+        upgradeConfigs:
+        - upgradeType: websocket
+        weightedClusters:
+          clusters:
+          - name: first-route-dest/backend/0
+            requestHeadersToAdd:
+            - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+              header:
+                key: add-header-3
+                value: some-value
+            weight: 1
+    - match:
+        pathSeparatedPrefix: /bar
+      name: second-route
+      route:
+        clusterNotFoundResponseCode: INTERNAL_SERVER_ERROR
+        regexRewrite:
+          pattern:
+            regex: ^/bar\/*
+          substitution: /
+        upgradeConfigs:
+        - upgradeType: websocket
+        weightedClusters:
+          clusters:
+          - name: second-route-dest/backend/0
+            requestHeadersToAdd:
+            - header:
+                key: add-header-1
+                value: some-value
+            - header:
+                key: add-header-2
+                value: some-value
+            weight: 8
+          - name: second-route-dest/backend/1
+            weight: 2
diff --git a/test/gobench/translate_test.go b/test/gobench/translate_test.go
index cc1abaac1..ee2410d68 100644
--- a/test/gobench/translate_test.go
+++ b/test/gobench/translate_test.go
@@ -313,17 +313,17 @@ func BenchmarkGatewayAPItoXDS(b *testing.B) {
 		yaml string
 	}
 	medium := baseYAML + backendYAML + tlsSecretYAML + clientTrafficPolicyYAML +
-		genHTTPRoutes(100) +
-		genGRPCRoutes(50) +
-		genUDPRoutes(20) +
-		genSecurityPolicies(100) +
-		genBackendTrafficPolicies(100)
+		genHTTPRoutes(50) +
+		genGRPCRoutes(25) +
+		genUDPRoutes(10) +
+		genSecurityPolicies(50) +
+		genBackendTrafficPolicies(50)
 	large := baseYAML + backendYAML + tlsSecretYAML + clientTrafficPolicyYAML +
-		genHTTPRoutes(1000) +
-		genGRPCRoutes(500) +
+		genHTTPRoutes(500) +
+		genGRPCRoutes(250) +
 		genUDPRoutes(100) +
-		genSecurityPolicies(1000) +
-		genBackendTrafficPolicies(1000)
+		genSecurityPolicies(500) +
+		genBackendTrafficPolicies(500)
 
 	cases := []benchCase{
 		{