Repository: envoyproxy/gateway
Pull request #6082 (2025-05-15 06:51:57)

e2e: fix SessionPersistence flaky
fixes: https://github.com/envoyproxy/gateway/issues/6062

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/6082?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 70.41%. Comparing base [(`3e1bce3`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/3e1bce315feba4b197f5c8ae520f029c210b51fd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`c4b6c9f`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/c4b6c9f383f4b212968fef511dbe00f8fd8a0c8e?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    #6082      +/-   ##
==========================================
- Coverage   70.43%   70.41%   -0.02%     
==========================================
  Files         217      217              
  Lines       36129    36129              
==========================================
- Hits        25447    25441       -6     
- Misses       9166     9168       +2     
- Partials     1516     1520       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/6082?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).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/OWNERS b/OWNERS
index bd29b3425..5e4a3b86e 100644
--- a/OWNERS
+++ b/OWNERS
@@ -15,6 +15,8 @@ maintainers:
 - zhaohuabing
 - guydc
 - shawnh2
+- cnvergence
+- rudrakhp
 
 emeritus-maintainers:
 
@@ -30,9 +32,7 @@ reviewers:
 
 - kflynn
 - tanujd11
-- cnvergence
 - liorokman
-- rudrakhp
 
 emeritus-reviewers:
 
diff --git a/api/v1alpha1/ext_proc_types.go b/api/v1alpha1/ext_proc_types.go
index 88da0123c..95893d3c6 100644
--- a/api/v1alpha1/ext_proc_types.go
+++ b/api/v1alpha1/ext_proc_types.go
@@ -9,7 +9,7 @@ import (
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 )
 
-// +kubebuilder:validation:Enum=Streamed;Buffered;BufferedPartial
+// +kubebuilder:validation:Enum=Streamed;Buffered;BufferedPartial;FullDuplexStreamed
 type ExtProcBodyProcessingMode string
 
 const (
diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go
index de05eb167..11fb22051 100644
--- a/api/v1alpha1/oidc_types.go
+++ b/api/v1alpha1/oidc_types.go
@@ -102,6 +102,16 @@ type OIDC struct {
 	// Note: this field is only applicable when the "refreshToken" field is set to true.
 	// +optional
 	DefaultRefreshTokenTTL *metav1.Duration `json:"defaultRefreshTokenTTL,omitempty"`
+
+	// Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
+	// explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
+	//
+	// The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
+	// redirects and wish to directly supply a token instead.
+	//
+	// If not specified, defaults to false.
+	// +optional
+	PassThroughAuthHeader *bool `json:"passThroughAuthHeader,omitempty"`
 }
 
 // OIDCProvider defines the OIDC Provider configuration.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 87c150497..322875c03 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -4419,6 +4419,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) {
 		*out = new(metav1.Duration)
 		**out = **in
 	}
+	if in.PassThroughAuthHeader != nil {
+		in, out := &in.PassThroughAuthHeader, &out.PassThroughAuthHeader
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC.
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
index 01cb6663b..7e8fa5d33 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
@@ -1003,6 +1003,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                         response:
@@ -1026,6 +1027,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                       type: object
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
index a63bf9162..5e4f67965 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -3626,6 +3626,16 @@ spec:
 
                       If not specified, uses a default logout path "/logout"
                     type: string
+                  passThroughAuthHeader:
+                    description: |-
+                      Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
+                      explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
+
+                      The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
+                      redirects and wish to directly supply a token instead.
+
+                      If not specified, defaults to false.
+                    type: boolean
                   provider:
                     description: The OIDC Provider configuration.
                     properties:
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
index fb4f8bc75..d49a6046d 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
@@ -1002,6 +1002,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                         response:
@@ -1025,6 +1026,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                       type: object
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
index da26002a0..accfa3aa2 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -3625,6 +3625,16 @@ spec:
 
                       If not specified, uses a default logout path "/logout"
                     type: string
+                  passThroughAuthHeader:
+                    description: |-
+                      Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
+                      explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
+
+                      The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
+                      redirects and wish to directly supply a token instead.
+
+                      If not specified, defaults to false.
+                    type: boolean
                   provider:
                     description: The OIDC Provider configuration.
                     properties:
diff --git a/examples/kubernetes/gateway-namespace-mode.yaml b/examples/kubernetes/gateway-namespace-mode.yaml
new file mode 100644
index 000000000..f7c8e1a83
--- /dev/null
+++ b/examples/kubernetes/gateway-namespace-mode.yaml
@@ -0,0 +1,192 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: GatewayClass
+metadata:
+  name: eg
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: team-a-backend
+  namespace: team-a
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: team-a-backend
+  namespace: team-a
+  labels:
+    app: team-a-backend
+    service: team-a-backend
+spec:
+  ports:
+    - name: http
+      port: 3000
+      targetPort: 3000
+  selector:
+    app: team-a-backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: team-a-backend
+  namespace: team-a
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: team-a-backend
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: team-a-backend
+        version: v1
+    spec:
+      serviceAccountName: team-a-backend
+      containers:
+        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
+          imagePullPolicy: IfNotPresent
+          name: team-a-backend
+          ports:
+            - containerPort: 3000
+          env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: team-b-backend
+  namespace: team-b
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: team-b-backend
+  namespace: team-b
+  labels:
+    app: team-b-backend
+    service: team-b-backend
+spec:
+  ports:
+    - name: http
+      port: 3000
+      targetPort: 3000
+  selector:
+    app: team-b-backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: team-b-backend
+  namespace: team-b
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: team-b-backend
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: team-b-backend
+        version: v1
+    spec:
+      serviceAccountName: team-b-backend
+      containers:
+        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
+          imagePullPolicy: IfNotPresent
+          name: team-b-backend
+          ports:
+            - containerPort: 3000
+          env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-a
+  namespace: team-a
+spec:
+  gatewayClassName: eg
+  listeners:
+    - allowedRoutes:
+        namespaces:
+          from: Same
+      name: http
+      port: 8080
+      protocol: HTTP
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-b
+  namespace: team-b
+spec:
+  gatewayClassName: eg
+  listeners:
+    - allowedRoutes:
+        namespaces:
+          from: Same
+      name: http
+      port: 8081
+      protocol: HTTP
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: team-a-route
+  namespace: team-a
+spec:
+  parentRefs:
+    - name: gateway-a
+  hostnames:
+    - "www.team-a.com"
+  rules:
+    - backendRefs:
+        - group: ""
+          kind: Service
+          name: team-a-backend
+          port: 3000
+          weight: 1
+      matches:
+        - path:
+            type: PathPrefix
+            value: /example
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: team-b-route
+  namespace: team-b
+spec:
+  parentRefs:
+    - name: gateway-b
+  hostnames:
+    - "www.team-b.com"
+  rules:
+    - backendRefs:
+        - group: ""
+          kind: Service
+          name: team-b-backend
+          port: 3000
+          weight: 1
+      matches:
+        - path:
+            type: PathPrefix
+            value: /example
diff --git a/go.mod b/go.mod
index 808c826c4..2ad40b462 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.24.2
 require (
 	fortio.org/fortio v1.69.4
 	fortio.org/log v1.17.2
-	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
 	github.com/Masterminds/semver/v3 v3.3.1
 	github.com/andybalholm/brotli v1.1.1
 	github.com/avast/retry-go v3.0.0+incompatible
@@ -111,6 +110,7 @@ require (
 	fortio.org/version v1.0.4 // indirect
 	github.com/4meepo/tagalign v1.4.2 // indirect
 	github.com/Abirdcfly/dupword v0.1.3 // indirect
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
 	github.com/Antonboom/errname v1.1.0 // indirect
 	github.com/Antonboom/nilnil v1.1.0 // indirect
 	github.com/Antonboom/testifylint v1.6.0 // indirect
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index e2dd18b2b..fce38a2e5 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -39,11 +39,12 @@ import (
 )
 
 const (
-	defaultRedirectURL        = "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback"
-	defaultRedirectPath       = "/oauth2/callback"
-	defaultLogoutPath         = "/logout"
-	defaultForwardAccessToken = false
-	defaultRefreshToken       = false
+	defaultRedirectURL           = "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback"
+	defaultRedirectPath          = "/oauth2/callback"
+	defaultLogoutPath            = "/logout"
+	defaultForwardAccessToken    = false
+	defaultRefreshToken          = false
+	defaultPassThroughAuthHeader = false
 
 	// nolint: gosec
 	oidcHMACSecretName = "envoy-oidc-hmac"
@@ -270,6 +271,27 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error {
 			return err
 		}
 	}
+
+	oidc := p.Spec.OIDC
+	jwt := p.Spec.JWT
+	if oidc != nil && oidc.PassThroughAuthHeader != nil && *oidc.PassThroughAuthHeader {
+		if jwt == nil {
+			return errors.New("the OIDC.PassThroughAuthHeader setting must be used in conjunction with JWT settings")
+		}
+
+		hasValidJwtExtractor := false
+		for _, provider := range jwt.Providers {
+			// When ExtractFrom is not specified it falls back to looking at the "Authorization: Bearer ..." header
+			if provider.ExtractFrom == nil || len(provider.ExtractFrom.Headers) > 0 {
+				hasValidJwtExtractor = true
+				break
+			}
+		}
+		if !hasValidJwtExtractor {
+			return errors.New("the OIDC.PassThroughAuthHeader setting must be used in conjunction with a JWT provider that is configured to read from a header")
+		}
+	}
+
 	return nil
 }
 
@@ -843,15 +865,16 @@ func (t *Translator) buildOIDC(
 	envoyProxy *egv1a1.EnvoyProxy,
 ) (*ir.OIDC, error) {
 	var (
-		oidc               = policy.Spec.OIDC
-		provider           *ir.OIDCProvider
-		clientSecret       *corev1.Secret
-		redirectURL        = defaultRedirectURL
-		redirectPath       = defaultRedirectPath
-		logoutPath         = defaultLogoutPath
-		forwardAccessToken = defaultForwardAccessToken
-		refreshToken       = defaultRefreshToken
-		err                error
+		oidc                  = policy.Spec.OIDC
+		provider              *ir.OIDCProvider
+		clientSecret          *corev1.Secret
+		redirectURL           = defaultRedirectURL
+		redirectPath          = defaultRedirectPath
+		logoutPath            = defaultLogoutPath
+		forwardAccessToken    = defaultForwardAccessToken
+		refreshToken          = defaultRefreshToken
+		passThroughAuthHeader = defaultPassThroughAuthHeader
+		err                   error
 	)
 
 	if provider, err = t.buildOIDCProvider(policy, resources, envoyProxy); err != nil {
@@ -896,6 +919,10 @@ func (t *Translator) buildOIDC(
 		refreshToken = *oidc.RefreshToken
 	}
 
+	if oidc.PassThroughAuthHeader != nil {
+		passThroughAuthHeader = *oidc.PassThroughAuthHeader
+	}
+
 	// Generate a unique cookie suffix for oauth filters.
 	// This is to avoid cookie name collision when multiple security policies are applied
 	// to the same route.
@@ -933,6 +960,7 @@ func (t *Translator) buildOIDC(
 		CookieNameOverrides:    policy.Spec.OIDC.CookieNames,
 		CookieDomain:           policy.Spec.OIDC.CookieDomain,
 		HMACSecret:             hmacData,
+		PassThroughAuthHeader:  passThroughAuthHeader,
 	}, nil
 }
 
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 30bd1d1fc..b3759e45f 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -1094,6 +1094,11 @@ type OIDC struct {
 
 	// CookieDomain sets the domain of the cookies set by the oauth filter.
 	CookieDomain *string `json:"cookieDomain,omitempty"`
+
+	// Skips OIDC authentication when the request contains any header that will be extracted by the JWT
+	// filter, normally "Authorization: Bearer ...". This is typically used for non-browser clients that
+	// may not be able to handle OIDC redirects and wish to directly supply a token instead.
+	PassThroughAuthHeader bool `json:"passThroughAuthHeader,omitempty"`
 }
 
 // OIDCProvider defines the schema for the OIDC Provider.
diff --git a/internal/utils/helm/package.go b/internal/utils/helm/package.go
index 68d769a4b..11a2af4da 100644
--- a/internal/utils/helm/package.go
+++ b/internal/utils/helm/package.go
@@ -66,12 +66,14 @@ type PackageTool struct {
 	logger Printer
 }
 
-func NewPackageTool() *PackageTool {
+func NewPackageTool(valueFiles ...string) *PackageTool {
 	return &PackageTool{
 		envSettings:  cli.New(),
 		actionConfig: &action.Configuration{},
 		chartName:    "oci://docker.io/envoyproxy/gateway-helm",
-		valuesOpts:   &values.Options{},
+		valuesOpts: &values.Options{
+			ValueFiles: valueFiles,
+		},
 	}
 }
 
@@ -256,6 +258,7 @@ func (pt *PackageTool) RunInstall(opts *PackageOptions) error {
 		return err
 	}
 
+	pt.logger.Println(fmt.Sprintf("Install with values: %v", egChartValues))
 	pt.setInstallOptions(opts)
 	release, err := pt.actionInstall.Run(egChart, egChartValues)
 	if err != nil {
diff --git a/internal/wasm/premissioncache.go b/internal/wasm/premissioncache.go
index 053b3880c..e680574ad 100644
--- a/internal/wasm/premissioncache.go
+++ b/internal/wasm/premissioncache.go
@@ -71,9 +71,6 @@ type permissionCache struct {
 
 	cache  map[string]*permissionCacheEntry
 	logger logging.Logger
-
-	// For tests
-	triggerChan chan struct{}
 }
 
 // permissionCacheEntry is an entry in the permission cache.
@@ -128,7 +125,6 @@ func newPermissionCache(options permissionCacheOptions, logger logging.Logger) *
 		cache:                  make(map[string]*permissionCacheEntry),
 		permissionCacheOptions: options,
 		logger:                 logger,
-		triggerChan:            make(chan struct{}),
 	}
 }
 
@@ -148,11 +144,48 @@ func (p *permissionCache) Start(ctx context.Context) {
 		defer ticker.Stop()
 		for {
 			select {
-			// explicitly trigger a check for tests
-			case <-p.triggerChan:
-				p.checkPermissions(ctx)
 			case <-ticker.C:
-				p.checkPermissions(ctx)
+				func() {
+					p.Lock()
+					defer p.Unlock()
+					for _, e := range p.cache {
+						if e.isCacheExpired(p.cacheExpiry) {
+							p.logger.Info("removing permission cache entry", "image", e.image.String())
+							delete(p.cache, e.key())
+							continue
+						}
+						if e.isPermissionExpired(p.permissionExpiry) {
+							const retryAttempts = 3
+							const retryDelay = 1 * time.Second
+							p.logger.Info("rechecking permission for image", "image", e.image.String())
+							err := retry.Do(
+								func() error {
+									err := p.checkAndUpdatePermission(ctx, e)
+									if err != nil && isRetriableError(err) {
+										p.logger.Error(
+											err,
+											"failed to check permission for image, will retry again",
+											"image",
+											e.image.String())
+										return err
+									}
+									return nil
+								},
+								retry.Attempts(retryAttempts),
+								retry.DelayType(retry.BackOffDelay),
+								retry.Delay(retryDelay),
+								retry.Context(ctx),
+							)
+							if err != nil {
+								p.logger.Error(
+									err,
+									fmt.Sprintf("failed to recheck permission for image after %d attempts", retryAttempts),
+									"image",
+									e.image.String())
+							}
+						}
+					}
+				}()
 			case <-ctx.Done():
 				return
 			}
@@ -160,48 +193,6 @@ func (p *permissionCache) Start(ctx context.Context) {
 	}()
 }
 
-func (p *permissionCache) checkPermissions(ctx context.Context) {
-	p.Lock()
-	defer p.Unlock()
-	for _, e := range p.cache {
-		if e.isCacheExpired(p.cacheExpiry) {
-			p.logger.Info("removing permission cache entry", "image", e.image.String())
-			delete(p.cache, e.key())
-			continue
-		}
-		if e.isPermissionExpired(p.permissionExpiry) {
-			const retryAttempts = 3
-			const retryDelay = 1 * time.Second
-			p.logger.Info("rechecking permission for image", "image", e.image.String())
-			err := retry.Do(
-				func() error {
-					err := p.checkAndUpdatePermission(ctx, e)
-					if err != nil && isRetriableError(err) {
-						p.logger.Error(
-							err,
-							"failed to check permission for image, will retry again",
-							"image",
-							e.image.String())
-						return err
-					}
-					return nil
-				},
-				retry.Attempts(retryAttempts),
-				retry.DelayType(retry.BackOffDelay),
-				retry.Delay(retryDelay),
-				retry.Context(ctx),
-			)
-			if err != nil {
-				p.logger.Error(
-					err,
-					fmt.Sprintf("failed to recheck permission for image after %d attempts", retryAttempts),
-					"image",
-					e.image.String())
-			}
-		}
-	}
-}
-
 // isRetriableError checks if the error is retriable.
 // If the error is a permission error, it's not retriable. For example, 401 and 403 HTTP status code.
 func isRetriableError(err error) bool {
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index 60527be07..b5082361e 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -60,7 +60,7 @@ func (*oidc) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListe
 			continue
 		}
 
-		filter, err := buildHCMOAuth2Filter(route.Security.OIDC)
+		filter, err := buildHCMOAuth2Filter(route.Security)
 		if err != nil {
 			errs = errors.Join(errs, err)
 			continue
@@ -73,8 +73,8 @@ func (*oidc) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListe
 }
 
 // buildHCMOAuth2Filter returns an OAuth2 HTTP filter from the provided IR HTTPRoute.
-func buildHCMOAuth2Filter(oidc *ir.OIDC) (*hcmv3.HttpFilter, error) {
-	oauth2Proto, err := oauth2Config(oidc)
+func buildHCMOAuth2Filter(securityFeatures *ir.SecurityFeatures) (*hcmv3.HttpFilter, error) {
+	oauth2Proto, err := oauth2Config(securityFeatures)
 	if err != nil {
 		return nil, err
 	}
@@ -85,7 +85,7 @@ func buildHCMOAuth2Filter(oidc *ir.OIDC) (*hcmv3.HttpFilter, error) {
 	}
 
 	return &hcmv3.HttpFilter{
-		Name:     oauth2FilterName(oidc),
+		Name:     oauth2FilterName(securityFeatures.OIDC),
 		Disabled: true,
 		ConfigType: &hcmv3.HttpFilter_TypedConfig{
 			TypedConfig: OAuth2Any,
@@ -97,12 +97,14 @@ func oauth2FilterName(oidc *ir.OIDC) string {
 	return perRouteFilterName(egv1a1.EnvoyFilterOAuth2, oidc.Name)
 }
 
-func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
+func oauth2Config(securityFeatures *ir.SecurityFeatures) (*oauth2v3.OAuth2, error) {
 	var (
 		tokenEndpointCluster string
 		err                  error
 	)
 
+	oidc := securityFeatures.OIDC
+
 	if oidc.Provider.Destination != nil && len(oidc.Provider.Destination.Settings) > 0 {
 		tokenEndpointCluster = oidc.Provider.Destination.Name
 	} else {
@@ -221,9 +223,48 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
 		}
 		oauth2.Config.RetryPolicy = rp
 	}
+
+	if oidc.PassThroughAuthHeader {
+		oauth2.Config.PassThroughMatcher = buildHeaderMatchers(securityFeatures.JWT)
+	}
+
 	return oauth2, nil
 }
 
+func buildHeaderMatchers(jwt *ir.JWT) []*routev3.HeaderMatcher {
+	// Bypass OIDC if a header that will be handled by JWT is passed.
+	passThroughMatchers := make([]*routev3.HeaderMatcher, 0, len(jwt.Providers))
+
+	for _, provider := range jwt.Providers {
+		if provider.ExtractFrom == nil {
+			// If extractFrom is not specified, it adds "Authorization: Bearer ..." as a default
+			stringMatcher := matcherv3.StringMatcher{MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: "Bearer "}}
+			headerMatcher := routev3.HeaderMatcher{
+				Name:                 "Authorization",
+				HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{StringMatch: &stringMatcher},
+			}
+			passThroughMatchers = append(passThroughMatchers, &headerMatcher)
+		} else {
+			// Any matching header will be bypassed (JWT effectively OR's them).
+			for _, extractHeader := range provider.ExtractFrom.Headers {
+				if extractHeader.ValuePrefix == nil {
+					headerMatcher := routev3.HeaderMatcher{Name: extractHeader.Name}
+					passThroughMatchers = append(passThroughMatchers, &headerMatcher)
+				} else {
+					stringMatcher := matcherv3.StringMatcher{MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: *extractHeader.ValuePrefix}}
+					headerMatcher := routev3.HeaderMatcher{
+						Name:                 extractHeader.Name,
+						HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{StringMatch: &stringMatcher},
+					}
+					passThroughMatchers = append(passThroughMatchers, &headerMatcher)
+				}
+			}
+		}
+	}
+
+	return passThroughMatchers
+}
+
 func buildNonRouteRetryPolicy(rr *ir.Retry) (*corev3.RetryPolicy, error) {
 	rp := &corev3.RetryPolicy{
 		RetryOn: retryDefaultRetryOn,
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 3271a4f90..818799641 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -8,6 +8,8 @@ security updates: |
 
 # New features or capabilities added in this release.
 new features: |
+  Added support for percentage-based request mirroring
+  Add an option to OIDC authentication to bypass it and defer to JWT when the request contains an "Authorization: Bearer ..." header.
 
 bug fixes: |
 
diff --git a/site/content/en/contributions/CODEOWNERS.md b/site/content/en/contributions/CODEOWNERS.md
index 071532f02..e5d6f0af6 100644
--- a/site/content/en/contributions/CODEOWNERS.md
+++ b/site/content/en/contributions/CODEOWNERS.md
@@ -6,10 +6,12 @@ description: "This section includes Maintainers of Envoy Gateway."
 ## The following maintainers, listed in alphabetical order, own everything
 
 - @arkodg
-- @qicz
 - @Xunzhuo
 - @zirain
 - @zhaohuabing
+- @guydc
+- @cnvergence
+- @rudrakhp
 
 ## Emeritus Maintainers
 
diff --git a/site/content/en/v1.4/api/extension_types.md b/site/content/en/v1.4/api/extension_types.md
index 4858d5b3d..9441bf08d 100644
--- a/site/content/en/v1.4/api/extension_types.md
+++ b/site/content/en/v1.4/api/extension_types.md
@@ -3146,6 +3146,7 @@ _Appears in:_
 | `defaultTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ |  false  |  | DefaultTokenTTL is the default lifetime of the id token and access token.<br />Please note that Envoy will always use the expiry time from the response<br />of the authorization server if it is provided. This field is only used when<br />the expiry time is not provided by the authorization.<br />If not specified, defaults to 0. In this case, the "expires_in" field in<br />the authorization response must be set by the authorization server, or the<br />OAuth flow will fail. |
 | `refreshToken` | _boolean_ |  false  |  | RefreshToken indicates whether the Envoy should automatically refresh the<br />id token and access token when they expire.<br />When set to true, the Envoy will use the refresh token to get a new id token<br />and access token when they expire.<br />If not specified, defaults to false. |
 | `defaultRefreshTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ |  false  |  | DefaultRefreshTokenTTL is the default lifetime of the refresh token.<br />This field is only used when the exp (expiration time) claim is omitted in<br />the refresh token or the refresh token is not JWT.<br />If not specified, defaults to 604800s (one week).<br />Note: this field is only applicable when the "refreshToken" field is set to true. |
+| `passThroughAuthHeader` | _boolean_ |  false  |  | Skips OIDC authentication when the request contains any header that will be extracted by the JWT<br />filter, normally "Authorization: Bearer ...". This is typically used for non-browser clients that<br />may not be able to handle OIDC redirects and wish to directly supply a token instead.<br /><br />If not specified, defaults to false. |
 
 
 #### OIDCCookieNames
diff --git a/site/content/en/v1.4/tasks/operations/deployment-mode.md b/site/content/en/v1.4/tasks/operations/deployment-mode.md
index c50d65b28..725f7c3cb 100644
--- a/site/content/en/v1.4/tasks/operations/deployment-mode.md
+++ b/site/content/en/v1.4/tasks/operations/deployment-mode.md
@@ -4,18 +4,22 @@ title: "Deployment Mode"
 ## Deployment modes
 
 ### One GatewayClass per Envoy Gateway Controller
+
 * An Envoy Gateway is associated with a single [GatewayClass][] resource under one controller.
 This is the simplest deployment mode and is suitable for scenarios where each Gateway needs to have its own dedicated set of resources and configurations.
 
 ### Multiple GatewayClasses per Envoy Gateway Controller
+
 * An Envoy Gateway is associated with multiple [GatewayClass][] resources under one controller.
 * Support for accepting multiple GatewayClasses was added [here][issue1231].
 
 ### Separate Envoy Gateway Controllers
+
 If you've instantiated multiple GatewayClasses, you can also run separate Envoy Gateway controllers in different namespaces, linking a GatewayClass to each of them for multi-tenancy.
 Please follow the example [Multi-tenancy](#multi-tenancy).
 
 ### Merged Gateways onto a single EnvoyProxy fleet
+
 By default, each Gateway has its own dedicated set of Envoy Proxy and its configurations.
 However, for some deployments, it may be more convenient to merge listeners across multiple Gateways and deploy a single Envoy Proxy fleet.
 
@@ -26,6 +30,14 @@ Setting the `mergeGateways` field in the EnvoyProxy resource linked to GatewayCl
 
 Please follow the example [Merged gateways deployment](#merged-gateways-deployment).
 
+### Gateway Namespace Mode
+
+Gateway Namespace Mode is a deployment model for Envoy Gateway that creates Envoy Proxy infrastructure resources like Deployments, Services and ServiceAccounts in the namespace where each Gateway resource is defined, rather than in the Envoy Gateway controller namespace.
+
+* Support for this deployment mode was added [here][issue2629].
+
+Please follow the example [Gateway Namespace Mode][].
+
 ### Supported Modes
 
 #### Kubernetes
@@ -34,7 +46,6 @@ Please follow the example [Merged gateways deployment](#merged-gateways-deployme
 and **creates** managed data plane resources such as EnvoyProxy `Deployment` in the **namespace where Envoy Gateway is running**.
 * Envoy Gateway also supports [Namespaced deployment mode][], you can watch resources in the specific namespaces by assigning
 `EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector` and **creates** managed data plane resources in the **namespace where Envoy Gateway is running**.
-* Support for alternate deployment modes is being tracked [here][issue1117].
 
 ### Multi-tenancy
 
@@ -1068,5 +1079,6 @@ curl --header "Host: www.merged3.com" http://$GATEWAY_HOST:8082/example3
 [EnvoyProxy]: ../../api/extension_types#envoyproxy
 [GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/
 [Namespaced deployment mode]: ../../api/extension_types#kuberneteswatchmode
+[Gateway Namespace Mode]: ./gateway-namespace-mode
 [issue1231]: https://github.com/envoyproxy/gateway/issues/1231
-[issue1117]: https://github.com/envoyproxy/gateway/issues/1117
+[issue2629]: https://github.com/envoyproxy/gateway/issues/2629
diff --git a/site/content/en/v1.4/tasks/operations/gateway-namespace-mode.md b/site/content/en/v1.4/tasks/operations/gateway-namespace-mode.md
new file mode 100644
index 000000000..ad7f3cd51
--- /dev/null
+++ b/site/content/en/v1.4/tasks/operations/gateway-namespace-mode.md
@@ -0,0 +1,270 @@
+---
+title: "Gateway Namespace Mode"
+---
+
+{{% alert title="Notice" color="warning" %}}
+
+Gateway Namespace Mode is currently an **alpha** feature. We recommend against using it in production workloads until it reaches beta status.
+
+For status updates or to provide feedback, please follow our [GitHub issues](https://github.com/envoyproxy/gateway/issues).
+
+{{% /alert %}}
+
+## Overview
+
+In standard deployment mode, Envoy Gateway creates all data plane resources in the controller namespace (typically `envoy-gateway-system`).
+
+Gateway Namespace Mode changes this behavior by placing Envoy Proxy data plane resources like Deployments, Services and ServiceAccounts in each Gateway's namespace, providing stronger isolation and multi-tenancy.
+
+Traditional deployment mode uses mTLS where both the client and server authenticate each other. However, in Gateway Namespace Mode, we've shifted to server-side TLS and JWT token validation between infra and control-plane.
+
+* Only the CA certificate is available in pods running in Gateway namespaces
+* Client certificates are not mounted in these namespaces
+* The Envoy proxy still validates server certificates using the CA certificate
+
+Gateway Namespace Mode uses projected service account JWT tokens for authentication.
+* Use short-lived, audience-specific JWT tokens. These tokens are automatically mounted into pods via the projected volume mechanism
+* JWT validation ensures that only authorized proxies can connect to the xDS server
+
+{{% alert title="Note" color="warning" %}}
+
+Currently it is not supported to run Gateway Namespace Mode with Merged Gateways deployments.
+
+{{% /alert %}}
+
+## Configuration
+
+To enable Gateway Namespace Mode, configure the `provider.kubernetes.deploy.type` field in your Envoy Gateway Helm chart values:
+
+```yaml
+config:
+  envoyGateway:
+    provider:
+      type: Kubernetes
+      kubernetes:
+        deploy:
+          type: GatewayNamespace
+```
+
+You can also use this command to install Envoy Gateway with Gateway Namespace Mode using Helm:
+
+```shell
+helm install \
+  --set config.envoyGateway.provider.kubernetes.deploy.type=GatewayNamespace \
+  eg oci://docker.io/envoyproxy/gateway-helm \
+  --version {{< helm-version >}} -n envoy-gateway-system --create-namespace
+```
+
+### RBAC configuration
+
+When using Gateway Namespace Mode, Envoy Gateway needs additional RBAC permissions to create and manage resources across different namespaces. The following RBAC resources are automatically created when installing Envoy Gateway Helm Chart with Gateway Namespace Mode enabled.
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: gateway-helm-cluster-infra-manager
+rules:
+- apiGroups: [""]
+  resources: ["serviceaccounts", "services", "configmaps"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["apps"]
+  resources: ["deployments", "daemonsets"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["autoscaling", "policy"]
+  resources: ["horizontalpodautoscalers", "poddisruptionbudgets"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["authentication.k8s.io"]
+  resources: ["tokenreviews"]
+  verbs: ["create"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gateway-helm-cluster-infra-manager
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: 'gateway-helm-cluster-infra-manager'
+subjects:
+- kind: ServiceAccount
+  name: 'envoy-gateway'
+  namespace: 'envoy-gateway-system'
+```
+
+Envoy Gateway also supports configuration to you only watch resources in the specific namespaces by assigning
+`EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector`.
+In this case, when you specify this configuration with Gateway Namespace Mode,Envoy Gateway will only watch for Gateway API resources in the specified namespaces and create needed Roles for infrastructure management in the specified namespaces.
+
+## Testing
+
+The following example demonstrates deploying two Gateways in different namespaces `team-a` and `team-b`.
+
+### Create test namespaces
+
+```shell
+kubectl create namespace team-a
+kubectl create namespace team-b
+```
+
+### Deploy Gateway Namespace Mode Example
+
+Deploy resources on your cluster from the example, it will create two sets of backend deployments, Gateways and their respective HTTPRoutes in the previously created namespaces `team-a` and `team-b`.
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/{{< yaml-version >}}/examples/kubernetes/gateway-namespace-mode.yaml
+```
+
+Verify that Gateways are deployed and programmed
+
+```shell
+kubectl get gateways -n team-a
+
+NAME        CLASS   ADDRESS        PROGRAMMED   AGE
+gateway-a   eg      172.18.0.200   True         67s
+```
+
+```shell
+kubectl get gateways -n team-b
+
+NAME        CLASS   ADDRESS        PROGRAMMED   AGE
+gateway-b   eg      172.18.0.201   True         67s
+```
+
+Verify that HTTPRoutes are deployed
+
+```shell
+kubectl get httproute -n team-a
+
+NAME           HOSTNAMES            AGE
+team-a-route   ["www.team-a.com"]   67s
+```
+
+```shell
+kubectl get httproute -n team-b
+
+NAME           HOSTNAMES            AGE
+team-b-route   ["www.team-b.com"]   67s
+```
+
+Envoy Proxy resources should be created now in the namespace of every Gateway.
+
+```shell
+kubectl get pods -n team-a
+
+NAME                                              READY   STATUS    RESTARTS   AGE
+envoy-team-a-gateway-a-b65c6264-d56f5d989-6dv5s   2/2     Running   0          65s
+team-a-backend-6f786fb76f-nx26p                   1/1     Running   0          65s
+```
+
+```shell
+kubectl get pods -n team-b
+
+NAME                                               READY   STATUS    RESTARTS   AGE
+envoy-team-b-gateway-b-0ac91f5a-74f445884f-95pl8   2/2     Running   0          87s
+team-b-backend-966b5f47c-zxngl                     1/1     Running   0          87s
+```
+
+```shell
+kubectl get services -n team-a
+
+NAME                              TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
+envoy-team-a-gateway-a-b65c6264   LoadBalancer   10.96.191.198   172.18.0.200   8080:30999/TCP   3m2s
+team-a-backend                    ClusterIP      10.96.92.226    <none>         3000/TCP         3m2s
+```
+
+```shell
+kubectl get services -n team-b
+
+NAME                              TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
+envoy-team-b-gateway-b-0ac91f5a   LoadBalancer   10.96.144.13   172.18.0.201   8081:31683/TCP   3m43s
+team-b-backend                    ClusterIP      10.96.26.162   <none>         3000/TCP         3m43s
+```
+
+### Testing the Configuration
+
+Fetch external IPs of the services:
+
+```shell
+export GATEWAY_HOST_A=$(kubectl get gateway/gateway-a -n team-a -o jsonpath='{.status.addresses[0].value}')
+```
+
+```shell
+export GATEWAY_HOST_B=$(kubectl get gateway/gateway-b -n team-b -o jsonpath='{.status.addresses[0].value}')
+```
+
+Curl the route team-a-route through Envoy proxy:
+
+```shell
+curl --header "Host: www.team-a.com" http://$GATEWAY_HOST_A:8080/example
+```
+
+```shell
+{
+ "path": "/example",
+ "host": "www.team-a.com",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/8.7.1"
+  ],
+  "X-Envoy-External-Address": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-For": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-Proto": [
+   "http"
+  ],
+  "X-Request-Id": [
+   "52f08a5c-7e07-43b7-bd23-44693c60fc0c"
+  ]
+ },
+ "namespace": "team-a",
+ "ingress": "",
+ "service": "",
+ "pod": "team-a-backend-6f786fb76f-nx26p"
+```
+
+Curl the route team-b-route through Envoy proxy:
+
+```shell
+curl --header "Host: www.team-b.com" http://$GATEWAY_HOST_B:8081/example
+```
+
+```shell
+{
+ "path": "/example",
+ "host": "www.team-b.com",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/8.7.1"
+  ],
+  "X-Envoy-External-Address": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-For": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-Proto": [
+   "http"
+  ],
+  "X-Request-Id": [
+   "62a06bd7-4754-475b-854a-dca3fc159e93"
+  ]
+ },
+ "namespace": "team-b",
+ "ingress": "",
+ "service": "",
+ "pod": "team-b-backend-966b5f47c-d6jwj"
+```
diff --git a/site/content/en/v1.4/tasks/traffic/client-traffic-policy.md b/site/content/en/v1.4/tasks/traffic/client-traffic-policy.md
index 987bab510..cb9748160 100644
--- a/site/content/en/v1.4/tasks/traffic/client-traffic-policy.md
+++ b/site/content/en/v1.4/tasks/traffic/client-traffic-policy.md
@@ -406,16 +406,25 @@ Curl the admin interface port to fetch the configured value for `xff_num_trusted
 
 ```shell
 curl -s 'http://localhost:19000/config_dump?resource=dynamic_listeners' \
-  | jq -r '.configs[0].active_state.listener.default_filter_chain.filters[0].typed_config 
-      | with_entries(select(.key | match("xff|remote_address|original_ip")))'
+  | jq -r '.configs[1].active_state.listener.default_filter_chain.filters[0].typed_config
+    | {use_remote_address, original_ip_detection_extensions}'
 ```
 
 You should expect to see the following:
 
 ```json
 {
-  "use_remote_address": true,
-  "xff_num_trusted_hops": 2
+  "use_remote_address": false,
+  "original_ip_detection_extensions": [
+    {
+      "name": "envoy.extensions.http.original_ip_detection.xff",
+      "typed_config": {
+        "@type": "type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig",
+        "xff_num_trusted_hops": 1,
+        "skip_xff_append": false
+      }
+    }
+  ]
 }
 ```
 
diff --git a/site/go.mod b/site/go.mod
index 1f97d802b..16f434b62 100644
--- a/site/go.mod
+++ b/site/go.mod
@@ -3,8 +3,8 @@ module github.com/google/docsy-example
 go 1.24.2
 
 require (
-	github.com/FortAwesome/Font-Awesome v0.0.0-20240402185447-c0f460dca7f7 // indirect
-	github.com/google/docsy v0.10.0 // indirect
+	github.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de // indirect
+	github.com/google/docsy v0.11.0 // indirect
 	github.com/google/docsy/dependencies v0.7.2 // indirect
-	github.com/twbs/bootstrap v5.3.3+incompatible // indirect
+	github.com/twbs/bootstrap v5.3.6+incompatible // indirect
 )
diff --git a/site/go.sum b/site/go.sum
index 0db3accef..a6acef5f8 100644
--- a/site/go.sum
+++ b/site/go.sum
@@ -6,6 +6,8 @@ github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2 h1:Uv1z5E
 github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=
 github.com/FortAwesome/Font-Awesome v0.0.0-20240402185447-c0f460dca7f7 h1:2aWEKCRLqQ9nPyXaz4/IYtRrDr3PzEiX0DUSUr2/EDs=
 github.com/FortAwesome/Font-Awesome v0.0.0-20240402185447-c0f460dca7f7/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=
+github.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de h1:JvHOfdSqvArF+7cffH9oWU8oLhn6YFYI60Pms8M/6tI=
+github.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=
 github.com/google/docsy v0.5.1 h1:D/ZdFKiE29xM/gwPwQzmkyXhcbQGkReRS6aGrF7lnYk=
 github.com/google/docsy v0.5.1/go.mod h1:maoUAQU5H/d+FrZIB4xg1EVWAx7RyFMGSDJyWghm31E=
 github.com/google/docsy v0.6.0 h1:43bVF18t2JihAamelQjjGzx1vO2ljCilVrBgetCA8oI=
@@ -16,6 +18,8 @@ github.com/google/docsy v0.7.1 h1:DUriA7Nr3lJjNi9Ulev1SfiG1sUYmvyDeU4nTp7uDxY=
 github.com/google/docsy v0.7.1/go.mod h1:JCmE+c+izhE0Rvzv3y+AzHhz1KdwlA9Oj5YBMklJcfc=
 github.com/google/docsy v0.10.0 h1:6tMDacPwAyRWNCfvsn/9qGOZDQ8b0aRzjRZvnZPY5dg=
 github.com/google/docsy v0.10.0/go.mod h1:c0nIAqmRTOuJ01F85U/wJPQtc3Zj9N58Kea9bOT2AJc=
+github.com/google/docsy v0.11.0 h1:QnV40cc28QwS++kP9qINtrIv4hlASruhC/K3FqkHAmM=
+github.com/google/docsy v0.11.0/go.mod h1:hGGW0OjNuG5ZbH5JRtALY3yvN8ybbEP/v2iaK4bwOUI=
 github.com/google/docsy/dependencies v0.5.1/go.mod h1:EDGc2znMbGUw0RW5kWwy2oGgLt0iVXBmoq4UOqstuNE=
 github.com/google/docsy/dependencies v0.6.0/go.mod h1:EDGc2znMbGUw0RW5kWwy2oGgLt0iVXBmoq4UOqstuNE=
 github.com/google/docsy/dependencies v0.7.0/go.mod h1:gihhs5gmgeO+wuoay4FwOzob+jYJVyQbNaQOh788lD4=
@@ -28,3 +32,5 @@ github.com/twbs/bootstrap v5.2.3+incompatible h1:lOmsJx587qfF7/gE7Vv4FxEofegyJlE
 github.com/twbs/bootstrap v5.2.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
 github.com/twbs/bootstrap v5.3.3+incompatible h1:goFoqinzdHfkeegpFP7pvhbd0g+A3O2hbU3XCjuNrEQ=
 github.com/twbs/bootstrap v5.3.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
+github.com/twbs/bootstrap v5.3.6+incompatible h1:efmXVyq839m5QQ0+JBUdQQ1TrmoBqvQ5kRhUueKsH+4=
+github.com/twbs/bootstrap v5.3.6+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
diff --git a/site/layouts/_default/_markup/render-heading.html b/site/layouts/_default/_markup/render-heading.html
index 7f8e97424..d984a0ca1 100644
--- a/site/layouts/_default/_markup/render-heading.html
+++ b/site/layouts/_default/_markup/render-heading.html
@@ -1,1 +1,12 @@
+{{ define "_default/_markup/td-render-heading.html" }}
+<h{{.Level}}
+{{- range $key, $value := .Attributes -}}
+  {{printf " %s=%q" $key (string $value) | safeHTMLAttr -}}
+{{- end -}}
+>
+{{- .Text | safeHTML -}}
+<a class="td-heading-self-link" href="#{{.Anchor | safeURL}}" aria-label="Heading self-link"></a>
+</h{{.Level}}>
+{{ end }}
+
 {{ template "_default/_markup/td-render-heading.html" . }}


Test patch:
diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go
index 1caeff658..2d1308dc8 100644
--- a/internal/gatewayapi/securitypolicy_test.go
+++ b/internal/gatewayapi/securitypolicy_test.go
@@ -534,3 +534,129 @@ func Test_APIKeyAuth(t *testing.T) {
 		})
 	}
 }
+
+func Test_OIDC_PassThroughAuthHeader(t *testing.T) {
+	tests := []struct {
+		name      string
+		OIDC      egv1a1.OIDC
+		JWT       *egv1a1.JWT
+		wantError bool
+	}{
+		{
+			name: "oidc and jwt with PassThroughAuthHeader configured",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT: &egv1a1.JWT{
+				Providers: []egv1a1.JWTProvider{
+					{
+						Name: "test",
+					},
+				},
+			},
+			wantError: false,
+		},
+		{
+			name: "jwt configured to read a non-standard header is ok",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT: &egv1a1.JWT{
+				Providers: []egv1a1.JWTProvider{
+					{
+						Name: "test",
+						ExtractFrom: &egv1a1.JWTExtractor{
+							Headers: []egv1a1.JWTHeaderExtractor{{Name: "SomeHeader", ValuePrefix: ToPointer("Bearer ")}},
+						},
+					},
+				},
+			},
+			wantError: false,
+		},
+		{
+			name: "jwt configured to read a non-standard header without valuePrefix is ok",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT: &egv1a1.JWT{
+				Providers: []egv1a1.JWTProvider{
+					{
+						Name: "test",
+						ExtractFrom: &egv1a1.JWTExtractor{
+							Headers: []egv1a1.JWTHeaderExtractor{{Name: "SomeHeader", ValuePrefix: nil}},
+						},
+					},
+				},
+			},
+			wantError: false,
+		},
+		{
+			name: "oidc with PassThroughAuthHeader configured requires jwt configured too",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT:       nil,
+			wantError: true,
+		},
+		{
+			name: "jwt configured to read cookie only is not ok",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT: &egv1a1.JWT{
+				Providers: []egv1a1.JWTProvider{
+					{
+						Name: "test",
+						ExtractFrom: &egv1a1.JWTExtractor{
+							Cookies: []string{"SomeCookie"},
+						},
+					},
+				},
+			},
+			wantError: true,
+		},
+		{
+			name: "jwt configured with multiple providers is ok",
+			OIDC: egv1a1.OIDC{
+				PassThroughAuthHeader: ToPointer(true),
+			},
+			JWT: &(egv1a1.JWT{
+				Providers: []egv1a1.JWTProvider{
+					{
+						Name: "test",
+						ExtractFrom: &egv1a1.JWTExtractor{
+							Headers: []egv1a1.JWTHeaderExtractor{{Name: "Blah"}},
+						},
+					},
+					{
+						Name: "test2",
+						ExtractFrom: &egv1a1.JWTExtractor{
+							Cookies: []string{"SomeCookie"},
+						},
+					},
+				},
+			}),
+			wantError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			securityPolicy := egv1a1.SecurityPolicy{
+				Spec: egv1a1.SecurityPolicySpec{
+					OIDC: &tt.OIDC,
+					JWT:  tt.JWT,
+				},
+			}
+			err := validateSecurityPolicy(&securityPolicy)
+			if (err != nil) != tt.wantError {
+				t.Errorf("validateSecurityPolicy() error = %v, wantErr %v", err, tt.wantError)
+				return
+			}
+		})
+	}
+}
+
+func ToPointer[T any](v T) *T {
+	return &v
+}
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.in.yaml
new file mode 100644
index 000000000..b95792290
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.in.yaml
@@ -0,0 +1,78 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway
+    name: client-secret-1
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway-system
+    name: envoy-oidc-hmac
+  data:
+    hmac-secret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/"
+      backendRefs:
+      - name: service-1
+        port: 8080
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: security-policy-1
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    oidc:
+      provider:
+        issuer: "https://oauth.foo.com"
+        authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth.foo.com/token"
+      clientID: "client.oauth.foo.com"
+      clientSecret:
+        name: "client-secret-1"
+      redirectURL: "https://www.example.com/oauth2/callback"
+      passThroughAuthHeader: true
+    jwt:
+      providers:
+      - name: example1
+        issuer: https://oauth.foo.com
+        remoteJWKS:
+          uri: https://oauth.foo.com/jwt/public-key/jwks.json
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.out.yaml
new file mode 100644
index 000000000..6f46ebd26
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-and-jwt-passthrough.out.yaml
@@ -0,0 +1,211 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: security-policy-1
+    namespace: envoy-gateway
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    jwt:
+      providers:
+      - issuer: https://oauth.foo.com
+        name: example1
+        remoteJWKS:
+          uri: https://oauth.foo.com/jwt/public-key/jwks.json
+    oidc:
+      clientID: client.oauth.foo.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client-secret-1
+      passThroughAuthHeader: true
+      provider:
+        authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+        issuer: https://oauth.foo.com
+        tokenEndpoint: https://oauth.foo.com/token
+      redirectURL: https://www.example.com/oauth2/callback
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: www.example.com
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        security:
+          jwt:
+            providers:
+            - issuer: https://oauth.foo.com
+              name: example1
+              remoteJWKS:
+                uri: https://oauth.foo.com/jwt/public-key/jwks.json
+          oidc:
+            clientID: client.oauth.foo.com
+            clientSecret: '[redacted]'
+            cookieSuffix: b0a1b740
+            hmacSecret: '[redacted]'
+            logoutPath: /logout
+            name: securitypolicy/envoy-gateway/security-policy-1
+            passThroughAuthHeader: true
+            provider:
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              tokenEndpoint: https://oauth.foo.com/token
+            redirectPath: /oauth2/callback
+            redirectURL: https://www.example.com/oauth2/callback
+            scopes:
+            - openid
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/wasm/premissioncache_test.go b/internal/wasm/premissioncache_test.go
index 9cc7bb10c..a6231fe59 100644
--- a/internal/wasm/premissioncache_test.go
+++ b/internal/wasm/premissioncache_test.go
@@ -65,193 +65,270 @@ func TestPermissionCache(t *testing.T) {
 	latestImage, _ := url.Parse(ociURLWithLatestTag)
 	secret := []byte("")
 
-	t.Run("Cached permission should be updated", func(t *testing.T) {
-		lock.Lock()
-		failPermissionCheck = false
-		lock.Unlock()
-
-		ctx := context.Background()
-		defer ctx.Done()
-		cache, entry := setupTestPermissionCache(
-			permissionCacheOptions{
-				checkInterval:    1 * time.Nanosecond,
-				permissionExpiry: 10 * time.Nanosecond,
-			},
-			image,
-			latestImage,
-			secret)
-		cache.Start(ctx)
-
-		lastAccessTime := entry.lastAccess
-		lastCheckTime := entry.lastCheck
-
-		cache.triggerChan <- struct{}{}
-		time.Sleep(1 * time.Millisecond)
-		allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
-		require.True(
-			t,
-			allowed,
-			"permission should be rechecked and allowed after permission expired")
-		require.NoError(
-			t,
-			err,
-			"permission should be rechecked and allowed after permission expired")
-
-		entry, ok := cache.getForTest(entry.key())
-		require.True(t, ok, "cache entry should exist")
-		require.True(t, entry.lastAccess.After(lastAccessTime), "last access time should be updated")
-		require.True(t, entry.lastCheck.After(lastCheckTime), "last check time should be updated")
+	t.Run("Cached permission should be updated after expiry", func(t *testing.T) {
+		require.Eventually(t, func() bool {
+			lock.Lock()
+			failPermissionCheck = false
+			lock.Unlock()
+
+			ctx := context.Background()
+			defer ctx.Done()
+			cache, entry := setupTestPermissionCache(
+				permissionCacheOptions{
+					checkInterval:    1 * time.Nanosecond,
+					permissionExpiry: 10 * time.Nanosecond,
+				},
+				image,
+				latestImage,
+				secret)
+			cache.Start(ctx)
+
+			lastAccessTime := entry.lastAccess
+			lastCheckTime := entry.lastCheck
+
+			// Wait for the cache to expire
+			time.Sleep(10 * time.Millisecond)
+			allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
+			if err != nil {
+				t.Logf("permission rechecked should not return error: %v", err)
+				return false
+			}
+			if !allowed {
+				t.Log("ppermission should be rechecked and allowed after permission expired")
+				return false
+			}
+			entry, ok := cache.getForTest(entry.key())
+			if !ok {
+				t.Log("cache entry should exist")
+				return false
+			}
+			// Verify the cached image permission is rechecked
+			if !entry.lastAccess.After(lastAccessTime) {
+				t.Log("last access time should be updated")
+				return false
+			}
+			if !entry.lastCheck.After(lastCheckTime) {
+				t.Log("last check time should be updated")
+				return false
+			}
+			return true
+		}, time.Second*5, time.Millisecond*20)
 	})
 
 	t.Run("Cached permission failed after recheck", func(t *testing.T) {
-		lock.Lock()
-		failPermissionCheck = true
-		lock.Unlock()
-
-		ctx := context.Background()
-		defer ctx.Done()
-		cache, entry := setupTestPermissionCache(
-			permissionCacheOptions{
-				checkInterval:    1 * time.Nanosecond,
-				permissionExpiry: 10 * time.Nanosecond,
-			},
-			image,
-			latestImage,
-			secret)
-		cache.Start(ctx)
-
-		lastAccessTime := entry.lastAccess
-		lastCheckTime := entry.lastCheck
-
-		cache.triggerChan <- struct{}{}
-		time.Sleep(1 * time.Millisecond)
-		allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
-		require.False(t, isRetriableError(err), "permission check error should not be retriable")
-		require.False(
-			t,
-			allowed,
-			"permission should be rechecked and denied after permission expired and secret is invalid")
-		require.Error(
-			t,
-			err,
-			"permission should be rechecked and denied after permission expired and secret is invalid")
-
-		entry, ok := cache.getForTest(entry.key())
-		require.True(t, ok, "cache entry should exist")
-		require.True(t, entry.lastAccess.After(lastAccessTime), "last access time should be updated")
-		require.True(t, entry.lastCheck.After(lastCheckTime), "last check time should be updated")
+		require.Eventually(t, func() bool {
+			lock.Lock()
+			failPermissionCheck = true
+			lock.Unlock()
+
+			ctx := context.Background()
+			defer ctx.Done()
+			cache, entry := setupTestPermissionCache(
+				permissionCacheOptions{
+					checkInterval:    1 * time.Nanosecond,
+					permissionExpiry: 10 * time.Nanosecond,
+				},
+				image,
+				latestImage,
+				secret)
+			cache.Start(ctx)
+
+			lastAccessTime := entry.lastAccess
+			lastCheckTime := entry.lastCheck
+
+			// Wait for the cache to expire
+			time.Sleep(10 * time.Millisecond)
+			allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
+			if err == nil {
+				t.Log("permission rechecked should return error if failed permission check")
+				return false
+			}
+			if isRetriableError(err) {
+				t.Logf("permission check error should not be retriable: %v", err)
+				return false
+			}
+			if allowed {
+				t.Log("permission should be rechecked and denied after permission expired and secret is invalid")
+				return false
+			}
+			entry, ok := cache.getForTest(entry.key())
+			if !ok {
+				t.Log("cache entry should exist")
+				return false
+			}
+			if !entry.lastAccess.After(lastAccessTime) {
+				t.Log("last access time should be updated")
+				return false
+			}
+			if !entry.lastCheck.After(lastCheckTime) {
+				t.Log("last check time should be updated")
+				return false
+			}
+			return true
+		}, time.Second*5, time.Millisecond*20)
 	})
 
 	t.Run("Cached permission should be removed after expiry", func(t *testing.T) {
-		lock.Lock()
-		failPermissionCheck = false
-		lock.Unlock()
-
-		ctx := context.Background()
-		defer ctx.Done()
-		cache, entry := setupTestPermissionCache(
-			permissionCacheOptions{
-				checkInterval: 1 * time.Nanosecond,
-				cacheExpiry:   10 * time.Nanosecond,
-			},
-			image,
-			latestImage,
-			secret)
-		cache.Start(ctx)
-
-		lastAccessTime := entry.lastAccess
-		lastCheckTime := entry.lastCheck
-
-		cache.triggerChan <- struct{}{}
-		time.Sleep(1 * time.Millisecond)
-		key := entry.key()
-		entry, ok := cache.getForTest(key)
-		require.False(t, ok, "cache entry should be removed after expiry")
-		allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
-		require.True(t,
-			allowed,
-			"permission should be rechecked and allowed after cache removed")
-		require.NoError(t,
-			err,
-			"permission should be rechecked and allowed after cache removed")
-		entry, ok = cache.getForTest(key)
-		require.True(t, ok, "expired entry should be added after recheck")
-		require.True(t, entry.lastAccess.After(lastAccessTime), "last access time should be updated")
-		require.True(t, entry.lastCheck.After(lastCheckTime), "last check time should be updated")
+		require.Eventually(t, func() bool {
+			lock.Lock()
+			failPermissionCheck = false
+			lock.Unlock()
+
+			ctx := context.Background()
+			defer ctx.Done()
+			cache, entry := setupTestPermissionCache(
+				permissionCacheOptions{
+					checkInterval: 1 * time.Nanosecond,
+					cacheExpiry:   10 * time.Nanosecond,
+				},
+				image,
+				latestImage,
+				secret)
+			cache.Start(ctx)
+
+			lastAccessTime := entry.lastAccess
+			lastCheckTime := entry.lastCheck
+
+			// Wait for the cache to expire
+			time.Sleep(10 * time.Millisecond)
+			key := entry.key()
+			entry, ok := cache.getForTest(key)
+			if ok {
+				t.Log("cache entry should be removed after expiry")
+				return false
+			}
+			allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
+			if err != nil {
+				t.Logf("permission rechecked should not return error: %v", err)
+				return false
+			}
+			if !allowed {
+				t.Log("permission should be rechecked and allowed after cache removed")
+				return false
+			}
+			entry, ok = cache.getForTest(key)
+			if !ok {
+				t.Log("expired entry should be added after recheck")
+				return false
+			}
+			if !entry.lastAccess.After(lastAccessTime) {
+				t.Log("last access time should be updated")
+				return false
+			}
+			if !entry.lastCheck.After(lastCheckTime) {
+				t.Log("last check time should be updated")
+				return false
+			}
+			return true
+		}, time.Second*5, time.Millisecond*20)
 	})
 
 	t.Run("Non-exist permission should be checked and cached after first access for allowed permission", func(t *testing.T) {
-		lock.Lock()
-		failPermissionCheck = false
-		lock.Unlock()
-
-		ctx := context.Background()
-		defer ctx.Done()
-		cache, entry := setupTestPermissionCache(
-			permissionCacheOptions{
-				checkInterval: 1 * time.Nanosecond,
-			},
-			image,
-			latestImage,
-			secret)
-		key := entry.key()
-		// remove the cache entry
-		cache.deleteForTest(key)
-		cache.Start(ctx)
-
-		_, ok := cache.getForTest(key)
-		require.False(t, ok, "cache entry should not exist before access")
-
-		now := time.Now()
-		allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
-		require.True(t,
-			allowed,
-			"non-exist permission should be checked and allowed at first access")
-		require.NoError(t,
-			err,
-			"non-exist permission should be checked and allowed at first access")
-
-		entry, ok = cache.getForTest(key)
-		require.True(t, ok, "non-exist permission should be added to the cache after first access ")
-		require.True(t, entry.lastAccess.After(now), "last access time should be updated after first access")
-		require.True(t, entry.lastCheck.After(now), "last check time should be updated after first access")
+		require.Eventually(t, func() bool {
+			lock.Lock()
+			failPermissionCheck = false
+			lock.Unlock()
+
+			ctx := context.Background()
+			defer ctx.Done()
+			cache, entry := setupTestPermissionCache(
+				permissionCacheOptions{
+					checkInterval: 1 * time.Nanosecond,
+				},
+				image,
+				latestImage,
+				secret)
+			key := entry.key()
+			// remove the cache entry
+			cache.deleteForTest(key)
+			cache.Start(ctx)
+
+			_, ok := cache.getForTest(key)
+			if ok {
+				t.Log("cache entry should not exist before access")
+				return false
+			}
+
+			now := time.Now()
+			allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
+			if err != nil {
+				t.Logf("permission check should not return error: %v", err)
+				return false
+			}
+			if !allowed {
+				t.Log("non-exist permission should be checked and allowed at first access")
+				return false
+			}
+
+			entry, ok = cache.getForTest(key)
+			if !ok {
+				t.Log("non-exist permission should be added to the cache after first access")
+				return false
+			}
+			if !entry.lastAccess.After(now) {
+				t.Log("last access time should be updated after first access")
+				return false
+			}
+			if !entry.lastCheck.After(now) {
+				t.Log("last check time should be updated after first access")
+				return false
+			}
+			return true
+		}, time.Second*5, time.Millisecond*20)
 	})
 
 	t.Run("Non-exist permission should be checked and cached after first access for denied permission", func(t *testing.T) {
-		lock.Lock()
-		failPermissionCheck = true
-		lock.Unlock()
-
-		ctx := context.Background()
-		defer ctx.Done()
-		cache, entry := setupTestPermissionCache(
-			permissionCacheOptions{
-				checkInterval: 1 * time.Nanosecond,
-			},
-			image,
-			latestImage,
-			secret)
-		key := entry.key()
-		// remove the cache entry
-		cache.deleteForTest(key)
-		cache.Start(ctx)
-
-		_, ok := cache.getForTest(key)
-		require.False(t, ok, "cache entry should not exist before access")
-
-		now := time.Now()
-		allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
-		require.False(t,
-			allowed,
-			"non-exist permission should be checked and denied at first access if secret is invalid")
-		require.Error(t,
-			err,
-			"non-exist permission should be checked and denied at first access if secret is invalid")
-
-		entry, ok = cache.getForTest(key)
-		require.True(t, ok, "non-exist permission should be added to the cache after first access ")
-		require.True(t, entry.lastAccess.After(now), "last access time should be updated after first access")
-		require.True(t, entry.lastCheck.After(now), "last check time should be updated after first access")
+		require.Eventually(t, func() bool {
+			lock.Lock()
+			failPermissionCheck = true
+			lock.Unlock()
+
+			ctx := context.Background()
+			defer ctx.Done()
+			cache, entry := setupTestPermissionCache(
+				permissionCacheOptions{
+					checkInterval: 1 * time.Nanosecond,
+				},
+				image,
+				latestImage,
+				secret)
+			key := entry.key()
+			// remove the cache entry
+			cache.deleteForTest(key)
+			cache.Start(ctx)
+
+			_, ok := cache.getForTest(key)
+			if ok {
+				t.Log("cache entry should not exist before access")
+				return false
+			}
+
+			now := time.Now()
+			allowed, err := cache.IsAllowed(context.Background(), image, secret, true)
+			if err == nil {
+				t.Logf("non-exist permission should be checked and denied at first access if secret is invalid")
+				return false
+			}
+			if allowed {
+				t.Log("non-exist permission should be checked and denied at first access if secret is invalid")
+				return false
+			}
+
+			entry, ok = cache.getForTest(key)
+			if !ok {
+				t.Log("non-exist permission should be added to the cache after first access")
+				return false
+			}
+			if !entry.lastAccess.After(now) {
+				t.Log("last access time should be updated after first access")
+				return false
+			}
+			if !entry.lastCheck.After(now) {
+				t.Log("last check time should be updated after first access")
+				return false
+			}
+			return true
+		}, time.Second*5, time.Millisecond*20)
 	})
 }
 
diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc-and-jwt-with-passthrough.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc-and-jwt-with-passthrough.yaml
new file mode 100644
index 000000000..4cddbaafd
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/oidc-and-jwt-with-passthrough.yaml
@@ -0,0 +1,70 @@
+http:
+  - name: envoy-gateway/gateway-1/http
+    address: 0.0.0.0
+    port: 10080
+    hostnames:
+      - '*'
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    routes:
+      - name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+        hostname: www.example.com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+            - endpoints:
+                - host: 7.7.7.7
+                  port: 8080
+              name: httproute/default/httproute-1/rule/0/backend/0
+        security:
+          jwt:
+            providers:
+              - issuer: https://oauth.foo.com
+                name: example1
+                remoteJWKS:
+                  uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              - issuer: https://oauth.foo.com
+                name: example2
+                extractFrom:
+                  headers:
+                    - name: MyHeaderPrefixed
+                      valuePrefix: MyPrefix
+                    - name: MyHeaderNoPrefix
+                remoteJWKS:
+                  uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              - issuer: https://oauth.foo.com
+                name: example3
+                extractFrom:
+                  cookies:
+                    - Cookie1
+                    - Cookie1
+                remoteJWKS:
+                  uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              - issuer: https://oauth.foo.com
+                name: example4
+                extractFrom:
+                  params:
+                    - Param1
+                    - Param1
+                remoteJWKS:
+                  uri: https://oauth.foo.com/jwt/public-key/jwks.json
+          oidc:
+            clientID: client.oauth.foo.com
+            clientSecret: Y2xpZW50MTpzZWNyZXQK
+            cookieSuffix: b0a1b740
+            hmacSecret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+            logoutPath: /logout
+            name: securitypolicy/envoy-gateway/security-policy-1
+            passThroughAuthHeader: true
+            provider:
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              tokenEndpoint: https://oauth.foo.com/token
+            redirectPath: /oauth2/callback
+            redirectURL: https://www.example.com/oauth2/callback
+            scopes:
+              - openid
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.clusters.yaml
new file mode 100644
index 000000000..19ed98a02
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.clusters.yaml
@@ -0,0 +1,52 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  dnsRefreshRate: 30s
+  lbPolicy: LEAST_REQUEST
+  loadAssignment:
+    clusterName: oauth_foo_com_443
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: oauth.foo.com
+              portValue: 443
+        loadBalancingWeight: 1
+      loadBalancingWeight: 1
+      locality:
+        region: oauth_foo_com_443/backend/-1
+  name: oauth_foo_com_443
+  perConnectionBufferLimitBytes: 32768
+  respectDnsTtl: true
+  transportSocket:
+    name: envoy.transport_sockets.tls
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+      commonTlsContext:
+        validationContext:
+          trustedCa:
+            filename: /etc/ssl/certs/ca-certificates.crt
+      sni: oauth.foo.com
+  type: STRICT_DNS
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.endpoints.yaml
new file mode 100644
index 000000000..29bb6b4e4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: httproute/default/httproute-1/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/0/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.listeners.yaml
new file mode 100644
index 000000000..8996b15c6
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.listeners.yaml
@@ -0,0 +1,161 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.oauth2/securitypolicy/envoy-gateway/security-policy-1
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
+            config:
+              authScopes:
+              - openid
+              authType: BASIC_AUTH
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              credentials:
+                clientId: client.oauth.foo.com
+                cookieNames:
+                  bearerToken: AccessToken-b0a1b740
+                  idToken: IdToken-b0a1b740
+                  oauthExpires: OauthExpires-b0a1b740
+                  oauthHmac: OauthHMAC-b0a1b740
+                  oauthNonce: OauthNonce-b0a1b740
+                  refreshToken: RefreshToken-b0a1b740
+                hmacSecret:
+                  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/security-policy-1
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+                tokenSecret:
+                  name: oauth2/client_secret/securitypolicy/envoy-gateway/security-policy-1
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+              passThroughMatcher:
+              - name: Authorization
+                stringMatch:
+                  prefix: 'Bearer '
+              - name: MyHeaderPrefixed
+                stringMatch:
+                  prefix: MyPrefix
+              - name: MyHeaderNoPrefix
+              preserveAuthorizationHeader: true
+              redirectPathMatcher:
+                path:
+                  exact: /oauth2/callback
+              redirectUri: https://www.example.com/oauth2/callback
+              signoutPath:
+                path:
+                  exact: /logout
+              tokenEndpoint:
+                cluster: oauth_foo_com_443
+                timeout: 10s
+                uri: https://oauth.foo.com/token
+              useRefreshToken: false
+        - name: envoy.filters.http.jwt_authn
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
+            providers:
+              httproute/default/httproute-1/rule/0/match/0/www_example_com/example1:
+                forward: true
+                issuer: https://oauth.foo.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: example1
+                remoteJwks:
+                  asyncFetch: {}
+                  cacheDuration: 300s
+                  httpUri:
+                    cluster: oauth_foo_com_443
+                    timeout: 10s
+                    uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              httproute/default/httproute-1/rule/0/match/0/www_example_com/example2:
+                forward: true
+                fromHeaders:
+                - name: MyHeaderPrefixed
+                  valuePrefix: MyPrefix
+                - name: MyHeaderNoPrefix
+                issuer: https://oauth.foo.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: example2
+                remoteJwks:
+                  asyncFetch: {}
+                  cacheDuration: 300s
+                  httpUri:
+                    cluster: oauth_foo_com_443
+                    timeout: 10s
+                    uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              httproute/default/httproute-1/rule/0/match/0/www_example_com/example3:
+                forward: true
+                fromCookies:
+                - Cookie1
+                - Cookie1
+                issuer: https://oauth.foo.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: example3
+                remoteJwks:
+                  asyncFetch: {}
+                  cacheDuration: 300s
+                  httpUri:
+                    cluster: oauth_foo_com_443
+                    timeout: 10s
+                    uri: https://oauth.foo.com/jwt/public-key/jwks.json
+              httproute/default/httproute-1/rule/0/match/0/www_example_com/example4:
+                forward: true
+                fromParams:
+                - Param1
+                - Param1
+                issuer: https://oauth.foo.com
+                normalizePayloadInMetadata:
+                  spaceDelimitedClaims:
+                  - scope
+                payloadInMetadata: example4
+                remoteJwks:
+                  asyncFetch: {}
+                  cacheDuration: 300s
+                  httpUri:
+                    cluster: oauth_foo_com_443
+                    timeout: 10s
+                    uri: https://oauth.foo.com/jwt/public-key/jwks.json
+            requirementMap:
+              httproute/default/httproute-1/rule/0/match/0/www_example_com:
+                requiresAny:
+                  requirements:
+                  - providerName: httproute/default/httproute-1/rule/0/match/0/www_example_com/example1
+                  - providerName: httproute/default/httproute-1/rule/0/match/0/www_example_com/example2
+                  - providerName: httproute/default/httproute-1/rule/0/match/0/www_example_com/example3
+                  - providerName: httproute/default/httproute-1/rule/0/match/0/www_example_com/example4
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: envoy-gateway/gateway-1/http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: envoy-gateway/gateway-1/http
+  name: envoy-gateway/gateway-1/http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.routes.yaml
new file mode 100644
index 000000000..532d7d504
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.routes.yaml
@@ -0,0 +1,21 @@
+- ignorePortInHostMatching: true
+  name: envoy-gateway/gateway-1/http
+  virtualHosts:
+  - domains:
+    - www.example.com
+    name: envoy-gateway/gateway-1/http/www_example_com
+    routes:
+    - match:
+        prefix: /
+      name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+      route:
+        cluster: httproute/default/httproute-1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.jwt_authn:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig
+          requirementName: httproute/default/httproute-1/rule/0/match/0/www_example_com
+        envoy.filters.http.oauth2/securitypolicy/envoy-gateway/security-policy-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.secrets.yaml
new file mode 100755
index 000000000..bc57f5e0e
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-and-jwt-with-passthrough.secrets.yaml
@@ -0,0 +1,8 @@
+- genericSecret:
+    secret:
+      inlineBytes: Y2xpZW50MTpzZWNyZXQK
+  name: oauth2/client_secret/securitypolicy/envoy-gateway/security-policy-1
+- genericSecret:
+    secret:
+      inlineBytes: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/security-policy-1
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 8e7da3240..a29c08aa5 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -3146,6 +3146,7 @@ _Appears in:_
 | `defaultTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ |  false  |  | DefaultTokenTTL is the default lifetime of the id token and access token.<br />Please note that Envoy will always use the expiry time from the response<br />of the authorization server if it is provided. This field is only used when<br />the expiry time is not provided by the authorization.<br />If not specified, defaults to 0. In this case, the "expires_in" field in<br />the authorization response must be set by the authorization server, or the<br />OAuth flow will fail. |
 | `refreshToken` | _boolean_ |  false  |  | RefreshToken indicates whether the Envoy should automatically refresh the<br />id token and access token when they expire.<br />When set to true, the Envoy will use the refresh token to get a new id token<br />and access token when they expire.<br />If not specified, defaults to false. |
 | `defaultRefreshTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ |  false  |  | DefaultRefreshTokenTTL is the default lifetime of the refresh token.<br />This field is only used when the exp (expiration time) claim is omitted in<br />the refresh token or the refresh token is not JWT.<br />If not specified, defaults to 604800s (one week).<br />Note: this field is only applicable when the "refreshToken" field is set to true. |
+| `passThroughAuthHeader` | _boolean_ |  false  |  | Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless<br />explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.<br />The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC<br />redirects and wish to directly supply a token instead.<br />If not specified, defaults to false. |
 
 
 #### OIDCCookieNames
diff --git a/site/content/en/latest/boilerplates/testing.md b/site/content/en/latest/boilerplates/testing.md
index 58f5377da..3cda43e6d 100644
--- a/site/content/en/latest/boilerplates/testing.md
+++ b/site/content/en/latest/boilerplates/testing.md
@@ -28,6 +28,12 @@ Get the name of the Envoy service created the by the example Gateway:
 export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
 ```
 
+Get the deployment of the Envoy service created the by the example Gateway:
+
+```shell
+export ENVOY_DEPLOYMENT=$(kubectl get deploy -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
+```
+
 Port forward to the Envoy service:
 
 ```shell
diff --git a/site/content/en/latest/tasks/operations/deployment-mode.md b/site/content/en/latest/tasks/operations/deployment-mode.md
index c50d65b28..725f7c3cb 100644
--- a/site/content/en/latest/tasks/operations/deployment-mode.md
+++ b/site/content/en/latest/tasks/operations/deployment-mode.md
@@ -4,18 +4,22 @@ title: "Deployment Mode"
 ## Deployment modes
 
 ### One GatewayClass per Envoy Gateway Controller
+
 * An Envoy Gateway is associated with a single [GatewayClass][] resource under one controller.
 This is the simplest deployment mode and is suitable for scenarios where each Gateway needs to have its own dedicated set of resources and configurations.
 
 ### Multiple GatewayClasses per Envoy Gateway Controller
+
 * An Envoy Gateway is associated with multiple [GatewayClass][] resources under one controller.
 * Support for accepting multiple GatewayClasses was added [here][issue1231].
 
 ### Separate Envoy Gateway Controllers
+
 If you've instantiated multiple GatewayClasses, you can also run separate Envoy Gateway controllers in different namespaces, linking a GatewayClass to each of them for multi-tenancy.
 Please follow the example [Multi-tenancy](#multi-tenancy).
 
 ### Merged Gateways onto a single EnvoyProxy fleet
+
 By default, each Gateway has its own dedicated set of Envoy Proxy and its configurations.
 However, for some deployments, it may be more convenient to merge listeners across multiple Gateways and deploy a single Envoy Proxy fleet.
 
@@ -26,6 +30,14 @@ Setting the `mergeGateways` field in the EnvoyProxy resource linked to GatewayCl
 
 Please follow the example [Merged gateways deployment](#merged-gateways-deployment).
 
+### Gateway Namespace Mode
+
+Gateway Namespace Mode is a deployment model for Envoy Gateway that creates Envoy Proxy infrastructure resources like Deployments, Services and ServiceAccounts in the namespace where each Gateway resource is defined, rather than in the Envoy Gateway controller namespace.
+
+* Support for this deployment mode was added [here][issue2629].
+
+Please follow the example [Gateway Namespace Mode][].
+
 ### Supported Modes
 
 #### Kubernetes
@@ -34,7 +46,6 @@ Please follow the example [Merged gateways deployment](#merged-gateways-deployme
 and **creates** managed data plane resources such as EnvoyProxy `Deployment` in the **namespace where Envoy Gateway is running**.
 * Envoy Gateway also supports [Namespaced deployment mode][], you can watch resources in the specific namespaces by assigning
 `EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector` and **creates** managed data plane resources in the **namespace where Envoy Gateway is running**.
-* Support for alternate deployment modes is being tracked [here][issue1117].
 
 ### Multi-tenancy
 
@@ -1068,5 +1079,6 @@ curl --header "Host: www.merged3.com" http://$GATEWAY_HOST:8082/example3
 [EnvoyProxy]: ../../api/extension_types#envoyproxy
 [GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/
 [Namespaced deployment mode]: ../../api/extension_types#kuberneteswatchmode
+[Gateway Namespace Mode]: ./gateway-namespace-mode
 [issue1231]: https://github.com/envoyproxy/gateway/issues/1231
-[issue1117]: https://github.com/envoyproxy/gateway/issues/1117
+[issue2629]: https://github.com/envoyproxy/gateway/issues/2629
diff --git a/site/content/en/latest/tasks/operations/gateway-namespace-mode.md b/site/content/en/latest/tasks/operations/gateway-namespace-mode.md
new file mode 100644
index 000000000..ad7f3cd51
--- /dev/null
+++ b/site/content/en/latest/tasks/operations/gateway-namespace-mode.md
@@ -0,0 +1,270 @@
+---
+title: "Gateway Namespace Mode"
+---
+
+{{% alert title="Notice" color="warning" %}}
+
+Gateway Namespace Mode is currently an **alpha** feature. We recommend against using it in production workloads until it reaches beta status.
+
+For status updates or to provide feedback, please follow our [GitHub issues](https://github.com/envoyproxy/gateway/issues).
+
+{{% /alert %}}
+
+## Overview
+
+In standard deployment mode, Envoy Gateway creates all data plane resources in the controller namespace (typically `envoy-gateway-system`).
+
+Gateway Namespace Mode changes this behavior by placing Envoy Proxy data plane resources like Deployments, Services and ServiceAccounts in each Gateway's namespace, providing stronger isolation and multi-tenancy.
+
+Traditional deployment mode uses mTLS where both the client and server authenticate each other. However, in Gateway Namespace Mode, we've shifted to server-side TLS and JWT token validation between infra and control-plane.
+
+* Only the CA certificate is available in pods running in Gateway namespaces
+* Client certificates are not mounted in these namespaces
+* The Envoy proxy still validates server certificates using the CA certificate
+
+Gateway Namespace Mode uses projected service account JWT tokens for authentication.
+* Use short-lived, audience-specific JWT tokens. These tokens are automatically mounted into pods via the projected volume mechanism
+* JWT validation ensures that only authorized proxies can connect to the xDS server
+
+{{% alert title="Note" color="warning" %}}
+
+Currently it is not supported to run Gateway Namespace Mode with Merged Gateways deployments.
+
+{{% /alert %}}
+
+## Configuration
+
+To enable Gateway Namespace Mode, configure the `provider.kubernetes.deploy.type` field in your Envoy Gateway Helm chart values:
+
+```yaml
+config:
+  envoyGateway:
+    provider:
+      type: Kubernetes
+      kubernetes:
+        deploy:
+          type: GatewayNamespace
+```
+
+You can also use this command to install Envoy Gateway with Gateway Namespace Mode using Helm:
+
+```shell
+helm install \
+  --set config.envoyGateway.provider.kubernetes.deploy.type=GatewayNamespace \
+  eg oci://docker.io/envoyproxy/gateway-helm \
+  --version {{< helm-version >}} -n envoy-gateway-system --create-namespace
+```
+
+### RBAC configuration
+
+When using Gateway Namespace Mode, Envoy Gateway needs additional RBAC permissions to create and manage resources across different namespaces. The following RBAC resources are automatically created when installing Envoy Gateway Helm Chart with Gateway Namespace Mode enabled.
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: gateway-helm-cluster-infra-manager
+rules:
+- apiGroups: [""]
+  resources: ["serviceaccounts", "services", "configmaps"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["apps"]
+  resources: ["deployments", "daemonsets"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["autoscaling", "policy"]
+  resources: ["horizontalpodautoscalers", "poddisruptionbudgets"]
+  verbs: ["create", "get", "delete", "deletecollection", "patch"]
+- apiGroups: ["authentication.k8s.io"]
+  resources: ["tokenreviews"]
+  verbs: ["create"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gateway-helm-cluster-infra-manager
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: 'gateway-helm-cluster-infra-manager'
+subjects:
+- kind: ServiceAccount
+  name: 'envoy-gateway'
+  namespace: 'envoy-gateway-system'
+```
+
+Envoy Gateway also supports configuration to you only watch resources in the specific namespaces by assigning
+`EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector`.
+In this case, when you specify this configuration with Gateway Namespace Mode,Envoy Gateway will only watch for Gateway API resources in the specified namespaces and create needed Roles for infrastructure management in the specified namespaces.
+
+## Testing
+
+The following example demonstrates deploying two Gateways in different namespaces `team-a` and `team-b`.
+
+### Create test namespaces
+
+```shell
+kubectl create namespace team-a
+kubectl create namespace team-b
+```
+
+### Deploy Gateway Namespace Mode Example
+
+Deploy resources on your cluster from the example, it will create two sets of backend deployments, Gateways and their respective HTTPRoutes in the previously created namespaces `team-a` and `team-b`.
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/{{< yaml-version >}}/examples/kubernetes/gateway-namespace-mode.yaml
+```
+
+Verify that Gateways are deployed and programmed
+
+```shell
+kubectl get gateways -n team-a
+
+NAME        CLASS   ADDRESS        PROGRAMMED   AGE
+gateway-a   eg      172.18.0.200   True         67s
+```
+
+```shell
+kubectl get gateways -n team-b
+
+NAME        CLASS   ADDRESS        PROGRAMMED   AGE
+gateway-b   eg      172.18.0.201   True         67s
+```
+
+Verify that HTTPRoutes are deployed
+
+```shell
+kubectl get httproute -n team-a
+
+NAME           HOSTNAMES            AGE
+team-a-route   ["www.team-a.com"]   67s
+```
+
+```shell
+kubectl get httproute -n team-b
+
+NAME           HOSTNAMES            AGE
+team-b-route   ["www.team-b.com"]   67s
+```
+
+Envoy Proxy resources should be created now in the namespace of every Gateway.
+
+```shell
+kubectl get pods -n team-a
+
+NAME                                              READY   STATUS    RESTARTS   AGE
+envoy-team-a-gateway-a-b65c6264-d56f5d989-6dv5s   2/2     Running   0          65s
+team-a-backend-6f786fb76f-nx26p                   1/1     Running   0          65s
+```
+
+```shell
+kubectl get pods -n team-b
+
+NAME                                               READY   STATUS    RESTARTS   AGE
+envoy-team-b-gateway-b-0ac91f5a-74f445884f-95pl8   2/2     Running   0          87s
+team-b-backend-966b5f47c-zxngl                     1/1     Running   0          87s
+```
+
+```shell
+kubectl get services -n team-a
+
+NAME                              TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
+envoy-team-a-gateway-a-b65c6264   LoadBalancer   10.96.191.198   172.18.0.200   8080:30999/TCP   3m2s
+team-a-backend                    ClusterIP      10.96.92.226    <none>         3000/TCP         3m2s
+```
+
+```shell
+kubectl get services -n team-b
+
+NAME                              TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
+envoy-team-b-gateway-b-0ac91f5a   LoadBalancer   10.96.144.13   172.18.0.201   8081:31683/TCP   3m43s
+team-b-backend                    ClusterIP      10.96.26.162   <none>         3000/TCP         3m43s
+```
+
+### Testing the Configuration
+
+Fetch external IPs of the services:
+
+```shell
+export GATEWAY_HOST_A=$(kubectl get gateway/gateway-a -n team-a -o jsonpath='{.status.addresses[0].value}')
+```
+
+```shell
+export GATEWAY_HOST_B=$(kubectl get gateway/gateway-b -n team-b -o jsonpath='{.status.addresses[0].value}')
+```
+
+Curl the route team-a-route through Envoy proxy:
+
+```shell
+curl --header "Host: www.team-a.com" http://$GATEWAY_HOST_A:8080/example
+```
+
+```shell
+{
+ "path": "/example",
+ "host": "www.team-a.com",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/8.7.1"
+  ],
+  "X-Envoy-External-Address": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-For": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-Proto": [
+   "http"
+  ],
+  "X-Request-Id": [
+   "52f08a5c-7e07-43b7-bd23-44693c60fc0c"
+  ]
+ },
+ "namespace": "team-a",
+ "ingress": "",
+ "service": "",
+ "pod": "team-a-backend-6f786fb76f-nx26p"
+```
+
+Curl the route team-b-route through Envoy proxy:
+
+```shell
+curl --header "Host: www.team-b.com" http://$GATEWAY_HOST_B:8081/example
+```
+
+```shell
+{
+ "path": "/example",
+ "host": "www.team-b.com",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/8.7.1"
+  ],
+  "X-Envoy-External-Address": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-For": [
+   "172.18.0.3"
+  ],
+  "X-Forwarded-Proto": [
+   "http"
+  ],
+  "X-Request-Id": [
+   "62a06bd7-4754-475b-854a-dca3fc159e93"
+  ]
+ },
+ "namespace": "team-b",
+ "ingress": "",
+ "service": "",
+ "pod": "team-b-backend-966b5f47c-d6jwj"
+```
diff --git a/site/content/en/latest/tasks/traffic/client-traffic-policy.md b/site/content/en/latest/tasks/traffic/client-traffic-policy.md
index 987bab510..cb9748160 100644
--- a/site/content/en/latest/tasks/traffic/client-traffic-policy.md
+++ b/site/content/en/latest/tasks/traffic/client-traffic-policy.md
@@ -406,16 +406,25 @@ Curl the admin interface port to fetch the configured value for `xff_num_trusted
 
 ```shell
 curl -s 'http://localhost:19000/config_dump?resource=dynamic_listeners' \
-  | jq -r '.configs[0].active_state.listener.default_filter_chain.filters[0].typed_config 
-      | with_entries(select(.key | match("xff|remote_address|original_ip")))'
+  | jq -r '.configs[1].active_state.listener.default_filter_chain.filters[0].typed_config
+    | {use_remote_address, original_ip_detection_extensions}'
 ```
 
 You should expect to see the following:
 
 ```json
 {
-  "use_remote_address": true,
-  "xff_num_trusted_hops": 2
+  "use_remote_address": false,
+  "original_ip_detection_extensions": [
+    {
+      "name": "envoy.extensions.http.original_ip_detection.xff",
+      "typed_config": {
+        "@type": "type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig",
+        "xff_num_trusted_hops": 1,
+        "skip_xff_append": false
+      }
+    }
+  ]
 }
 ```
 
diff --git a/site/content/en/v1.4/boilerplates/testing.md b/site/content/en/v1.4/boilerplates/testing.md
index 58f5377da..3cda43e6d 100644
--- a/site/content/en/v1.4/boilerplates/testing.md
+++ b/site/content/en/v1.4/boilerplates/testing.md
@@ -28,6 +28,12 @@ Get the name of the Envoy service created the by the example Gateway:
 export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
 ```
 
+Get the deployment of the Envoy service created the by the example Gateway:
+
+```shell
+export ENVOY_DEPLOYMENT=$(kubectl get deploy -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
+```
+
 Port forward to the Envoy service:
 
 ```shell
diff --git a/test/config/gatewayclass.yaml b/test/config/gatewayclass.yaml
index ba40972c6..8c337911d 100644
--- a/test/config/gatewayclass.yaml
+++ b/test/config/gatewayclass.yaml
@@ -22,7 +22,6 @@ spec:
     kubernetes:
       envoyDeployment:
         container:
-          image: envoyproxy/envoy:distroless-v1.34.1   # https://github.com/envoyproxy/gateway/issues/6077
           volumeMounts:
             - mountPath: /var/run/ext-proc
               name: socket-dir
diff --git a/test/e2e/testdata/backend-tls-settings.yaml b/test/e2e/testdata/backend-tls-settings.yaml
index 2bcb291a2..0efea1ff2 100644
--- a/test/e2e/testdata/backend-tls-settings.yaml
+++ b/test/e2e/testdata/backend-tls-settings.yaml
@@ -1,7 +1,7 @@
 apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
-  name: backend-namespaces
+  name: backend-tls-setting
   namespace: gateway-conformance-infra
 spec:
   gatewayClassName: "{GATEWAY_CLASS_NAME}"
@@ -13,11 +13,11 @@ spec:
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
-  name: http-with-backend-tls
+  name: backend-tls-setting
   namespace: gateway-conformance-infra
 spec:
   parentRefs:
-  - name: backend-namespaces
+  - name: backend-tls-setting
   rules:
   - matches:
     - path:
diff --git a/test/e2e/testdata/oidc-securitypolicy.yaml b/test/e2e/testdata/oidc-securitypolicy.yaml
index 0c34ab0e9..82e207929 100644
--- a/test/e2e/testdata/oidc-securitypolicy.yaml
+++ b/test/e2e/testdata/oidc-securitypolicy.yaml
@@ -77,3 +77,13 @@ spec:
       name: "oidctest-secret"
     redirectURL: "http://www.example.com/myapp/oauth2/callback"
     logoutPath: "/myapp/logout"
+    forwardAccessToken: true
+    passThroughAuthHeader: true
+  jwt:
+    providers:
+    - name: "keycloak"  # This is needed so JWTs generated through the OIDC flow can be validated
+      remoteJWKS:
+        uri: "http://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/certs"
+    - name: "example"  # This allows us to use the static JWTs in the test
+      remoteJWKS:
+        uri: "http://static-file-server.gateway-conformance-infra/jwt/jwks.json"
diff --git a/test/e2e/tests/backend_tls_settings.go b/test/e2e/tests/backend_tls_settings.go
index 41d7bb03c..3d196c7ea 100644
--- a/test/e2e/tests/backend_tls_settings.go
+++ b/test/e2e/tests/backend_tls_settings.go
@@ -53,8 +53,8 @@ var BackendTLSSettingsTest = suite.ConformanceTest{
 		t.Run("Apply custom TLS settings when making backend requests.", func(t *testing.T) {
 			depNS := "envoy-gateway-system"
 			ns := "gateway-conformance-infra"
-			routeNN := types.NamespacedName{Name: "http-with-backend-tls", Namespace: ns}
-			gwNN := types.NamespacedName{Name: "backend-namespaces", Namespace: ns}
+			routeNN := types.NamespacedName{Name: "backend-tls-setting", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "backend-tls-setting", Namespace: ns}
 			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
 			kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS})
 			backendTLSPolicy := &gwapiv1a3.BackendTLSPolicy{}
diff --git a/test/e2e/tests/eg_upgrade.go b/test/e2e/tests/eg_upgrade.go
index 7e37aa454..caeef8268 100644
--- a/test/e2e/tests/eg_upgrade.go
+++ b/test/e2e/tests/eg_upgrade.go
@@ -52,7 +52,7 @@ var EGUpgradeTest = suite.ConformanceTest{
 			chartPath := "../../../charts/gateway-helm"
 			relName := "eg"
 			depNS := "envoy-gateway-system"
-			lastVersionTag := "v1.3.2" //  the latest prior release
+			lastVersionTag := "v1.4.0" //  the latest prior release
 
 			t.Logf("Upgrading from version: %s", lastVersionTag)
 
@@ -60,7 +60,13 @@ var EGUpgradeTest = suite.ConformanceTest{
 			relNamespace := "envoy-gateway-system"
 			options.DefaultConfigFlags.Namespace = ptr.To(relNamespace)
 
-			ht := helm.NewPackageTool()
+			// use values file for e2e test
+			valuesFile := "../../config/helm/default.yaml"
+			if IsGatewayNamespaceMode() {
+				valuesFile = "../../config/helm/gateway-namespace-mode.yaml"
+			}
+
+			ht := helm.NewPackageTool(valuesFile)
 			if err := ht.Setup(); err != nil {
 				t.Errorf("failed to setup of packageTool: %v", err)
 			}
@@ -243,7 +249,7 @@ func upgradeChartFromPath(relName, relNamespace, chartPath string, timeout time.
 		return err
 	}
 
-	_, err = upgrade.Run(relName, gatewayChart, map[string]interface{}{})
+	_, err = upgrade.Run(relName, gatewayChart, map[string]any{})
 	if err != nil {
 		return err
 	}
diff --git a/test/e2e/tests/oidc.go b/test/e2e/tests/oidc.go
index a633e4ad1..dfbaedb8c 100644
--- a/test/e2e/tests/oidc.go
+++ b/test/e2e/tests/oidc.go
@@ -54,7 +54,7 @@ var OIDCTest = suite.ConformanceTest{
 			testOIDC(t, suite, "testdata/oidc-securitypolicy.yaml")
 		})
 
-		t.Run("http route without oidc authentication", func(t *testing.T) {
+		t.Run("oidc bypass", func(t *testing.T) {
 			ns := "gateway-conformance-infra"
 
 			podInitialized := corev1.PodCondition{Type: corev1.PodInitialized, Status: corev1.ConditionTrue}
@@ -64,9 +64,10 @@ var OIDCTest = suite.ConformanceTest{
 			// Apply the security policy that configures OIDC authentication
 			suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, "testdata/oidc-securitypolicy.yaml", true)
 
-			routeNN := types.NamespacedName{Name: "http-without-oidc", Namespace: ns}
+			routeWithOIDCNN := types.NamespacedName{Name: "http-with-oidc", Namespace: ns}
+			routeWithoutOIDCNN := types.NamespacedName{Name: "http-without-oidc", Namespace: ns}
 			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeWithOIDCNN, routeWithoutOIDCNN)
 
 			ancestorRef := gwapiv1a2.ParentReference{
 				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
@@ -76,25 +77,50 @@ var OIDCTest = suite.ConformanceTest{
 			}
 			SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "oidc-test", Namespace: ns}, suite.ControllerName, ancestorRef)
 
-			expectedResponse := gwhttp.ExpectedResponse{
-				Request: gwhttp.Request{
-					Host: "www.example.com",
-					Path: "/public",
+			testCases := []gwhttp.ExpectedResponse{
+				{
+					TestCaseName: "http route without oidc authentication",
+					Request: gwhttp.Request{
+						Host: "www.example.com",
+						Path: "/public",
+					},
+					Response: gwhttp.Response{
+						StatusCode: 200,
+					},
+					Namespace: ns,
 				},
-				Response: gwhttp.Response{
-					StatusCode: 200,
+				{
+					TestCaseName: "oidc with jwt passthrough",
+					Request: gwhttp.Request{
+						Host: "www.example.com",
+						Path: "/myapp",
+						Headers: map[string]string{
+							"Authorization": "Bearer " + v1Token,
+						},
+					},
+					Backend: "infra-backend-v1",
+					Response: gwhttp.Response{
+						StatusCode: 200,
+					},
+					Namespace: ns,
 				},
-				Namespace: ns,
-			}
-
-			req := gwhttp.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http")
-			cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req)
-			if err != nil {
-				t.Errorf("failed to get expected response: %v", err)
 			}
 
-			if err := gwhttp.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil {
-				t.Errorf("failed to compare request and response: %v", err)
+			for i := range testCases {
+				tc := testCases[i]
+				t.Run(tc.GetTestCaseName(i), func(t *testing.T) {
+					t.Parallel()
+
+					req := gwhttp.MakeRequest(t, &tc, gwAddr, "HTTP", "http")
+					cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req)
+					if err != nil {
+						t.Errorf("failed to get expected response: %v", err)
+					}
+
+					if err := gwhttp.CompareRequest(t, &req, cReq, cResp, tc); err != nil {
+						t.Errorf("failed to compare request and response: %v", err)
+					}
+				})
 			}
 		})
 	},
diff --git a/test/e2e/tests/session_persistence.go b/test/e2e/tests/session_persistence.go
index 4448ba885..ccc5b1be9 100644
--- a/test/e2e/tests/session_persistence.go
+++ b/test/e2e/tests/session_persistence.go
@@ -41,9 +41,20 @@ var HeaderBasedSessionPersistenceTest = suite.ConformanceTest{
 				},
 			}, gwAddr, "HTTP", "http")
 
+			// Make sure backend is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/v2",
+				},
+				Response: httputils.Response{
+					StatusCode: http.StatusOK,
+				},
+				Namespace: ns,
+			})
+
 			pod := ""
 			// We make 10 requests to the gateway and expect them to be routed to the same pod.
-			for i := 0; i < 10; i++ {
+			for i := range 10 {
 				captReq, res, err := suite.RoundTripper.CaptureRoundTrip(req)
 				if err != nil {
 					t.Fatalf("failed to make request: %v", err)
@@ -83,6 +94,17 @@ var HeaderBasedSessionPersistenceTest = suite.ConformanceTest{
 				},
 			}, gwAddr, "HTTP", "http")
 
+			// Make sure backend is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/v1",
+				},
+				Response: httputils.Response{
+					StatusCode: http.StatusOK,
+				},
+				Namespace: ns,
+			})
+
 			_, res, err := suite.RoundTripper.CaptureRoundTrip(req)
 			if err != nil {
 				t.Fatalf("failed to make request: %v", err)
@@ -105,15 +127,27 @@ var CookieBasedSessionPersistenceTest = suite.ConformanceTest{
 			routeNN := types.NamespacedName{Name: "cookie-based-session-persistence", Namespace: ns}
 			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
 			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
 			req := httputils.MakeRequest(t, &httputils.ExpectedResponse{
 				Request: httputils.Request{
 					Path: "/v2",
 				},
 			}, gwAddr, "HTTP", "http")
 
+			// Make sure backend is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/v2",
+				},
+				Response: httputils.Response{
+					StatusCode: http.StatusOK,
+				},
+				Namespace: ns,
+			})
+
 			pod := ""
 			// We make 10 requests to the gateway and expect them to be routed to the same pod.
-			for i := 0; i < 10; i++ {
+			for i := range 10 {
 				captReq, res, err := suite.RoundTripper.CaptureRoundTrip(req)
 				if err != nil {
 					t.Fatalf("failed to make request: %v", err)
@@ -166,6 +200,17 @@ var CookieBasedSessionPersistenceTest = suite.ConformanceTest{
 				},
 			}, gwAddr, "HTTP", "http")
 
+			// Make sure backend is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/v1",
+				},
+				Response: httputils.Response{
+					StatusCode: http.StatusOK,
+				},
+				Namespace: ns,
+			})
+
 			_, res, err := suite.RoundTripper.CaptureRoundTrip(req)
 			if err != nil {
 				t.Fatalf("failed to make request: %v", err)
diff --git a/test/e2e/upgrade/eg_upgrade_test.go b/test/e2e/upgrade/eg_upgrade_test.go
index 509b874fb..12bb99850 100644
--- a/test/e2e/upgrade/eg_upgrade_test.go
+++ b/test/e2e/upgrade/eg_upgrade_test.go
@@ -64,18 +64,10 @@ func TestEGUpgrade(t *testing.T) {
 		t.Fatalf("Failed to create test suite: %v", err)
 	}
 
-	if tests.IsGatewayNamespaceMode() {
-		tests.UpgradeTests = []suite.ConformanceTest{
-			tests.EnvoyShutdownTest,
-			// TODO: enable EGUpgradeTest in gateway namespace mode
-			// tests.EGUpgradeTest,
-		}
-	} else {
-		// upgrade tests should be executed in a specific order
-		tests.UpgradeTests = []suite.ConformanceTest{
-			tests.EnvoyShutdownTest,
-			tests.EGUpgradeTest,
-		}
+	// upgrade tests should be executed in a specific order
+	tests.UpgradeTests = []suite.ConformanceTest{
+		tests.EnvoyShutdownTest,
+		tests.EGUpgradeTest,
 	}
 
 	tlog.Logf(t, "Running %d Upgrade tests", len(tests.UpgradeTests))
diff --git a/test/fuzz/testdata/FuzzGatewayAPIToXDS/grpc_routing b/test/fuzz/testdata/FuzzGatewayAPIToXDS/grpc_routing
index af3477bf7..abd6e9ce2 100644
--- a/test/fuzz/testdata/FuzzGatewayAPIToXDS/grpc_routing
+++ b/test/fuzz/testdata/FuzzGatewayAPIToXDS/grpc_routing
@@ -37,13 +37,13 @@ spec:
             - name: com.example.Header
               value: foobar
       backendRefs:
-        - name: providedBackend
+        - name: provided-backend
           port: 9000
 ---
 apiVersion: gateway.envoyproxy.io/v1alpha1
 kind: Backend
 metadata:
-  name: providedBackend
+  name: provided-backend
   namespace: default
 spec:
   endpoints:
diff --git a/test/fuzz/testdata/FuzzGatewayAPIToXDS/http_routing b/test/fuzz/testdata/FuzzGatewayAPIToXDS/http_routing
index eb6b97651..cbcc895dc 100644
--- a/test/fuzz/testdata/FuzzGatewayAPIToXDS/http_routing
+++ b/test/fuzz/testdata/FuzzGatewayAPIToXDS/http_routing
@@ -29,13 +29,13 @@ spec:
     - "www.example.com"
   rules:
     - backendRefs:
-        - name: providedBackend
+        - name: provided-backend
           port: 8000
 ---
 apiVersion: gateway.envoyproxy.io/v1alpha1
 kind: Backend
 metadata:
-  name: providedBackend
+  name: provided-backend
   namespace: default
 spec:
   endpoints:
diff --git a/test/fuzz/testdata/FuzzGatewayAPIToXDS/udp_routing b/test/fuzz/testdata/FuzzGatewayAPIToXDS/udp_routing
index 9b8642c14..ea2e1b965 100644
--- a/test/fuzz/testdata/FuzzGatewayAPIToXDS/udp_routing
+++ b/test/fuzz/testdata/FuzzGatewayAPIToXDS/udp_routing
@@ -34,7 +34,7 @@ spec:
 apiVersion: gateway.envoyproxy.io/v1alpha1
 kind: Backend
 metadata:
-  name: providedBackend
+  name: provided-backend
   namespace: default
 spec:
   endpoints:
diff --git a/test/fuzz/xds_fuzz_test.go b/test/fuzz/xds_fuzz_test.go
index 38885226f..95120682f 100644
--- a/test/fuzz/xds_fuzz_test.go
+++ b/test/fuzz/xds_fuzz_test.go
@@ -9,24 +9,114 @@ import (
 	"strings"
 	"testing"
 
-	fuzz "github.com/AdaLogics/go-fuzz-headers"
-
 	"github.com/envoyproxy/gateway/internal/cmd/egctl"
 	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 )
 
 func FuzzGatewayAPIToXDS(f *testing.F) {
+	baseYAML := `apiVersion: gateway.networking.k8s.io/v1
+kind: GatewayClass
+metadata:
+  name: eg
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: eg
+  namespace: default
+spec:
+  gatewayClassName: eg
+  listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+`
+
+	backendYAML := `---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: Backend
+metadata:
+  name: provided-backend
+  namespace: default
+spec:
+  endpoints:
+    - ip:
+        address: 0.0.0.0
+        port: 8000
+`
+
+	grpcRoute := `---
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+  name: backend
+  namespace: default
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: grpc
+  hostnames:
+    - "www.grpc-example.com"
+  rules:
+    - matches:
+        - method:
+            service: com.example.Things
+            method: DoThing
+          headers:
+            - name: com.example.Header
+              value: foobar
+      backendRefs:
+        - name: provided-backend
+          port: 9000
+`
+
+	httpRoute := `---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: backend
+  namespace: default
+spec:
+  parentRefs:
+    - name: eg
+  hostnames:
+    - "www.example.com"
+  rules:
+    - backendRefs:
+        - name: provided-backend
+          port: 8000
+`
+
+	udpRoute := `---
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: UDPRoute
+metadata:
+  name: backend
+  namespace: default
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: udp
+  rules:
+    - backendRefs:
+        - name: provided-backend
+          port: 3000
+`
+
+	// Add seed corpus for golang native fuzzing. OSS-Fuzz will not take these corpus into consideration.
+	f.Add([]byte(baseYAML + grpcRoute + backendYAML))
+	f.Add([]byte(baseYAML + httpRoute + backendYAML))
+	f.Add([]byte(baseYAML + udpRoute + backendYAML))
+
 	f.Fuzz(func(t *testing.T, b []byte) {
 		rs, err := resource.LoadResourcesFromYAMLBytes(b, true)
 		if err != nil {
 			return
 		}
-		fc := fuzz.NewConsumer(b)
-		namespace, _ := fc.GetString()
-		dnsDomain, _ := fc.GetString()
-		resourceType, _ := fc.GetString()
 
-		_, err = egctl.TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType, rs)
+		_, err = egctl.TranslateGatewayAPIToXds("default", "cluster.local", "all", rs)
 		if err != nil && strings.Contains(err.Error(), "failed to translate xds") {
 			t.Fatalf("%v", err)
 		}
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 2612431a8..fc3e84656 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -22006,6 +22006,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                         response:
@@ -22029,6 +22030,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                       type: object
@@ -42006,6 +42008,16 @@ spec:
 
                       If not specified, uses a default logout path "/logout"
                     type: string
+                  passThroughAuthHeader:
+                    description: |-
+                      Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
+                      explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
+
+                      The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
+                      redirects and wish to directly supply a token instead.
+
+                      If not specified, defaults to false.
+                    type: boolean
                   provider:
                     description: The OIDC Provider configuration.
                     properties:
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index 9dc657ce7..9db16013a 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -4694,6 +4694,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                         response:
@@ -4717,6 +4718,7 @@ spec:
                               - Streamed
                               - Buffered
                               - BufferedPartial
+                              - FullDuplexStreamed
                               type: string
                           type: object
                       type: object
@@ -24694,6 +24696,16 @@ spec:
 
                       If not specified, uses a default logout path "/logout"
                     type: string
+                  passThroughAuthHeader:
+                    description: |-
+                      Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
+                      explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
+
+                      The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
+                      redirects and wish to directly supply a token instead.
+
+                      If not specified, defaults to false.
+                    type: boolean
                   provider:
                     description: The OIDC Provider configuration.
                     properties: