Repository: envoyproxy/gateway
Pull request #5607 (2025-03-25 12:38:35)

feat: support CredentialInjector as a Backend Filter 
This PR adds support for using the EG HTTPRouteFilter as a Backend Filter. Currently, only the credential injector is supported, which allows configuring different credentials for individual backends within an HTTPRoute.


Implement: https://github.com/envoyproxy/gateway/issues/5351
Release Notes: Yes


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?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 `53.75940%` with `123 lines` in your changes missing coverage. Please review.
> Project coverage is 65.16%. Comparing base [(`14b506c`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/14b506c51c7dfa21df0fc734cf6e03d378fa3ab5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`0fa71a7`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/0fa71a7c71b8de53b4246d3e0b37fac897b9d893?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?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/xds/translator/credentialInjector.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2FcredentialInjector.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvY3JlZGVudGlhbEluamVjdG9yLmdv) | 70.37% | [21 Missing and 11 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/provider/kubernetes/filters.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fprovider%2Fkubernetes%2Ffilters.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy9maWx0ZXJzLmdv) | 4.00% | [23 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/provider/kubernetes/predicates.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fprovider%2Fkubernetes%2Fpredicates.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy9wcmVkaWNhdGVzLmdv) | 0.00% | [15 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/provider/kubernetes/indexers.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fprovider%2Fkubernetes%2Findexers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy9pbmRleGVycy5nbw==) | 6.25% | [14 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/xds/translator/cluster.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Fcluster.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvY2x1c3Rlci5nbw==) | 73.68% | [7 Missing and 3 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/ir/xds.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fir%2Fxds.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvaXIveGRzLmdv) | 0.00% | [8 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/gatewayapi/filters.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Ffilters.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9maWx0ZXJzLmdv) | 71.42% | [7 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/gatewayapi/validate.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Fvalidate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS92YWxpZGF0ZS5nbw==) | 50.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/xds/translator/translator.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Ftranslator.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvdHJhbnNsYXRvci5nbw==) | 25.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/xds/translator/httpfilters.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Fhttpfilters.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvaHR0cGZpbHRlcnMuZ28=) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?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 (53.75%) 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    #5607      +/-   ##
==========================================
- Coverage   65.27%   65.16%   -0.11%     
==========================================
  Files         213      214       +1     
  Lines       34073    34323     +250     
==========================================
+ Hits        22241    22368     +127     
- Misses      10499    10602     +103     
- Partials     1333     1353      +20     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5607?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>
zhaohuabing: > LGTM, should we change the PR title?

Done

Patch:
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 04fda3a0e..da70aa56a 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -1535,6 +1535,9 @@ func applyHTTPFiltersContextToDestinationFilters(httpFiltersContext *HTTPFilters
 	if len(httpFiltersContext.RemoveResponseHeaders) > 0 {
 		destFilters.RemoveResponseHeaders = httpFiltersContext.RemoveResponseHeaders
 	}
+	if httpFiltersContext.CredentialInjection != nil {
+		destFilters.CredentialInjection = httpFiltersContext.CredentialInjection
+	}
 }
 
 func inspectAppProtocolByRouteKind(kind gwapiv1.Kind) ir.AppProtocol {
diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go
index 15e7cc29d..a6b84aef0 100644
--- a/internal/gatewayapi/validate.go
+++ b/internal/gatewayapi/validate.go
@@ -144,13 +144,21 @@ func (t *Translator) validateBackendRefFilters(backendRef BackendRefContext, par
 	switch routeKind {
 	case resource.KindHTTPRoute:
 		for _, filter := range filters.([]gwapiv1.HTTPRouteFilter) {
-			if filter.Type != gwapiv1.HTTPRouteFilterRequestHeaderModifier && filter.Type != gwapiv1.HTTPRouteFilterResponseHeaderModifier {
+			// Reuse the same validation logic as HTTPRoute to validate the ExtensionRef
+			if err := ValidateHTTPRouteFilter(&filter, t.ExtensionGroupKinds...); err != nil {
+				unsupportedFilters = true
+				continue
+			}
+			if filter.Type != gwapiv1.HTTPRouteFilterRequestHeaderModifier &&
+				filter.Type != gwapiv1.HTTPRouteFilterResponseHeaderModifier &&
+				filter.Type != gwapiv1.HTTPRouteFilterExtensionRef {
 				unsupportedFilters = true
 			}
 		}
 	case resource.KindGRPCRoute:
 		for _, filter := range filters.([]gwapiv1.GRPCRouteFilter) {
-			if filter.Type != gwapiv1.GRPCRouteFilterRequestHeaderModifier && filter.Type != gwapiv1.GRPCRouteFilterResponseHeaderModifier {
+			if filter.Type != gwapiv1.GRPCRouteFilterRequestHeaderModifier &&
+				filter.Type != gwapiv1.GRPCRouteFilterResponseHeaderModifier {
 				unsupportedFilters = true
 			}
 		}
@@ -159,6 +167,10 @@ func (t *Translator) validateBackendRefFilters(backendRef BackendRefContext, par
 	}
 
 	if unsupportedFilters {
+		message := "Specific filter is not supported within BackendRef, only RequestHeaderModifier, ResponseHeaderModifier and gateway.envoyproxy.io/HTTPRouteFilter are supported"
+		if routeKind == resource.KindGRPCRoute {
+			message = "Specific filter is not supported within BackendRef, only RequestHeaderModifier and ResponseHeaderModifier are supported"
+		}
 		routeStatus := GetRouteStatus(route)
 		status.SetRouteStatusCondition(routeStatus,
 			parentRef.routeParentStatusIdx,
@@ -166,7 +178,7 @@ func (t *Translator) validateBackendRefFilters(backendRef BackendRefContext, par
 			gwapiv1.RouteConditionResolvedRefs,
 			metav1.ConditionFalse,
 			"UnsupportedRefValue",
-			"Specific filter is not supported within BackendRef, only RequestHeaderModifier and ResponseHeaderModifier are supported",
+			message,
 		)
 		return errors.New("unsupported filter type in backend reference")
 	}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 33fb42603..7139d0dc5 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -3001,6 +3001,8 @@ type DestinationFilters struct {
 	AddResponseHeaders []AddHeader `json:"addResponseHeaders,omitempty" yaml:"addResponseHeaders,omitempty"`
 	// RemoveResponseHeaders defines a list of headers to be removed from response.
 	RemoveResponseHeaders []string `json:"removeResponseHeaders,omitempty" yaml:"removeResponseHeaders,omitempty"`
+	// CredentialInjection defines the credential injection configuration.
+	CredentialInjection *CredentialInjection `json:"credentialInjection,omitempty" yaml:"credentialInjection,omitempty"`
 }
 
 // ResourceMetadata is metadata from the provider resource that is translated to an envoy resource
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 8eaa7e90e..ef88a0c44 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -858,6 +858,11 @@ func (in *DestinationFilters) DeepCopyInto(out *DestinationFilters) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.CredentialInjection != nil {
+		in, out := &in.CredentialInjection, &out.CredentialInjection
+		*out = new(CredentialInjection)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestinationFilters.
diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go
index 4a3cfb113..7f90d8762 100644
--- a/internal/provider/kubernetes/indexers.go
+++ b/internal/provider/kubernetes/indexers.go
@@ -7,6 +7,7 @@ package kubernetes
 
 import (
 	"context"
+	"fmt"
 
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -144,22 +145,33 @@ func backendHTTPRouteIndexFunc(rawObj client.Object) []string {
 
 func httpRouteFilterHTTPRouteIndexFunc(rawObj client.Object) []string {
 	httproute := rawObj.(*gwapiv1.HTTPRoute)
-	var httpRouteFilterRefs []string
+	httpRouteFilterRefs := make(map[string]struct{})
 	for _, rule := range httproute.Spec.Rules {
 		for _, filter := range rule.Filters {
 			if filter.ExtensionRef != nil && string(filter.ExtensionRef.Kind) == resource.KindHTTPRouteFilter {
-				// If an explicit Backend namespace is not provided, use the HTTPRoute namespace to
-				// lookup the provided Gateway Name.
-				httpRouteFilterRefs = append(httpRouteFilterRefs,
-					types.NamespacedName{
-						Namespace: httproute.Namespace,
-						Name:      string(filter.ExtensionRef.Name),
-					}.String(),
-				)
+				httpRouteFilterRefs[types.NamespacedName{
+					Namespace: httproute.Namespace,
+					Name:      string(filter.ExtensionRef.Name),
+				}.String()] = struct{}{}
+			}
+			for _, backendRef := range rule.BackendRefs {
+				for _, filter := range backendRef.Filters {
+					if filter.ExtensionRef != nil && string(filter.ExtensionRef.Kind) == resource.KindHTTPRouteFilter {
+						httpRouteFilterRefs[types.NamespacedName{
+							Namespace: httproute.Namespace,
+							Name:      string(filter.ExtensionRef.Name),
+						}.String()] = struct{}{}
+						fmt.Println("xxxxxbackendRef: ", backendRef.Name, "filter: ", filter.ExtensionRef.Name)
+					}
+				}
 			}
 		}
 	}
-	return httpRouteFilterRefs
+	refs := make([]string, 0, len(httpRouteFilterRefs))
+	for ref := range httpRouteFilterRefs {
+		refs = append(refs, ref)
+	}
+	return refs
 }
 
 func secretEnvoyProxyIndexFunc(rawObj client.Object) []string {
diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go
index f9894858e..ddb033912 100644
--- a/internal/provider/kubernetes/routes.go
+++ b/internal/provider/kubernetes/routes.go
@@ -8,6 +8,7 @@ package kubernetes
 import (
 	"context"
 	"errors"
+	"fmt"
 
 	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -317,123 +318,21 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 						}
 					}
 				}
+				for i := range backendRef.Filters {
+					// Some of the validation logic in processHTTPRouteFilter is not needed for backendRef filters.
+					// However, we reuse the same function to avoid code duplication.
+					if err := r.processHTTPRouteFilter(ctx, &backendRef.Filters[i], &httpRoute, resourceMap, resourceTree); err != nil {
+						r.log.Error(err, "bypassing backendRef filter", "index", i)
+						continue
+					}
+				}
 			}
 
 			for i := range rule.Filters {
-				filter := rule.Filters[i]
-				var extGKs []schema.GroupKind
-				for _, gvk := range r.extGVKs {
-					extGKs = append(extGKs, gvk.GroupKind())
-				}
-				if err := gatewayapi.ValidateHTTPRouteFilter(&filter, extGKs...); err != nil {
+				if err := r.processHTTPRouteFilter(ctx, &rule.Filters[i], &httpRoute, resourceMap, resourceTree); err != nil {
 					r.log.Error(err, "bypassing filter rule", "index", i)
 					continue
 				}
-
-				// Load in the backendRefs from any requestMirrorFilters on the HTTPRoute
-				if filter.Type == gwapiv1.HTTPRouteFilterRequestMirror {
-					// Make sure the config actually exists
-					mirrorFilter := filter.RequestMirror
-					if mirrorFilter == nil {
-						r.log.Error(errors.New("invalid requestMirror filter"), "bypassing filter rule", "index", i)
-						continue
-					}
-
-					mirrorBackendObj := mirrorFilter.BackendRef
-					// Wrap the filter's BackendObjectReference into a BackendRef so we can use existing tooling to check it
-					weight := int32(1)
-					mirrorBackendRef := gwapiv1.BackendRef{
-						BackendObjectReference: mirrorBackendObj,
-						Weight:                 &weight,
-					}
-
-					if err := validateBackendRef(&mirrorBackendRef); err != nil {
-						r.log.Error(err, "invalid backendRef")
-						continue
-					}
-
-					backendNamespace := gatewayapi.NamespaceDerefOr(mirrorBackendRef.Namespace, httpRoute.Namespace)
-					resourceMap.allAssociatedBackendRefs.Insert(gwapiv1.BackendObjectReference{
-						Group:     mirrorBackendRef.BackendObjectReference.Group,
-						Kind:      mirrorBackendRef.BackendObjectReference.Kind,
-						Namespace: gatewayapi.NamespacePtr(backendNamespace),
-						Name:      mirrorBackendRef.Name,
-					})
-
-					if backendNamespace != httpRoute.Namespace {
-						from := ObjectKindNamespacedName{
-							kind:      resource.KindHTTPRoute,
-							namespace: httpRoute.Namespace,
-							name:      httpRoute.Name,
-						}
-						to := ObjectKindNamespacedName{
-							kind:      gatewayapi.KindDerefOr(mirrorBackendRef.Kind, resource.KindService),
-							namespace: backendNamespace,
-							name:      string(mirrorBackendRef.Name),
-						}
-						refGrant, err := r.findReferenceGrant(ctx, from, to)
-						switch {
-						case err != nil:
-							r.log.Error(err, "failed to find ReferenceGrant")
-						case refGrant == nil:
-							r.log.Info("no matching ReferenceGrants found", "from", from.kind,
-								"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
-						default:
-							refGrantNamespacedName := utils.NamespacedName(refGrant).String()
-							if !resourceMap.allAssociatedReferenceGrants.Has(refGrantNamespacedName) {
-								resourceMap.allAssociatedReferenceGrants.Insert(refGrantNamespacedName)
-								resourceTree.ReferenceGrants = append(resourceTree.ReferenceGrants, refGrant)
-								r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
-									"name", refGrant.Name)
-							}
-						}
-					}
-				} else if filter.Type == gwapiv1.HTTPRouteFilterExtensionRef {
-					// NOTE: filters must be in the same namespace as the HTTPRoute
-					// Check if it's a Kind managed by an extension and add to resourceTree
-					key := utils.NamespacedNameWithGroupKind{
-						NamespacedName: types.NamespacedName{
-							Namespace: httpRoute.Namespace,
-							Name:      string(filter.ExtensionRef.Name),
-						},
-						GroupKind: schema.GroupKind{
-							Group: string(filter.ExtensionRef.Group),
-							Kind:  string(filter.ExtensionRef.Kind),
-						},
-					}
-
-					switch string(filter.ExtensionRef.Kind) {
-					case egv1a1.KindHTTPRouteFilter:
-						if r.hrfCRDExists {
-							httpFilter, err := r.getHTTPRouteFilter(ctx, key.Name, key.Namespace)
-							if err != nil {
-								r.log.Error(err, "HTTPRouteFilters not found; bypassing rule", "index", i)
-								continue
-							}
-							if !resourceMap.allAssociatedHTTPRouteExtensionFilters.Has(key) {
-								r.processRouteFilterConfigMapRef(ctx, httpFilter, resourceMap, resourceTree)
-								r.processRouteFilterSecretRef(ctx, httpFilter, resourceMap, resourceTree)
-								resourceMap.allAssociatedHTTPRouteExtensionFilters.Insert(key)
-								resourceTree.HTTPRouteFilters = append(resourceTree.HTTPRouteFilters, httpFilter)
-							}
-						}
-					default:
-						extRefFilter, ok := resourceMap.extensionRefFilters[key]
-						if !ok {
-							r.log.Error(
-								errors.New("filter not found; bypassing rule"),
-								"Filter not found; bypassing rule",
-								"name", filter.ExtensionRef.Name,
-								"index", i)
-							continue
-						}
-
-						if !resourceMap.allAssociatedHTTPRouteExtensionFilters.Has(key) {
-							resourceMap.allAssociatedHTTPRouteExtensionFilters.Insert(key)
-							resourceTree.ExtensionRefFilters = append(resourceTree.ExtensionRefFilters, extRefFilter)
-						}
-					}
-				}
 			}
 		}
 
@@ -448,6 +347,120 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 	return nil
 }
 
+func (r *gatewayAPIReconciler) processHTTPRouteFilter(
+	ctx context.Context,
+	filter *gwapiv1.HTTPRouteFilter,
+	httpRoute *gwapiv1.HTTPRoute,
+	resourceMap *resourceMappings,
+	resourceTree *resource.Resources,
+) error {
+	var extGKs []schema.GroupKind
+	for _, gvk := range r.extGVKs {
+		extGKs = append(extGKs, gvk.GroupKind())
+	}
+	if err := gatewayapi.ValidateHTTPRouteFilter(filter, extGKs...); err != nil {
+		return err
+	}
+
+	// Load in the backendRefs from any requestMirrorFilters on the HTTPRoute
+	if filter.Type == gwapiv1.HTTPRouteFilterRequestMirror {
+		// Make sure the config actually exists
+		mirrorFilter := filter.RequestMirror
+		if mirrorFilter == nil {
+			return errors.New("invalid requestMirror filter")
+		}
+
+		mirrorBackendObj := mirrorFilter.BackendRef
+		// Wrap the filter's BackendObjectReference into a BackendRef so we can use existing tooling to check it
+		weight := int32(1)
+		mirrorBackendRef := gwapiv1.BackendRef{
+			BackendObjectReference: mirrorBackendObj,
+			Weight:                 &weight,
+		}
+
+		if err := validateBackendRef(&mirrorBackendRef); err != nil {
+			return fmt.Errorf("invalid backendRef for requestMirror filter: %w", err)
+		}
+
+		backendNamespace := gatewayapi.NamespaceDerefOr(mirrorBackendRef.Namespace, httpRoute.Namespace)
+		resourceMap.allAssociatedBackendRefs.Insert(gwapiv1.BackendObjectReference{
+			Group:     mirrorBackendRef.BackendObjectReference.Group,
+			Kind:      mirrorBackendRef.BackendObjectReference.Kind,
+			Namespace: gatewayapi.NamespacePtr(backendNamespace),
+			Name:      mirrorBackendRef.Name,
+		})
+
+		if backendNamespace != httpRoute.Namespace {
+			from := ObjectKindNamespacedName{
+				kind:      resource.KindHTTPRoute,
+				namespace: httpRoute.Namespace,
+				name:      httpRoute.Name,
+			}
+			to := ObjectKindNamespacedName{
+				kind:      gatewayapi.KindDerefOr(mirrorBackendRef.Kind, resource.KindService),
+				namespace: backendNamespace,
+				name:      string(mirrorBackendRef.Name),
+			}
+			refGrant, err := r.findReferenceGrant(ctx, from, to)
+			switch {
+			case err != nil:
+				r.log.Error(err, "failed to find ReferenceGrant")
+			case refGrant == nil:
+				r.log.Info("no matching ReferenceGrants found", "from", from.kind,
+					"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
+			default:
+				refGrantNamespacedName := utils.NamespacedName(refGrant).String()
+				if !resourceMap.allAssociatedReferenceGrants.Has(refGrantNamespacedName) {
+					resourceMap.allAssociatedReferenceGrants.Insert(refGrantNamespacedName)
+					resourceTree.ReferenceGrants = append(resourceTree.ReferenceGrants, refGrant)
+					r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
+						"name", refGrant.Name)
+				}
+			}
+		}
+	} else if filter.Type == gwapiv1.HTTPRouteFilterExtensionRef {
+		// NOTE: filters must be in the same namespace as the HTTPRoute
+		// Check if it's a Kind managed by an extension and add to resourceTree
+		key := utils.NamespacedNameWithGroupKind{
+			NamespacedName: types.NamespacedName{
+				Namespace: httpRoute.Namespace,
+				Name:      string(filter.ExtensionRef.Name),
+			},
+			GroupKind: schema.GroupKind{
+				Group: string(filter.ExtensionRef.Group),
+				Kind:  string(filter.ExtensionRef.Kind),
+			},
+		}
+
+		switch string(filter.ExtensionRef.Kind) {
+		case egv1a1.KindHTTPRouteFilter:
+			if r.hrfCRDExists {
+				httpFilter, err := r.getHTTPRouteFilter(ctx, key.Name, key.Namespace)
+				if err != nil {
+					return fmt.Errorf("filter not found: %w", err)
+				}
+				if !resourceMap.allAssociatedHTTPRouteExtensionFilters.Has(key) {
+					r.processRouteFilterConfigMapRef(ctx, httpFilter, resourceMap, resourceTree)
+					r.processRouteFilterSecretRef(ctx, httpFilter, resourceMap, resourceTree)
+					resourceMap.allAssociatedHTTPRouteExtensionFilters.Insert(key)
+					resourceTree.HTTPRouteFilters = append(resourceTree.HTTPRouteFilters, httpFilter)
+				}
+			}
+		default:
+			extRefFilter, ok := resourceMap.extensionRefFilters[key]
+			if !ok {
+				return fmt.Errorf("filter not found: %s", filter.ExtensionRef.Name)
+			}
+
+			if !resourceMap.allAssociatedHTTPRouteExtensionFilters.Has(key) {
+				resourceMap.allAssociatedHTTPRouteExtensionFilters.Insert(key)
+				resourceTree.ExtensionRefFilters = append(resourceTree.ExtensionRefFilters, extRefFilter)
+			}
+		}
+	}
+	return nil
+}
+
 // processTCPRoutes finds TCPRoutes corresponding to a gatewayNamespaceName, further checks for
 // the backend references and pushes the TCPRoutes to the resourceTree.
 func (r *gatewayAPIReconciler) processTCPRoutes(ctx context.Context, gatewayNamespaceName string,
diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go
index a051c247c..4957ec2fd 100644
--- a/internal/xds/translator/cluster.go
+++ b/internal/xds/translator/cluster.go
@@ -14,9 +14,12 @@ import (
 	clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
 	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
+	codecv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/upstream_codec/v3"
+	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
 	preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
 	proxyprotocolv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/proxy_protocol/v3"
 	rawbufferv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3"
+	tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	httpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
 	xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
 	"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
@@ -83,7 +86,12 @@ func buildEndpointType(settings []*ir.DestinationSetting) EndpointType {
 	return EndpointTypeStatic
 }
 
-func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
+type buildClusterResult struct {
+	cluster *clusterv3.Cluster
+	secrets []*tlsv3.Secret // Secrets used in the cluster filters, we may need to add other types of resources in the future.
+}
+
+func buildXdsCluster(args *xdsClusterArgs) (*buildClusterResult, error) {
 	dnsLookupFamily := clusterv3.Cluster_V4_PREFERRED
 	customDNSPolicy := args.dns != nil && args.dns.LookupFamily != nil
 	// apply DNS lookup family if custom DNS traffic policy is set
@@ -168,7 +176,7 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
 			socket, err := buildXdsUpstreamTLSSocketWthCert(ds.TLS)
 			if err != nil {
 				// TODO: Log something here
-				return nil
+				return nil, err
 			}
 			if args.proxyProtocol != nil {
 				socket = buildProxyProtocolSocket(args.proxyProtocol, socket)
@@ -217,7 +225,10 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
 	}
 
 	// build common, HTTP/1 and HTTP/2  protocol options for cluster
-	epo := buildTypedExtensionProtocolOptions(args)
+	epo, secrets, err := buildTypedExtensionProtocolOptions(args)
+	if err != nil {
+		return nil, err
+	}
 	if epo != nil {
 		cluster.TypedExtensionProtocolOptions = epo
 	}
@@ -277,7 +288,10 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
 	if args.tcpkeepalive != nil {
 		cluster.UpstreamConnectionOptions = buildXdsClusterUpstreamOptions(args.tcpkeepalive)
 	}
-	return cluster
+	return &buildClusterResult{
+		cluster: cluster,
+		secrets: secrets,
+	}, nil
 }
 
 func buildXdsHealthCheck(healthcheck *ir.ActiveHealthCheck) []*corev3.HealthCheck {
@@ -576,7 +590,7 @@ func buildWeightedLocalities(metadata *corev3.Metadata, ds *ir.DestinationSettin
 	return locality
 }
 
-func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) map[string]*anypb.Any {
+func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) (map[string]*anypb.Any, []*tlsv3.Secret, error) {
 	requiresHTTP2Options := false
 	for _, ds := range args.settings {
 		if ds.Protocol == ir.GRPC ||
@@ -590,10 +604,13 @@ func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) map[string]*anypb.
 		(args.timeout.HTTP.MaxConnectionDuration != nil || args.timeout.HTTP.ConnectionIdleTimeout != nil)) ||
 		(args.circuitBreaker != nil && args.circuitBreaker.MaxRequestsPerConnection != nil)
 
-	requiresHTTP1Options := args.http1Settings != nil && (args.http1Settings.EnableTrailers || args.http1Settings.PreserveHeaderCase || args.http1Settings.HTTP10 != nil)
+	requiresHTTP1Options := args.http1Settings != nil &&
+		(args.http1Settings.EnableTrailers || args.http1Settings.PreserveHeaderCase || args.http1Settings.HTTP10 != nil)
 
-	if !(requiresCommonHTTPOptions || requiresHTTP1Options || requiresHTTP2Options || args.useClientProtocol) {
-		return nil
+	requiresHTTPFilters := len(args.settings) > 0 && args.settings[0].Filters != nil && args.settings[0].Filters.CredentialInjection != nil
+
+	if !(requiresCommonHTTPOptions || requiresHTTP1Options || requiresHTTP2Options || args.useClientProtocol || requiresHTTPFilters) {
+		return nil, nil, nil
 	}
 
 	protocolOptions := httpv3.HttpProtocolOptions{}
@@ -675,13 +692,74 @@ func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) map[string]*anypb.
 		}
 	}
 
+	var (
+		filters []*hcmv3.HttpFilter
+		secrets []*tlsv3.Secret
+		err     error
+	)
+	if requiresHTTPFilters {
+		filters, secrets, err = buildClusterHTTPFilters(args)
+		if err != nil {
+			return nil, nil, err
+		}
+		if len(filters) > 0 {
+			protocolOptions.HttpFilters = filters
+		}
+	}
+
 	anyProtocolOptions, _ := proto.ToAnyWithValidation(&protocolOptions)
 
 	extensionOptions := map[string]*anypb.Any{
 		extensionOptionsKey: anyProtocolOptions,
 	}
 
-	return extensionOptions
+	return extensionOptions, secrets, nil
+}
+
+// buildClusterHTTPFilters builds the HTTP filters for the cluster.
+// EG only supports credential injector filter for now, more filters can be added in the future.
+func buildClusterHTTPFilters(args *xdsClusterArgs) ([]*hcmv3.HttpFilter, []*tlsv3.Secret, error) {
+	filters := make([]*hcmv3.HttpFilter, 0)
+	secrets := make([]*tlsv3.Secret, 0)
+	if len(args.settings) > 0 {
+		// There is only one setting in the settings slice because EG creates one cluster per backendRef
+		// if there are backend filters.
+		if args.settings[0].Filters != nil && args.settings[0].Filters.CredentialInjection != nil {
+			filter, err := buildHCMCredentialInjectorFilter(args.settings[0].Filters.CredentialInjection)
+			filter.Disabled = false
+			if err != nil {
+				return nil, nil, err
+			}
+			secret := buildCredentialSecret(args.settings[0].Filters.CredentialInjection)
+			filters = append(filters, filter)
+			secrets = append(secrets, secret)
+		}
+	}
+
+	// UpstreamCodec filter is required as the terminal filter for the upstream HTTP filters.
+	if len(filters) > 0 {
+		upstreamCodec, err := buildUpstreamCodecFilter()
+		if err != nil {
+			return nil, nil, err
+		}
+		filters = append(filters, upstreamCodec)
+	}
+	// We may need to add more Cluster filters in the future, so we return a slice of filters.
+	return filters, secrets, nil
+}
+
+func buildUpstreamCodecFilter() (*hcmv3.HttpFilter, error) {
+	codec := &codecv3.UpstreamCodec{}
+	codecAny, err := proto.ToAnyWithValidation(codec)
+	if err != nil {
+		return nil, err
+	}
+	return &hcmv3.HttpFilter{
+		Name: "envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec",
+		ConfigType: &hcmv3.HttpFilter_TypedConfig{
+			TypedConfig: codecAny,
+		},
+	}, nil
 }
 
 // buildProxyProtocolSocket builds the ProxyProtocol transport socket.
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 5672cc6e7..c872cacb0 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -1001,7 +1001,11 @@ func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error
 		return nil
 	}
 
-	xdsCluster := buildXdsCluster(args)
+	result, err := buildXdsCluster(args)
+	if err != nil {
+		return err
+	}
+	xdsCluster := result.cluster
 	xdsEndpoints := buildXdsClusterLoadAssignment(args.name, args.settings)
 	for _, ds := range args.settings {
 		if ds.TLS != nil {
@@ -1023,6 +1027,13 @@ func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error
 	if err := tCtx.AddXdsResource(resourcev3.ClusterType, xdsCluster); err != nil {
 		return err
 	}
+
+	// Add the secrets used in the cluster filters. (Currently only used for Credential Injector filter)
+	for _, secret := range result.secrets {
+		if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 7f0069774..401dac044 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -25,7 +25,7 @@ new features: |
   Added support for BackendTLSPolicy to target ServiceImport
   Added support for kubernetes.io/h2c application protocol in ServiceImport
   Added support for per-host circuit breaker thresholds
-  Added support for injecting a credential from a Kubernetes Secret into a request header
+  Added support for injecting a credential from a Kubernetes Secret into a request header. Credentials can be injected using either an HTTPRouteFilter or a BackendRef filter.
   Added support for egctl Websocket in addation to SPDY
   Added a configuration option in the Helm chart to set the TrafficDistribution field in the Envoy Gateway Service
   Added support for setting the log level to trace for the Envoy Proxy


Test patch:
diff --git a/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.in.yaml b/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.in.yaml
index 3e5cc4db8..f34d17a56 100644
--- a/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.in.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.in.yaml
@@ -46,6 +46,9 @@ httpRoutes:
             - path:
                 value: "/foo"
           backendRefs:
+            - name: service-2
+              port: 8080
+              weight: 2
             - name: service-3
               port: 8080
               weight: 1
@@ -55,3 +58,27 @@ httpRoutes:
                     set:
                       - name: "add-header-3"
                         value: "some-value"
+                - type: ExtensionRef
+                  extensionRef:
+                    group: gateway.envoyproxy.io
+                    kind: HTTPRouteFilter
+                    name: credential-injection
+httpFilters:
+  - apiVersion: gateway.envoyproxy.io/v1alpha1
+    kind: HTTPRouteFilter
+    metadata:
+      name: credential-injection
+      namespace: default
+    spec:
+      credentialInjection:
+        credential:
+          valueRef:
+            name: credential-1
+secrets:
+  - apiVersion: v1
+    kind: Secret
+    metadata:
+      name: credential-1
+      namespace: default
+    data:
+      credential: c2VjcmV0
diff --git a/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.out.yaml b/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.out.yaml
index 7a0cbd7fb..a6079fa70 100644
--- a/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-backendref-add-multiple-filters.out.yaml
@@ -70,12 +70,20 @@ httpRoutes:
       - path:
           value: /
     - backendRefs:
+      - name: service-2
+        port: 8080
+        weight: 2
       - filters:
         - requestHeaderModifier:
             set:
             - name: add-header-3
               value: some-value
           type: RequestHeaderModifier
+        - extensionRef:
+            group: gateway.envoyproxy.io
+            kind: HTTPRouteFilter
+            name: credential-injection
+          type: ExtensionRef
         name: service-3
         port: 8080
         weight: 1
@@ -139,6 +147,13 @@ xdsIR:
       - destination:
           name: httproute/default/httproute-1/rule/1
           settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/1/backend/0
+            protocol: HTTP
+            weight: 2
           - addressType: IP
             endpoints:
             - host: 7.7.7.7
@@ -149,7 +164,10 @@ xdsIR:
                 name: add-header-3
                 value:
                 - some-value
-            name: httproute/default/httproute-1/rule/1/backend/0
+              credentialInjection:
+                credential: '[redacted]'
+                name: httproutefilter/default/credential-injection
+            name: httproute/default/httproute-1/rule/1/backend/1
             protocol: HTTP
             weight: 1
         hostname: '*'
diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-unsupported-filter.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-unsupported-filter.out.yaml
index 4f031a058..e7ac021f1 100644
--- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-unsupported-filter.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-unsupported-filter.out.yaml
@@ -73,8 +73,8 @@ httpRoutes:
         status: "True"
         type: Accepted
       - lastTransitionTime: null
-        message: Specific filter is not supported within BackendRef, only RequestHeaderModifier
-          and ResponseHeaderModifier are supported
+        message: Specific filter is not supported within BackendRef, only RequestHeaderModifier,
+          ResponseHeaderModifier and gateway.envoyproxy.io/HTTPRouteFilter are supported
         reason: UnsupportedRefValue
         status: "False"
         type: ResolvedRefs
diff --git a/internal/xds/translator/cluster_test.go b/internal/xds/translator/cluster_test.go
index 131a37fef..a05363a7a 100644
--- a/internal/xds/translator/cluster_test.go
+++ b/internal/xds/translator/cluster_test.go
@@ -37,8 +37,9 @@ func TestBuildXdsCluster(t *testing.T) {
 			PanicThreshold: ptr.To[uint32](66),
 		},
 	}
-	dynamicXdsCluster := buildXdsCluster(args)
-
+	result, err := buildXdsCluster(args)
+	require.NoError(t, err)
+	dynamicXdsCluster := result.cluster
 	require.Equal(t, bootstrapXdsCluster.Name, dynamicXdsCluster.Name)
 	require.Equal(t, bootstrapXdsCluster.ClusterDiscoveryType, dynamicXdsCluster.ClusterDiscoveryType)
 	require.Equal(t, bootstrapXdsCluster.TransportSocket, dynamicXdsCluster.TransportSocket)
diff --git a/internal/xds/translator/testdata/in/xds-ir/credential-injection-backend-filter.yaml b/internal/xds/translator/testdata/in/xds-ir/credential-injection-backend-filter.yaml
new file mode 100644
index 000000000..1d8a4e797
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/credential-injection-backend-filter.yaml
@@ -0,0 +1,53 @@
+http:
+- address: 0.0.0.0
+  hostnames:
+  - '*'
+  isHTTP2: false
+  metadata:
+    kind: Gateway
+    name: gateway-1
+    namespace: envoy-gateway
+    sectionName: http
+  name: envoy-gateway/gateway-1/http
+  path:
+    escapedSlashesAction: UnescapeAndRedirect
+    mergeSlashes: true
+  port: 10080
+  routes:
+  - destination:
+      name: httproute/default/httproute-1/rule/1
+      settings:
+      - addressType: IP
+        endpoints:
+        - host: 7.7.7.7
+          port: 8080
+        name: httproute/default/httproute-1/rule/1/backend/0
+        protocol: HTTP
+        weight: 2
+      - addressType: IP
+        endpoints:
+        - host: 7.7.7.7
+          port: 8080
+        filters:
+          addRequestHeaders:
+          - append: false
+            name: add-header-3
+            value:
+            - some-value
+          credentialInjection:
+            name: httproutefilter/default/credential-injection-1
+            credential: '[redacted]'
+        name: httproute/default/httproute-1/rule/1/backend/1
+        protocol: HTTP
+        weight: 1
+    hostname: '*'
+    isHTTP2: false
+    metadata:
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+    name: httproute/default/httproute-1/rule/1/match/0/*
+    pathMatch:
+      distinct: false
+      name: ""
+      prefix: /foo
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.clusters.yaml
new file mode 100644
index 000000000..369e102c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.clusters.yaml
@@ -0,0 +1,55 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/1/backend/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/1/backend/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/1/backend/1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/1/backend/1
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+  typedExtensionProtocolOptions:
+    envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+      '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+      explicitHttpConfig:
+        httpProtocolOptions: {}
+      httpFilters:
+      - name: envoy.filters.http.credential_injector/httproutefilter/default/credential-injection-1
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.credential_injector.v3.CredentialInjector
+          credential:
+            name: envoy.http.injected_credentials.generic
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic
+              credential:
+                name: credential_injector/credential/httproutefilter/default/credential-injection-1
+                sdsConfig:
+                  ads: {}
+                  resourceApiVersion: V3
+      - name: envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.endpoints.yaml
new file mode 100644
index 000000000..ab5b19517
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.endpoints.yaml
@@ -0,0 +1,24 @@
+- clusterName: httproute/default/httproute-1/rule/1/backend/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 2
+    locality:
+      region: httproute/default/httproute-1/rule/1/backend/0
+- clusterName: httproute/default/httproute-1/rule/1/backend/1
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/1/backend/1
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.listeners.yaml
new file mode 100644
index 000000000..09426a317
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.listeners.yaml
@@ -0,0 +1,34 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: envoy-gateway/gateway-1/http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: envoy-gateway/gateway-1/http
+  name: envoy-gateway/gateway-1/http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.routes.yaml
new file mode 100644
index 000000000..caf51e2fc
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.routes.yaml
@@ -0,0 +1,40 @@
+- ignorePortInHostMatching: true
+  name: envoy-gateway/gateway-1/http
+  virtualHosts:
+  - domains:
+    - '*'
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: gateway-1
+            namespace: envoy-gateway
+            sectionName: http
+    name: envoy-gateway/gateway-1/http/*
+    routes:
+    - match:
+        pathSeparatedPrefix: /foo
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/1/match/0/*
+      route:
+        clusterNotFoundResponseCode: INTERNAL_SERVER_ERROR
+        upgradeConfigs:
+        - upgradeType: websocket
+        weightedClusters:
+          clusters:
+          - name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 2
+          - name: httproute/default/httproute-1/rule/1/backend/1
+            requestHeadersToAdd:
+            - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+              header:
+                key: add-header-3
+                value: some-value
+            weight: 1
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.secrets.yaml
new file mode 100644
index 000000000..7c76b21a4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection-backend-filter.secrets.yaml
@@ -0,0 +1,4 @@
+- genericSecret:
+    secret:
+      inlineBytes: W3JlZGFjdGVkXQ==
+  name: credential_injector/credential/httproutefilter/default/credential-injection-1
diff --git a/test/e2e/testdata/credential-injection-backend-filter.yaml b/test/e2e/testdata/credential-injection-backend-filter.yaml
new file mode 100644
index 000000000..1d9bb0f38
--- /dev/null
+++ b/test/e2e/testdata/credential-injection-backend-filter.yaml
@@ -0,0 +1,80 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: credential-injection
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  rules:
+  - matches:
+    - path:
+        value: "/foo"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+      filters:
+      - type: ExtensionRef
+        extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-1
+  - matches:
+    - path:
+        value: "/bar"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+      filters:
+      - type: ExtensionRef
+        extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-2
+  - matches:
+    - path:
+        value: "/baz"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: HTTPRouteFilter
+metadata:
+  name: credential-injection-1
+  namespace: gateway-conformance-infra
+spec:
+  credentialInjection:
+    credential:
+      valueRef:
+        name: credential-1
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: HTTPRouteFilter
+metadata:
+  name: credential-injection-2
+  namespace: gateway-conformance-infra
+spec:
+  credentialInjection:
+    header: x-credential
+    overwrite: true
+    credential:
+      valueRef:
+        name: credential-2
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: credential-1
+  namespace: gateway-conformance-infra
+data:
+  credential: QmFzaWMgZFhObGNqRTZkR1Z6ZERFPQ==
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: credential-2
+  namespace: gateway-conformance-infra
+data:
+  credential: QmFzaWMgZFhObGNqRTZkR1Z6ZERJPQ==
diff --git a/test/e2e/tests/credential_injection-backend-filter.go b/test/e2e/tests/credential_injection-backend-filter.go
new file mode 100644
index 000000000..e32f05405
--- /dev/null
+++ b/test/e2e/tests/credential_injection-backend-filter.go
@@ -0,0 +1,104 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	"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, CredentialInjectionBackendFilterTest)
+}
+
+var CredentialInjectionBackendFilterTest = suite.ConformanceTest{
+	ShortName:   "CredentialInjectionBackendFilter",
+	Description: "Backend filter with CredentialInjection",
+	Manifests:   []string{"testdata/credential-injection-backend-filter.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("inject credential to the default Authorization header", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "credential-injection", 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: "/foo",
+				},
+				ExpectedRequest: &http.ExpectedRequest{
+					Request: http.Request{
+						Headers: map[string]string{
+							"Authorization": "Basic dXNlcjE6dGVzdDE=",
+						},
+						Path: "/foo",
+					},
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+
+		t.Run("inject credential to a custom header", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "credential-injection", 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: "/bar",
+				},
+				ExpectedRequest: &http.ExpectedRequest{
+					Request: http.Request{
+						Headers: map[string]string{
+							"x-credential": "Basic dXNlcjE6dGVzdDI=",
+						},
+						Path: "/bar",
+					},
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+
+		t.Run("no credential injection", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "credential-injection", 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: "/baz",
+				},
+				ExpectedRequest: &http.ExpectedRequest{
+					Request: http.Request{
+						Path: "/baz",
+					},
+					AbsentHeaders: []string{"Authorization", "x-credential"},
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+	},
+}
diff --git a/test/e2e/tests/credential_injection.go b/test/e2e/tests/credential_injection.go
index 39137c51b..6ee6cedeb 100644
--- a/test/e2e/tests/credential_injection.go
+++ b/test/e2e/tests/credential_injection.go
@@ -22,7 +22,7 @@ func init() {
 
 var CredentialInjectionTest = suite.ConformanceTest{
 	ShortName:   "CredentialInjection",
-	Description: "Resource with CredentialInjection enabled",
+	Description: "HTTPRoute Filter with CredentialInjection",
 	Manifests:   []string{"testdata/credential-injection.yaml"},
 	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
 		t.Run("inject credential to the default Authorization header", func(t *testing.T) {