Repository: envoyproxy/gateway

Issue:
docs: Add GatewayNamespaceMode infrastructure deployment mode page
*Description*:
>Describe the desired behavior, what scenario it enables and how it
would be used.

Add the documentation needed to set up the infrastructure deployment mode, where envoy proxies are created in the namespace of their corresponding gateways.

[optional *Relevant Links*:]
>Any extra documentation required to understand the issue.


Hints from issue comments:
@zirain: @cnvergence we should hightlight that this's an experimental/alpha feature, which will give us free to make changes(e.g. the naming pattern).@zirain: By now, ratelimit/wasm is not support in gateway namespace mode.

Solution patch:
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/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/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"
+```



Test patch:
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/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"
+```