Repository: envoyproxy/gateway
Pull request #5914 (2025-05-04 02:35:07)

Feat: Extension server policy propagation to PostTranslateModify
**What type of PR is this?**
* "feat(translator): Add extension server policies to PostTranslateContext so that extension server can take actions accordingly."

**What this PR does / why we need it**:
Add custom extension server policies to PostTranslateContext and eventually pass to PostTranslateModify() method so that extension server can take actions accordingly based on CRDs.

Release Notes: Yes


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `30.00000%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 65.17%. Comparing base [(`d68f7a2`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/d68f7a2ccc1b5d0604f775ca22eced6071f24d81?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`33ad861`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/33ad8612e8da5c49cf3948d27336daad1215af21?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 128 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/extension/registry/xds\_hook.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?src=pr&el=tree&filepath=internal%2Fextension%2Fregistry%2Fxds_hook.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZXh0ZW5zaW9uL3JlZ2lzdHJ5L3hkc19ob29rLmdv) | 0.00% | [11 Missing :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/xds/translator/extension.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Fextension.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvZXh0ZW5zaW9uLmdv) | 50.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

:x: Your patch check has failed because the patch coverage (30.00%) is below the target coverage (60.00%). You can increase the patch coverage or adjust the [target](https://docs.codecov.com/docs/commit-status#target) coverage.

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


```diff
@@            Coverage Diff             @@
##             main    #5914      +/-   ##
==========================================
- Coverage   65.19%   65.17%   -0.02%     
==========================================
  Files         215      215              
  Lines       35833    35848      +15     
==========================================
+ Hits        23360    23365       +5     
- Misses      11028    11038      +10     
  Partials     1445     1445              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5914?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>
guydc: cc @nareddyt 
raviverma-ai: > hey @raviverma-ai can you share your use case, curious why the other hooks aren't applicable

This enables users to perform actions which needs modifications to clusters (other hooks are not proving options to modify clusters). In our particular use case, we would like to enable sds based mtls certificate issuance and rotation instead of manually updating certificates. This requires clusters to have UpstreamTlsContext pointing to sds server and get certificates automatically. In our case, we are creating native integration with Istio and to avoid sidecar injection on EG.
raviverma-ai: @arkodg, Yes, it would give more control to user to take actions on. For instance, we are using extension server policy CRD to enable this feature bound to specific gateway. i.e. If user wants to enable sds based mtls for specific gateway and not for all gateways. Furthermore, we are planning to provide even more control in future like user would be able to control this behaviour on httproute or backend/service basis (still under discussion for feasibility and use case).
guydc: cc @liorokman 
raviverma-ai: > hi @raviverma-ai. Thoughts on this comment from earlier?
> 
> > Can we avoid saving the extension policies twice per XDS? they are saved for both the entire XDS and the IR Listener now.

@guydc thanks for reminding. Updated logic to store the pointer to original policies instead of creating another copy.
raviverma-ai: > > @guydc thanks for reminding. Updated logic to store the pointer to original policies instead of creating another copy.
> 
> I think that this should be reflected in the test files as well. Can you regenerate the test output files with `make go.testdata.complete` so that we can see the impact?
> 
> Also, please add a few test cases for this new flow to `internal/xds/testdata/extension-xds-ir` and extend `testingExtensionServer` to have some logic to cover:
> 
> * Creation of new secrets/clusters based on extension resources
> * Emitting errors and handling them in the extension manager

@guydc added test cases as requested.
raviverma-ai: @guydc As I had mentioned earlier as well. For this or any similar use case, where user only needs to know which Gateway, that specific Extension Server Policy is applicable to, i.e. all dynamic cluster can be modified/reviewed or even specific ones as well, those information can be specified in Custom Extension Server Policy defined by user. For now, we will be using this to enable mtls via sds server on all backend communications. However, there is plan for future to even further fine tune and target specific route/service etc.
Hope, I was able to answer your question.

Patch:
diff --git a/.github/workflows/experimental_conformance.yaml b/.github/workflows/experimental_conformance.yaml
index 2422c945a..fbd1a5daf 100644
--- a/.github/workflows/experimental_conformance.yaml
+++ b/.github/workflows/experimental_conformance.yaml
@@ -1,13 +1,16 @@
 name: Experimental Conformance Test
 on:
   push:
+    # Sequence of patterns matched against refs/tags
+    tags:
+      - "v*.*.*"
     paths:
-    - 'charts/gateway-helm/crds/gatewayapi-crds.yaml'
-    - 'test/conformance/experimental_conformance_test.go'
+      - "charts/gateway-helm/crds/gatewayapi-crds.yaml"
+      - "test/conformance/experimental_conformance_test.go"
   pull_request:
     paths:
-    - 'charts/gateway-helm/crds/gatewayapi-crds.yaml'
-    - 'test/conformance/experimental_conformance_test.go'
+      - "charts/gateway-helm/crds/gatewayapi-crds.yaml"
+      - "test/conformance/experimental_conformance_test.go"
   # Add workflow_dispatch to trigger this workflow manually by maintainers.
   workflow_dispatch:
 
@@ -19,21 +22,42 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        version: [ v1.30.10, v1.31.6, v1.32.3, v1.33.0 ]
+        target:
+          - version: v1.30.10
+            ipFamily: ipv4
+            profile: default
+          - version: v1.31.6
+            ipFamily: ipv4
+            profile: default
+          - version: v1.32.3
+            # only run ipv6 test on this version to save time
+            ipFamily: ipv6
+            profile: default
+          # TODO: this's IPv4 first, need a way to test IPv6 first.
+          - version: v1.33.0
+            # only run dual test on latest version to save time
+            ipFamily: dual
+            profile: default
+          - version: v1.33.0
+            # only run dual test on latest version to save time
+            ipFamily: dual
+            profile: gateway-namespace-mode
     steps:
-    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
-    - uses: ./tools/github-actions/setup-deps
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
+      - uses: ./tools/github-actions/setup-deps
 
-    # gateway api experimental conformance
-    - name: Run Experimental Conformance Tests
-      env:
-        CONFORMANCE_REPORT_PATH: conformance-report-k8s-${{ matrix.version }}.yaml
-        KIND_NODE_TAG: ${{ matrix.version }}
-        IMAGE_PULL_POLICY: IfNotPresent
-      run: make experimental-conformance
+      # gateway api experimental conformance
+      - name: Run Experimental Conformance Tests
+        env:
+          IP_FAMILY: ${{ matrix.target.ipFamily }}
+          KUBE_DEPLOY_PROFILE: ${{ matrix.target.profile }}
+          CONFORMANCE_REPORT_PATH: conformance-report-k8s-${{ matrix.target.version }}-${{ matrix.target.profile }}.yaml
+          KIND_NODE_TAG: ${{ matrix.target.version }}
+          IMAGE_PULL_POLICY: IfNotPresent
+        run: make experimental-conformance
 
-    - name: Upload Conformance Report
-      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
-      with:
-        name: conformance-report-k8s-${{ matrix.version }}
-        path: ./test/conformance/conformance-report-k8s-${{ matrix.version }}.yaml
+      - name: Upload Conformance Report
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
+        with:
+          name: conformance-report-k8s-${{ matrix.target.version }}-${{ matrix.target.profile }}
+          path: ./test/conformance/conformance-report-k8s-${{ matrix.target.version }}-${{ matrix.target.profile }}.yaml
diff --git a/internal/extension/registry/xds_hook.go b/internal/extension/registry/xds_hook.go
index d21e29f44..976dac6fa 100644
--- a/internal/extension/registry/xds_hook.go
+++ b/internal/extension/registry/xds_hook.go
@@ -15,6 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
 	"github.com/envoyproxy/gateway/internal/extension/types"
+	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/proto/extension"
 )
 
@@ -105,14 +106,27 @@ func (h *XDSHook) PostHTTPListenerModifyHook(l *listener.Listener, extensionReso
 	return resp.Listener, nil
 }
 
-func (h *XDSHook) PostTranslateModifyHook(clusters []*cluster.Cluster, secrets []*tls.Secret) ([]*cluster.Cluster, []*tls.Secret, error) {
+func (h *XDSHook) PostTranslateModifyHook(clusters []*cluster.Cluster, secrets []*tls.Secret, extensionPolicies []*ir.UnstructuredRef) ([]*cluster.Cluster, []*tls.Secret, error) {
 	// Make the request to the extension server
+	// Take all of the unstructured resources for the extension and package them into bytes
+	unstructuredPolicies := make([]*unstructured.Unstructured, len(extensionPolicies))
+	for i, policy := range extensionPolicies {
+		unstructuredPolicies[i] = policy.Object
+	}
+	// Convert the unstructured policies to bytes
+	extensionPoliciesBytes, err := translateUnstructuredToUnstructuredBytes(unstructuredPolicies)
+	if err != nil {
+		return nil, nil, err
+	}
+
 	ctx := context.Background()
 	resp, err := h.grpcClient.PostTranslateModify(ctx,
 		&extension.PostTranslateModifyRequest{
-			PostTranslateContext: &extension.PostTranslateExtensionContext{},
-			Clusters:             clusters,
-			Secrets:              secrets,
+			PostTranslateContext: &extension.PostTranslateExtensionContext{
+				ExtensionResources: extensionPoliciesBytes,
+			},
+			Clusters: clusters,
+			Secrets:  secrets,
 		})
 	if err != nil {
 		return nil, nil, err
diff --git a/internal/extension/types/hooks.go b/internal/extension/types/hooks.go
index 08d8c4ede..21350696b 100644
--- a/internal/extension/types/hooks.go
+++ b/internal/extension/types/hooks.go
@@ -11,6 +11,8 @@ import (
 	route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"github.com/envoyproxy/gateway/internal/ir"
 )
 
 type XDSHookClient interface {
@@ -41,5 +43,5 @@ type XDSHookClient interface {
 	// An example of how this may be used is to inject a cluster that will be used by an ext_authz http filter created by the extension.
 	// The list of clusters and secrets returned by the extension are used as the final list of all clusters and secrets
 	// PostTranslateModifyHook is always executed when an extension is loaded
-	PostTranslateModifyHook([]*cluster.Cluster, []*tls.Secret) ([]*cluster.Cluster, []*tls.Secret, error)
+	PostTranslateModifyHook([]*cluster.Cluster, []*tls.Secret, []*ir.UnstructuredRef) ([]*cluster.Cluster, []*tls.Secret, error)
 }
diff --git a/internal/gatewayapi/extensionserverpolicy.go b/internal/gatewayapi/extensionserverpolicy.go
index 395a30ef3..e7f65f57a 100644
--- a/internal/gatewayapi/extensionserverpolicy.go
+++ b/internal/gatewayapi/extensionserverpolicy.go
@@ -46,7 +46,7 @@ func (t *Translator) ProcessExtensionServerPolicies(policies []unstructured.Unst
 	var errs error
 	// Process the policies targeting Gateways. Only update the policy status if it was accepted.
 	// A policy is considered accepted if at least one targetRef contained inside matched a listener.
-	for _, policy := range policies {
+	for policyIndex, policy := range policies {
 		policy := policy.DeepCopy()
 		var policyStatus gwapiv1a2.PolicyStatus
 		accepted := false
@@ -68,6 +68,13 @@ func (t *Translator) ProcessExtensionServerPolicies(policies []unstructured.Unst
 				continue
 			}
 
+			// Append policy extension server policy list for related gateway.
+			gatewayKey := t.getIRKey(gateway.Gateway)
+			unstructuredPolicy := &ir.UnstructuredRef{
+				Object: &policies[policyIndex],
+			}
+			xdsIR[gatewayKey].ExtensionServerPolicies = append(xdsIR[gatewayKey].ExtensionServerPolicies, unstructuredPolicy)
+
 			// Set conditions for translation if it got any
 			if t.translateExtServerPolicyForGateway(policy, gateway, currTarget, xdsIR) {
 				// Set Accepted condition if it is unset
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 08cc1cb07..c2c3c39cc 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -153,6 +153,8 @@ type Xds struct {
 	FilterOrder []egv1a1.FilterPosition `json:"filterOrder,omitempty" yaml:"filterOrder,omitempty"`
 	// GlobalResources holds the global resources used by the Envoy, for example, the envoy client certificate and the OIDC HMAC secret
 	GlobalResources *GlobalResources `json:"globalResources,omitempty" yaml:"globalResources,omitempty"`
+	// ExtensionServerPolicies is the intermediate representation of the ExtensionServerPolicy resource
+	ExtensionServerPolicies []*UnstructuredRef `json:"extensionServerPolicies,omitempty" yaml:"extensionServerPolicies,omitempty"`
 }
 
 // Equal implements the Comparable interface used by watchable.DeepEqual to skip unnecessary updates.
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 5dda160da..de924f1d5 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -4091,6 +4091,17 @@ func (in *Xds) DeepCopyInto(out *Xds) {
 		*out = new(GlobalResources)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ExtensionServerPolicies != nil {
+		in, out := &in.ExtensionServerPolicies, &out.ExtensionServerPolicies
+		*out = make([]*UnstructuredRef, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(UnstructuredRef)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Xds.
diff --git a/internal/xds/translator/extension.go b/internal/xds/translator/extension.go
index 77162e14f..771a4d7be 100644
--- a/internal/xds/translator/extension.go
+++ b/internal/xds/translator/extension.go
@@ -139,7 +139,7 @@ func processExtensionPostListenerHook(tCtx *types.ResourceVersionTable, xdsListe
 	return nil
 }
 
-func processExtensionPostTranslationHook(tCtx *types.ResourceVersionTable, em *extensionTypes.Manager) error {
+func processExtensionPostTranslationHook(tCtx *types.ResourceVersionTable, em *extensionTypes.Manager, policies []*ir.UnstructuredRef) error {
 	// Do nothing unless there is an extension manager
 	if em == nil {
 		return nil
@@ -169,7 +169,7 @@ func processExtensionPostTranslationHook(tCtx *types.ResourceVersionTable, em *e
 		oldSecrets[idx] = secret.(*tlsv3.Secret)
 	}
 
-	newClusters, newSecrets, err := extensionInsertHookClient.PostTranslateModifyHook(oldClusters, oldSecrets)
+	newClusters, newSecrets, err := extensionInsertHookClient.PostTranslateModifyHook(oldClusters, oldSecrets, policies)
 	if err != nil {
 		return err
 	}
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 79e59b39a..b89731fc1 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -142,7 +142,7 @@ func (t *Translator) Translate(xdsIR *ir.Xds) (*types.ResourceVersionTable, erro
 
 	// Check if an extension want to inject any clusters/secrets
 	// If no extension exists (or it doesn't subscribe to this hook) then this is a quick no-op
-	if err := processExtensionPostTranslationHook(tCtx, t.ExtensionManager); err != nil {
+	if err := processExtensionPostTranslationHook(tCtx, t.ExtensionManager, xdsIR.ExtensionServerPolicies); err != nil {
 		// If the extension server returns an error, and the extension server is not configured to fail open,
 		// then propagate the error
 		if !(*t.ExtensionManager).FailOpen() {
diff --git a/proto/extension/context.pb.go b/proto/extension/context.pb.go
index f39a09711..707c782ac 100644
--- a/proto/extension/context.pb.go
+++ b/proto/extension/context.pb.go
@@ -174,9 +174,12 @@ func (x *PostHTTPListenerExtensionContext) GetExtensionResources() []*ExtensionR
 // breaking any clients that use the API
 // additional context information can be added to this message as more use-cases are discovered
 type PostTranslateExtensionContext struct {
-	state         protoimpl.MessageState `protogen:"open.v1"`
-	unknownFields protoimpl.UnknownFields
-	sizeCache     protoimpl.SizeCache
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Resources/Policies introduced by the extension that were used as extension server
+	// policies targeting the clusters
+	ExtensionResources []*ExtensionResource `protobuf:"bytes,1,rep,name=extension_resources,json=extensionResources,proto3" json:"extension_resources,omitempty"`
+	unknownFields      protoimpl.UnknownFields
+	sizeCache          protoimpl.SizeCache
 }
 
 func (x *PostTranslateExtensionContext) Reset() {
@@ -209,6 +212,13 @@ func (*PostTranslateExtensionContext) Descriptor() ([]byte, []int) {
 	return file_proto_extension_context_proto_rawDescGZIP(), []int{3}
 }
 
+func (x *PostTranslateExtensionContext) GetExtensionResources() []*ExtensionResource {
+	if x != nil {
+		return x.ExtensionResources
+	}
+	return nil
+}
+
 // ExtensionResource stores the data for a K8s API object referenced in an HTTPRouteFilter
 // extensionRef. It is constructed from an unstructured.Unstructured marshalled to JSON. An extension
 // can marshal the bytes from this resource back into an unstructured.Unstructured and then
@@ -283,15 +293,20 @@ var file_proto_extension_context_proto_rawDesc = string([]byte{
 	0x61, 0x79, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x74,
 	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x12,
 	0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
-	0x65, 0x73, 0x22, 0x1f, 0x0a, 0x1d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c,
+	0x65, 0x73, 0x22, 0x7b, 0x0a, 0x1d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c,
 	0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74,
-	0x65, 0x78, 0x74, 0x22, 0x42, 0x0a, 0x11, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
-	0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x6e, 0x73, 0x74,
-	0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x75, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72,
-	0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x42, 0x11, 0x5a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x65, 0x78, 0x74, 0x12, 0x5a, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
+	0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x29, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e,
+	0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73,
+	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x12, 0x65, 0x78, 0x74,
+	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22,
+	0x42, 0x0a, 0x11, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f,
+	0x75, 0x72, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74,
+	0x75, 0x72, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x11, 0x75, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x64, 0x42, 0x79,
+	0x74, 0x65, 0x73, 0x42, 0x11, 0x5a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x74,
+	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 })
 
 var (
@@ -317,11 +332,12 @@ var file_proto_extension_context_proto_goTypes = []any{
 var file_proto_extension_context_proto_depIdxs = []int32{
 	4, // 0: envoygateway.extension.PostRouteExtensionContext.extension_resources:type_name -> envoygateway.extension.ExtensionResource
 	4, // 1: envoygateway.extension.PostHTTPListenerExtensionContext.extension_resources:type_name -> envoygateway.extension.ExtensionResource
-	2, // [2:2] is the sub-list for method output_type
-	2, // [2:2] is the sub-list for method input_type
-	2, // [2:2] is the sub-list for extension type_name
-	2, // [2:2] is the sub-list for extension extendee
-	0, // [0:2] is the sub-list for field type_name
+	4, // 2: envoygateway.extension.PostTranslateExtensionContext.extension_resources:type_name -> envoygateway.extension.ExtensionResource
+	3, // [3:3] is the sub-list for method output_type
+	3, // [3:3] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
 }
 
 func init() { file_proto_extension_context_proto_init() }
diff --git a/proto/extension/context.proto b/proto/extension/context.proto
index cff232343..6981c5eba 100644
--- a/proto/extension/context.proto
+++ b/proto/extension/context.proto
@@ -43,7 +43,9 @@ message PostHTTPListenerExtensionContext {
 // breaking any clients that use the API
 // additional context information can be added to this message as more use-cases are discovered
 message PostTranslateExtensionContext {
-
+    // Resources/Policies introduced by the extension that were used as extension server
+    // policies targeting the clusters
+    repeated ExtensionResource extension_resources = 1;
 }
 
 
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index e58dfcc56..62338fa9e 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -17,6 +17,7 @@ new features: |
   Added support for setting ownerreference to infra resources when enable gateway namespace mode.
   Added support for configuring hostname in active HTTP healthchecks.
   Added support for GatewayInfrastructure in gateway namespace mode.
+  Added support for using extension server policies to in PostTranslateModify hook.
 
 bug fixes: |
   Handle integer zone annotation values
diff --git a/tools/docker/envoy-gateway/Dockerfile b/tools/docker/envoy-gateway/Dockerfile
index a21dee5c9..24f5480bb 100644
--- a/tools/docker/envoy-gateway/Dockerfile
+++ b/tools/docker/envoy-gateway/Dockerfile
@@ -1,13 +1,13 @@
-FROM busybox@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f AS source
+FROM busybox@sha256:f85340bf132ae937d2c2a763b8335c9bab35d6e8293f70f606b9c6178d84f42b AS source
 # Create the data directory for eg
-RUN mkdir -p /var/lib/eg
+RUN mkdir -p /var/lib/eg && chmod -R 0777 /var/lib/eg
 
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
 FROM gcr.io/distroless/base-nossl:nonroot@sha256:ecbab76d6a504ddf7c58a9d786e70f1f1731fa546b1ac0b20dab35c6fc2f3138
 ARG TARGETPLATFORM
-COPY --chown=65532:65532 $TARGETPLATFORM/envoy-gateway /usr/local/bin/
-COPY --from=source --chown=65532:65532 /var/lib /var/lib
+COPY $TARGETPLATFORM/envoy-gateway /usr/local/bin/
+COPY --from=source /var/lib /var/lib
 
 USER 65532:65532
 
diff --git a/tools/linter/yamllint/.yamllint b/tools/linter/yamllint/.yamllint
index a75e7b5c4..ae2e0b76b 100644
--- a/tools/linter/yamllint/.yamllint
+++ b/tools/linter/yamllint/.yamllint
@@ -14,6 +14,7 @@ ignore: |
   test/helm/gateway-crds-helm/
   examples/extension-server/charts/extension-server
   site/node_modules/
+  .vscode/
 
 rules:
   braces:


Test patch:
diff --git a/internal/extension/registry/extension_manager_test.go b/internal/extension/registry/extension_manager_test.go
index fc63af64a..c11bb3929 100644
--- a/internal/extension/registry/extension_manager_test.go
+++ b/internal/extension/registry/extension_manager_test.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"crypto/tls"
 	"crypto/x509"
+	"errors"
 	"fmt"
 	"math"
 	"net"
@@ -17,6 +18,7 @@ import (
 	"sync"
 	"testing"
 
+	clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
 	v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	"github.com/stretchr/testify/require"
 	"google.golang.org/grpc"
@@ -26,12 +28,14 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/utils/ptr"
 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway"
+	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/proto/extension"
 )
 
@@ -546,3 +550,179 @@ func Test_Integration_RetryPolicy_MaxAttempts(t *testing.T) {
 		})
 	}
 }
+
+type clusterUpdateTestServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func getTargetRefKind(obj *unstructured.Unstructured) (string, error) {
+	targetRef, found, err := unstructured.NestedMap(obj.Object, "spec", "targetRef")
+	if err != nil || !found {
+		return "", errors.New("targetRef not found or error")
+	}
+
+	kind, ok := targetRef["kind"].(string)
+	if !ok {
+		return "", errors.New("kind is not a string or missing in targetRef")
+	}
+
+	return kind, nil
+}
+
+func (s *clusterUpdateTestServer) PostTranslateModify(ctx context.Context, req *extension.PostTranslateModifyRequest) (*extension.PostTranslateModifyResponse, error) {
+	clusters := req.GetClusters()
+	if clusters == nil {
+		return &extension.PostTranslateModifyResponse{
+			Clusters: clusters,
+			Secrets:  req.GetSecrets(),
+		}, errors.New("No clusters found")
+	}
+
+	if len(req.PostTranslateContext.ExtensionResources) == 0 {
+		return &extension.PostTranslateModifyResponse{
+			Clusters: clusters,
+			Secrets:  req.GetSecrets(),
+		}, errors.New("No policy found")
+	}
+
+	for _, extensionResourceBytes := range req.PostTranslateContext.ExtensionResources {
+		extensionResource := unstructured.Unstructured{}
+		if err := extensionResource.UnmarshalJSON(extensionResourceBytes.UnstructuredBytes); err != nil {
+			return &extension.PostTranslateModifyResponse{
+				Clusters: clusters,
+				Secrets:  req.GetSecrets(),
+			}, err
+		}
+
+		targetKind, err := getTargetRefKind(&extensionResource)
+		if err != nil || extensionResource.GetObjectKind().GroupVersionKind().Kind != "ExampleExtPolicy" || targetKind != "Gateway" {
+			return &extension.PostTranslateModifyResponse{
+				Clusters: clusters,
+				Secrets:  req.GetSecrets(),
+			}, errors.New("No matching policy found")
+		}
+	}
+
+	ret := &extension.PostTranslateModifyResponse{
+		Clusters: clusters,
+		Secrets:  req.GetSecrets(),
+	}
+
+	return ret, nil
+}
+
+func Test_Integration_ClusterUpdateExtensionServer(t *testing.T) {
+	testCases := []struct {
+		name              string
+		extensionPolicies []*ir.UnstructuredRef
+		errorExpected     bool
+	}{
+		{
+			name: "valid extension policy with targetRef",
+			extensionPolicies: []*ir.UnstructuredRef{
+				{
+					Object: &unstructured.Unstructured{
+						Object: map[string]any{
+							"apiVersion": "gateway.example.io/v1",
+							"kind":       "ExampleExtPolicy",
+							"metadata": map[string]any{
+								"name":      "test",
+								"namespace": "test",
+							},
+							"spec": map[string]any{
+								"targetRef": map[string]any{
+									"group": "gateway.networking.k8s.io",
+									"kind":  "Gateway",
+									"name":  "test",
+								},
+								"data": "some data",
+							},
+						},
+					},
+				},
+			},
+			errorExpected: false,
+		},
+
+		{
+			name: "invalid extension policy - no target",
+			extensionPolicies: []*ir.UnstructuredRef{
+				{
+					Object: &unstructured.Unstructured{
+						Object: map[string]any{
+							"apiVersion": "gateway.example.io/v1alpha1",
+							"kind":       "ExampleExtPolicy",
+							"metadata": map[string]any{
+								"name":      "test",
+								"namespace": "test",
+							},
+							"spec": map[string]any{
+								"data": "some data",
+							},
+						},
+					},
+				},
+			},
+			errorExpected: true,
+		},
+		{
+			name: "invalid extension policy - no spec",
+			extensionPolicies: []*ir.UnstructuredRef{
+				{
+					Object: &unstructured.Unstructured{
+						Object: map[string]any{
+							"apiVersion": "gateway.example.io/v1alpha1",
+							"kind":       "ExampleExtPolicy",
+							"metadata": map[string]any{
+								"name":      "test",
+								"namespace": "test",
+							},
+						},
+					},
+				},
+			},
+			errorExpected: true,
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			extManager := egv1a1.ExtensionManager{
+				Hooks: &egv1a1.ExtensionHooks{
+					XDSTranslator: &egv1a1.XDSTranslatorHooks{
+						Post: []egv1a1.XDSTranslatorHook{
+							egv1a1.XDSTranslation,
+						},
+					},
+				},
+				Service: &egv1a1.ExtensionService{
+					BackendEndpoint: egv1a1.BackendEndpoint{
+						FQDN: &egv1a1.FQDNEndpoint{
+							Hostname: "example.foo",
+							Port:     44344,
+						},
+					},
+				},
+			}
+
+			mgr, _, err := NewInMemoryManager(extManager, &clusterUpdateTestServer{})
+			require.NoError(t, err)
+
+			hook, err := mgr.GetPostXDSHookClient(egv1a1.XDSTranslation)
+			require.NoError(t, err)
+			require.NotNil(t, hook)
+
+			_, _, err = hook.PostTranslateModifyHook(
+				[]*clusterv3.Cluster{
+					{
+						Name: "test-cluster",
+					},
+				}, nil, tt.extensionPolicies)
+
+			if (err != nil) != tt.errorExpected {
+				t.Errorf("PostRouteModifyHook() error = %v, errorExpected %v", err, tt.errorExpected)
+				return
+			}
+		})
+	}
+}
diff --git a/internal/gatewayapi/testdata/extensions/extensionpolicy-tcp-listener.out.yaml b/internal/gatewayapi/testdata/extensions/extensionpolicy-tcp-listener.out.yaml
index d1d6fb410..fe833ea2b 100644
--- a/internal/gatewayapi/testdata/extensions/extensionpolicy-tcp-listener.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/extensionpolicy-tcp-listener.out.yaml
@@ -146,6 +146,32 @@ xdsIR:
     accessLog:
       json:
       - path: /dev/stdout
+    extensionServerPolicies:
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test1
+          namespace: envoy-gateway
+        spec:
+          data: attached to all listeners
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test2
+          namespace: envoy-gateway
+        spec:
+          data: attached only to listener on port 80
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+            sectionName: tcp1
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
diff --git a/internal/gatewayapi/testdata/extensions/extensionpolicy-udp-listener.out.yaml b/internal/gatewayapi/testdata/extensions/extensionpolicy-udp-listener.out.yaml
index cea577c54..56e227eab 100644
--- a/internal/gatewayapi/testdata/extensions/extensionpolicy-udp-listener.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/extensionpolicy-udp-listener.out.yaml
@@ -146,6 +146,32 @@ xdsIR:
     accessLog:
       json:
       - path: /dev/stdout
+    extensionServerPolicies:
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test1
+          namespace: envoy-gateway
+        spec:
+          data: attached to all listeners
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test2
+          namespace: envoy-gateway
+        spec:
+          data: attached only to listener on port 162
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+            sectionName: udp1
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
diff --git a/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target-array.out.yaml b/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target-array.out.yaml
index 2f9c597bb..203efa806 100644
--- a/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target-array.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target-array.out.yaml
@@ -158,6 +158,22 @@ xdsIR:
     accessLog:
       json:
       - path: /dev/stdout
+    extensionServerPolicies:
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test2
+          namespace: envoy-gateway
+        spec:
+          data: attached to multiple gateways
+          targetRefs:
+          - group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-2
+          - group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
     http:
     - address: 0.0.0.0
       extensionRefs:
@@ -224,6 +240,22 @@ xdsIR:
     accessLog:
       json:
       - path: /dev/stdout
+    extensionServerPolicies:
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test2
+          namespace: envoy-gateway
+        spec:
+          data: attached to multiple gateways
+          targetRefs:
+          - group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-2
+          - group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
     http:
     - address: 0.0.0.0
       extensionRefs:
diff --git a/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target.out.yaml b/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target.out.yaml
index 9dea789d1..9dcb168f8 100644
--- a/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/extensionpolicy-with-valid-target.out.yaml
@@ -150,6 +150,32 @@ xdsIR:
     accessLog:
       json:
       - path: /dev/stdout
+    extensionServerPolicies:
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test1
+          namespace: envoy-gateway
+        spec:
+          data: attached to all listeners
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+    - object:
+        apiVersion: foo.example.io/v1alpha1
+        kind: Bar
+        metadata:
+          name: test2
+          namespace: envoy-gateway
+        spec:
+          data: attached only to listener on port 80
+          targetRef:
+            group: gateway.networking.k8s.io
+            kind: Gateway
+            name: gateway-1
+            sectionName: http2
     http:
     - address: 0.0.0.0
       extensionRefs:
diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go
index e46824594..4ae505cc6 100644
--- a/internal/gatewayapi/translator_test.go
+++ b/internal/gatewayapi/translator_test.go
@@ -891,27 +891,29 @@ func (m *mockWasmCache) Cleanup() {}
 // This allows us to use cmp.Diff to compare the types with field-level cmpopts.
 func xdsWithoutEqual(a *ir.Xds) any {
 	ret := struct {
-		ReadyListener      *ir.ReadyListener
-		AccessLog          *ir.AccessLog
-		Tracing            *ir.Tracing
-		Metrics            *ir.Metrics
-		HTTP               []*ir.HTTPListener
-		TCP                []*ir.TCPListener
-		UDP                []*ir.UDPListener
-		EnvoyPatchPolicies []*ir.EnvoyPatchPolicy
-		FilterOrder        []egv1a1.FilterPosition
-		GlobalResources    *ir.GlobalResources
+		ReadyListener           *ir.ReadyListener
+		AccessLog               *ir.AccessLog
+		Tracing                 *ir.Tracing
+		Metrics                 *ir.Metrics
+		HTTP                    []*ir.HTTPListener
+		TCP                     []*ir.TCPListener
+		UDP                     []*ir.UDPListener
+		EnvoyPatchPolicies      []*ir.EnvoyPatchPolicy
+		FilterOrder             []egv1a1.FilterPosition
+		GlobalResources         *ir.GlobalResources
+		ExtensionServerPolicies []*ir.UnstructuredRef
 	}{
-		ReadyListener:      a.ReadyListener,
-		AccessLog:          a.AccessLog,
-		Tracing:            a.Tracing,
-		Metrics:            a.Metrics,
-		HTTP:               a.HTTP,
-		TCP:                a.TCP,
-		UDP:                a.UDP,
-		EnvoyPatchPolicies: a.EnvoyPatchPolicies,
-		FilterOrder:        a.FilterOrder,
-		GlobalResources:    a.GlobalResources,
+		ReadyListener:           a.ReadyListener,
+		AccessLog:               a.AccessLog,
+		Tracing:                 a.Tracing,
+		Metrics:                 a.Metrics,
+		HTTP:                    a.HTTP,
+		TCP:                     a.TCP,
+		UDP:                     a.UDP,
+		EnvoyPatchPolicies:      a.EnvoyPatchPolicies,
+		FilterOrder:             a.FilterOrder,
+		GlobalResources:         a.GlobalResources,
+		ExtensionServerPolicies: a.ExtensionServerPolicies,
 	}
 
 	// Ensure we didn't drop an exported field.
diff --git a/internal/xds/translator/extensionserver_test.go b/internal/xds/translator/extensionserver_test.go
index 0579b0ca3..fc1449bf0 100644
--- a/internal/xds/translator/extensionserver_test.go
+++ b/internal/xds/translator/extensionserver_test.go
@@ -18,6 +18,7 @@ import (
 	routeV3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	tlsV3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/anypb"
 	"google.golang.org/protobuf/types/known/durationpb"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
@@ -264,6 +265,65 @@ func (t *testingExtensionServer) PostTranslateModify(_ context.Context, req *pb.
 		},
 	})
 
+	for _, extensionResourceBytes := range req.PostTranslateContext.ExtensionResources {
+		extensionResource := unstructured.Unstructured{}
+		if err := extensionResource.UnmarshalJSON(extensionResourceBytes.UnstructuredBytes); err != nil {
+			return response, err
+		}
+
+		targetKind, err := getTargetRefKind(&extensionResource)
+		if err != nil {
+			return response, fmt.Errorf("failed to get targetRef kind: %w", err)
+		}
+
+		if extensionResource.GetObjectKind().GroupVersionKind().Kind == "ExampleExtPolicy" && targetKind == "Gateway" {
+			upstreamTLS := &tlsV3.UpstreamTlsContext{
+				CommonTlsContext: &tlsV3.CommonTlsContext{
+					TlsCertificateSdsSecretConfigs: []*tlsV3.SdsSecretConfig{
+						{
+							Name: "default",
+							SdsConfig: &coreV3.ConfigSource{
+								ConfigSourceSpecifier: &coreV3.ConfigSource_ApiConfigSource{
+									ApiConfigSource: &coreV3.ApiConfigSource{
+										ApiType: coreV3.ApiConfigSource_GRPC,
+										GrpcServices: []*coreV3.GrpcService{
+											{
+												TargetSpecifier: &coreV3.GrpcService_EnvoyGrpc_{
+													EnvoyGrpc: &coreV3.GrpcService_EnvoyGrpc{
+														ClusterName: "sds-cluster",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+
+			typedConfig, err := anypb.New(upstreamTLS)
+			if err != nil {
+				return nil, err
+			}
+
+			for _, cluster := range response.Clusters {
+				if cluster.Name == "mock-extension-injected-cluster" {
+					cluster.TransportSocket = &coreV3.TransportSocket{
+						Name: "envoy.transport_sockets.tls",
+						ConfigType: &coreV3.TransportSocket_TypedConfig{
+							TypedConfig: typedConfig,
+						},
+					}
+				}
+			}
+		} else if extensionResource.GetObjectKind().GroupVersionKind().Kind == "ExampleExtPolicy" && targetKind != "Gateway" {
+			// This simulates an extension server that returns an error. It allows verifying that fail-close is working.
+			return response, fmt.Errorf("invalid extension policy : %s", extensionResource.GetName())
+		}
+	}
+
 	for idx, secret := range req.Secrets {
 		response.Secrets[idx] = proto.Clone(secret).(*tlsV3.Secret)
 	}
@@ -282,3 +342,17 @@ func (t *testingExtensionServer) PostTranslateModify(_ context.Context, req *pb.
 
 	return response, nil
 }
+
+func getTargetRefKind(obj *unstructured.Unstructured) (string, error) {
+	targetRef, found, err := unstructured.NestedMap(obj.Object, "spec", "targetRef")
+	if err != nil || !found {
+		return "", errors.New("targetRef not found or error")
+	}
+
+	kind, ok := targetRef["kind"].(string)
+	if !ok {
+		return "", errors.New("kind is not a string or missing in targetRef")
+	}
+
+	return kind, nil
+}
diff --git a/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server-error.yaml b/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server-error.yaml
new file mode 100644
index 000000000..33f5d3794
--- /dev/null
+++ b/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server-error.yaml
@@ -0,0 +1,13 @@
+extensionServerPolicies:
+- Object:
+    apiVersion: security.example.io/v1alpha1
+    kind: ExampleExtPolicy
+    metadata:
+      name: ext-server-policy-invalid-test
+      namespace: test
+    spec:
+      data: attached to all clusters
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: Service
+        name: Service-1
diff --git a/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server.yaml b/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server.yaml
new file mode 100644
index 000000000..2ad12b9f3
--- /dev/null
+++ b/internal/xds/translator/testdata/in/extension-xds-ir/extensionpolicy-extension-server.yaml
@@ -0,0 +1,13 @@
+extensionServerPolicies:
+- Object:
+    apiVersion: security.example.io/v1alpha1
+    kind: ExampleExtPolicy
+    metadata:
+      name: ext-server-policy-test
+      namespace: test
+    spec:
+      data: attached to all clusters
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.clusters.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.clusters.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.clusters.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.endpoints.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.endpoints.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.endpoints.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.listeners.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.listeners.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.listeners.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.routes.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.secrets.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.secrets.yaml
new file mode 100644
index 000000000..b04034756
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server-error.secrets.yaml
@@ -0,0 +1,4 @@
+- genericSecret:
+    secret:
+      inlineString: super-secret-extension-secret
+  name: mock-extension-injected-secret
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.clusters.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.clusters.yaml
new file mode 100644
index 000000000..875b5acba
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.clusters.yaml
@@ -0,0 +1,23 @@
+- loadAssignment:
+    clusterName: mock-extension-injected-cluster
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: exampleservice.examplenamespace.svc.cluster.local
+              portValue: 5000
+  name: mock-extension-injected-cluster
+  transportSocket:
+    name: envoy.transport_sockets.tls
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+      commonTlsContext:
+        tlsCertificateSdsSecretConfigs:
+        - name: default
+          sdsConfig:
+            apiConfigSource:
+              apiType: GRPC
+              grpcServices:
+              - envoyGrpc:
+                  clusterName: sds-cluster
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.endpoints.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.endpoints.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.endpoints.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.listeners.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.listeners.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.listeners.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.routes.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.secrets.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.secrets.yaml
new file mode 100644
index 000000000..b04034756
--- /dev/null
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/extensionpolicy-extension-server.secrets.yaml
@@ -0,0 +1,4 @@
+- genericSecret:
+    secret:
+      inlineString: super-secret-extension-secret
+  name: mock-extension-injected-secret
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index 1ddd3d5ad..1c8c14e6f 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -288,6 +288,11 @@ func TestTranslateXdsWithExtensionErrorsWhenFailOpen(t *testing.T) {
 						Version: "v1alpha1",
 						Kind:    "Bar",
 					},
+					{
+						Group:   "security.example.io",
+						Version: "v1alpha1",
+						Kind:    "ExampleExtPolicy",
+					},
 				},
 				Hooks: &egv1a1.ExtensionHooks{
 					XDSTranslator: &egv1a1.XDSTranslatorHooks{
@@ -357,6 +362,9 @@ func TestTranslateXdsWithExtensionErrorsWhenFailClosed(t *testing.T) {
 		"multiple-listeners-same-port-error": {
 			errMsg: "rpc error: code = Unknown desc = simulate error when there is no default filter chain in the original resources",
 		},
+		"extensionpolicy-extension-server-error": {
+			errMsg: "rpc error: code = Unknown desc = invalid extension policy : ext-server-policy-invalid-test",
+		},
 	}
 
 	inputFiles, err := filepath.Glob(filepath.Join("testdata", "in", "extension-xds-ir", "*-error.yaml"))
@@ -398,6 +406,11 @@ func TestTranslateXdsWithExtensionErrorsWhenFailClosed(t *testing.T) {
 						Version: "v1alpha1",
 						Kind:    "Bar",
 					},
+					{
+						Group:   "security.example.io",
+						Version: "v1alpha1",
+						Kind:    "ExampleExtPolicy",
+					},
 				},
 				Hooks: &egv1a1.ExtensionHooks{
 					XDSTranslator: &egv1a1.XDSTranslatorHooks{
diff --git a/test/conformance/experimental_conformance_test.go b/test/conformance/experimental_conformance_test.go
index 0d87cef58..632fed77e 100644
--- a/test/conformance/experimental_conformance_test.go
+++ b/test/conformance/experimental_conformance_test.go
@@ -25,13 +25,14 @@ import (
 
 	internalconf "github.com/envoyproxy/gateway/internal/gatewayapi/conformance"
 	"github.com/envoyproxy/gateway/test/e2e"
+	ege2etest "github.com/envoyproxy/gateway/test/e2e/tests"
 )
 
 func TestExperimentalConformance(t *testing.T) {
 	flag.Parse()
 	log.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(true)))
 
-	internalSuite := internalconf.EnvoyGatewaySuite(false)
+	internalSuite := internalconf.EnvoyGatewaySuite(ege2etest.IsGatewayNamespaceMode())
 
 	opts := conformance.DefaultOptions(t)
 	opts.SkipTests = internalSuite.SkipTests
@@ -63,6 +64,7 @@ func TestExperimentalConformance(t *testing.T) {
 		t.Fatalf("error generating conformance profile report: %v", err)
 	}
 
+	// use to trigger the experimental conformance report
 	err = experimentalConformanceReport(t.Logf, *report, *flags.ReportOutput)
 	require.NoError(t, err)
 }
diff --git a/test/e2e/testdata/custom-container-security-contex-userid.yaml b/test/e2e/testdata/custom-container-security-contex-userid.yaml
new file mode 100644
index 000000000..3272a4eb7
--- /dev/null
+++ b/test/e2e/testdata/custom-container-security-contex-userid.yaml
@@ -0,0 +1,14 @@
+# This test is to verify that the basic http route works with EG having custom security context user id
+# The custom security context user id is set to 65534 in the test go code
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: custom-eg-security-context-userid
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
diff --git a/test/e2e/tests/httproute_with_custom_security_context_userid.go b/test/e2e/tests/httproute_with_custom_security_context_userid.go
new file mode 100644
index 000000000..2cd9c06ee
--- /dev/null
+++ b/test/e2e/tests/httproute_with_custom_security_context_userid.go
@@ -0,0 +1,105 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/ptr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, EnvoyGatewayCustomSecurityContextUseridTest)
+}
+
+var EnvoyGatewayCustomSecurityContextUseridTest = suite.ConformanceTest{
+	ShortName:   "EnvoyGatewayCustomSecurityContextUserid",
+	Description: "Envoy Gateway container with custom security context user id",
+	Manifests: []string{
+		"testdata/custom-container-security-contex-userid.yaml",
+	},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("route with custom security context user id", func(t *testing.T) {
+			// set envoy-gateway deployment security context user id to 65534 to test custom user has the necessary permissions
+			// to run the envoy-gateway container
+			setEGSecurityContextUserID(t, suite, 65534)
+
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "custom-eg-security-context-userid", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+			expectedResponse := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			// reset envoy-gateway deployment security context user id to the default value 65532
+			setEGSecurityContextUserID(t, suite, 65532)
+			// We have to manually delete the envoy proxy deployment to ensure that the test suite can clean up properly.
+			// This is because the rollout restart of the envoy-gateway deployment may cause Envoy Gateway fail to delete
+			// the envoy proxy deployments after the Gateway resources are deleted in ControllerNamspace mod, which can
+			// lead to failure of the upgrade test.
+			if suite.Cleanup {
+				proxies := appsv1.DeploymentList{}
+				err := suite.Client.List(
+					context.Background(),
+					&proxies,
+					client.InNamespace("envoy-gateway-system"),
+					client.MatchingLabels{"app.kubernetes.io/component": "proxy", "app.kubernetes.io/managed-by": "envoy-gateway"})
+				require.NoError(t, err, "failed to list envoy proxy deployments")
+				for _, proxy := range proxies.Items {
+					err = suite.Client.Delete(context.Background(), &proxy)
+					require.NoError(t, err, "failed to delete envoy proxy deployment %s", proxy.Name)
+				}
+			}
+		})
+	},
+}
+
+func setEGSecurityContextUserID(t *testing.T, suite *suite.ConformanceTestSuite, uid int64) {
+	// update envoy-gateway deployment with custom security context user id
+	egDeployment := &appsv1.Deployment{}
+	err := suite.Client.Get(
+		context.Background(),
+		types.NamespacedName{Name: "envoy-gateway", Namespace: "envoy-gateway-system"},
+		egDeployment)
+	require.NoError(t, err)
+	egDeployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsUser = ptr.To(uid)
+	egDeployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsGroup = ptr.To(uid)
+	err = suite.Client.Update(context.Background(), egDeployment)
+	require.NoError(t, err)
+	// test that envoy-gateway pod is running with custom security context user id
+	WaitForPods(t, suite.Client, "envoy-gateway-system", map[string]string{"control-plane": "envoy-gateway"}, corev1.PodRunning, PodReady)
+
+	// test that envoy-gateway deployment is updated with custom security context user id
+	egDeployment = &appsv1.Deployment{}
+	err = suite.Client.Get(
+		context.Background(),
+		types.NamespacedName{Name: "envoy-gateway", Namespace: "envoy-gateway-system"},
+		egDeployment)
+	require.NoError(t, err)
+	require.Equal(t, uid, *egDeployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsUser, "envoy-gateway deployment is not updated with custom security context user id")
+	require.Equal(t, uid, *egDeployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsGroup, "envoy-gateway deployment is not updated with custom security context group id")
+}