Repository: envoyproxy/gateway
Pull request #5496 (2025-03-14 00:28:17)

feat: impl credential injection
This PR implements the credential injection HTTPRoute filter, which allows adding credentials from a k8s secret into the Authorization header. It can be useful for egress scenarios, where authentication is required when talking to a backend service.

Implements #4757

Release Notes: Yes


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5496?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 `57.06215%` with `76 lines` in your changes missing coverage. Please review.
> Project coverage is 48.76%. Comparing base [(`b924e6a`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/b924e6a44184536016e2616284378fc843745085?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`06974c0`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/06974c04c61a39bd07a62841801ced17ae35432c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 18 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5496?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/5496?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) | 71.00% | [19 Missing and 10 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5496?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/5496?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/5496?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/5496?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/5496?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/5496?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/5496?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/5496?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) | 11.11% | [7 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

:x: Your project check has failed because the head coverage (48.76%) is below the target coverage (60.00%). You can increase the head 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    #5496       +/-   ##
===========================================
- Coverage   65.27%   48.76%   -16.52%     
===========================================
  Files         213      214        +1     
  Lines       33915    34164      +249     
===========================================
- Hits        22139    16660     -5479     
- Misses      10443    16223     +5780     
+ Partials     1333     1281       -52     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5496?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>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
zhaohuabing: This PR is ready for review, and we can land it after [the Envoy credential injector issue ](https://github.com/envoyproxy/gateway/pull/5496#discussion_r1998304248)is solved.

Patch:
diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index 5860bba4e..61fe5514c 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -250,6 +250,9 @@ const (
 	// EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
 	EnvoyFilterCustomResponse EnvoyFilter = "envoy.filters.http.custom_response"
 
+	// EnvoyFilterCredentialInjector defines the Envoy HTTP credential injector filter.
+	EnvoyFilterCredentialInjector EnvoyFilter = "envoy.filters.http.credential_injector"
+
 	// EnvoyFilterCompressor defines the Envoy HTTP compressor filter.
 	EnvoyFilterCompressor EnvoyFilter = "envoy.filters.http.compressor"
 
diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go
index f156e7b94..62225c0eb 100644
--- a/api/v1alpha1/httproutefilter_types.go
+++ b/api/v1alpha1/httproutefilter_types.go
@@ -13,6 +13,9 @@ import (
 const (
 	// KindHTTPRouteFilter is the name of the HTTPRouteFilter kind.
 	KindHTTPRouteFilter = "HTTPRouteFilter"
+
+	// InjectedCredentialKey is the key in the secret where the injected credential is stored.
+	InjectedCredentialKey = "credential"
 )
 
 // +kubebuilder:object:root=true
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index 85fdfb4e3..73bf889ed 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -46,8 +46,9 @@ type HTTPFiltersContext struct {
 
 // HTTPFilterIR contains the ir processing results.
 type HTTPFilterIR struct {
-	DirectResponse   *ir.CustomResponse
-	RedirectResponse *ir.Redirect
+	DirectResponse      *ir.CustomResponse
+	RedirectResponse    *ir.Redirect
+	CredentialInjection *ir.CredentialInjection
 
 	URLRewrite *ir.URLRewrite
 
@@ -797,6 +798,39 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec
 
 					filterContext.HTTPFilterIR.DirectResponse = dr
 				}
+
+				if hrf.Spec.CredentialInjection != nil {
+					secret, err := t.validateSecretRef(
+						false,
+						crossNamespaceFrom{
+							group:     egv1a1.GroupName,
+							kind:      resource.KindHTTPRouteFilter,
+							namespace: filterNs,
+						},
+						hrf.Spec.CredentialInjection.Credential.ValueRef, resources)
+					if err != nil {
+						t.processInvalidHTTPFilter(string(extFilter.Kind), filterContext, err)
+						return
+					}
+
+					secretBytes, ok := secret.Data[egv1a1.InjectedCredentialKey]
+					if !ok || len(secretBytes) == 0 {
+						err := fmt.Errorf(
+							"credential key %s not found in secret %s/%s",
+							egv1a1.InjectedCredentialKey, secret.Namespace,
+							secret.Name)
+						t.processInvalidHTTPFilter(string(extFilter.Kind), filterContext, err)
+						return
+					}
+
+					injection := &ir.CredentialInjection{
+						Name:       irConfigName(hrf),
+						Header:     hrf.Spec.CredentialInjection.Header,
+						Overwrite:  hrf.Spec.CredentialInjection.Overwrite,
+						Credential: secretBytes,
+					}
+					filterContext.HTTPFilterIR.CredentialInjection = injection
+				}
 			}
 		}
 		if !found {
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index efcda1597..e78467396 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -489,6 +489,9 @@ func applyHTTPFiltersContextToIRRoute(httpFiltersContext *HTTPFiltersContext, ir
 	if httpFiltersContext.URLRewrite != nil {
 		irRoute.URLRewrite = httpFiltersContext.URLRewrite
 	}
+	if httpFiltersContext.CredentialInjection != nil {
+		irRoute.CredentialInjection = httpFiltersContext.CredentialInjection
+	}
 	if len(httpFiltersContext.AddRequestHeaders) > 0 {
 		irRoute.AddRequestHeaders = httpFiltersContext.AddRequestHeaders
 	}
@@ -793,6 +796,7 @@ func (t *Translator) processHTTPRouteParentRefListener(route RouteContext, route
 					Destination:           routeRoute.Destination,
 					Redirect:              routeRoute.Redirect,
 					DirectResponse:        routeRoute.DirectResponse,
+					CredentialInjection:   routeRoute.CredentialInjection,
 					URLRewrite:            routeRoute.URLRewrite,
 					Mirrors:               routeRoute.Mirrors,
 					ExtensionRefs:         routeRoute.ExtensionRefs,
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index d40c4b5a4..69bd7ed68 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -79,6 +79,7 @@ var (
 	ErrBothXForwardedForAndCustomHeaderInvalid  = errors.New("only one of ClientIPDetection.XForwardedFor and ClientIPDetection.CustomHeader must be set")
 	ErrBothNumTrustedHopsAndTrustedCIDRsInvalid = errors.New("only one of ClientIPDetection.XForwardedFor.NumTrustedHops and ClientIPDetection.XForwardedFor.TrustedCIDRs must be set")
 	ErrPanicThresholdInvalid                    = errors.New("PanicThreshold value is outside of 0-100 range")
+	ErrCredentialInjectionCredentialEmpty       = errors.New("field CredentialInjection.Credential must be specified")
 
 	redacted = []byte("[redacted]")
 )
@@ -619,6 +620,32 @@ func (r *CustomResponse) Validate() error {
 	return errs
 }
 
+// CredentialInjection defines the configuration for injecting credentials into the request.
+// +k8s:deepcopy-gen=true
+type CredentialInjection struct {
+	// Name is a unique name for a CredentialInjection configuration.
+	// The xds translator only generates one CredentialInjecor filter for each unique name.
+	Name string `json:"name" yaml:"name"`
+
+	// Header is the name of the header where the credentials are injected.
+	// If not specified, the credentials are injected into the Authorization header.
+	Header *string `json:"header,omitempty"`
+
+	// Whether to overwrite the value or not if the injected headers already exist.
+	// If not specified, the default value is false.
+	Overwrite *bool `json:"overwrite,omitempty"`
+
+	// Credential is the credential to be injected.
+	Credential PrivateBytes `json:"credential"`
+}
+
+func (c *CredentialInjection) Validate() error {
+	if len(c.Credential) == 0 {
+		return ErrCredentialInjectionCredentialEmpty
+	}
+	return nil
+}
+
 // HealthCheckSettings provides HealthCheck configuration on the HTTP/HTTPS listener.
 // +k8s:deepcopy-gen=true
 type HealthCheckSettings egv1a1.HealthCheckSettings
@@ -718,6 +745,8 @@ type HTTPRoute struct {
 	Destination *RouteDestination `json:"destination,omitempty" yaml:"destination,omitempty"`
 	// Rewrite to be changed for this route.
 	URLRewrite *URLRewrite `json:"urlRewrite,omitempty" yaml:"urlRewrite,omitempty"`
+	// Credentials to be injected into the request.
+	CredentialInjection *CredentialInjection `json:"credentialInjection,omitempty" yaml:"credentialInjection,omitempty"`
 	// ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters
 	ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"`
 	// Traffic holds the features associated with BackendTrafficPolicy
@@ -1349,6 +1378,11 @@ func (h *HTTPRoute) Validate() error {
 			errs = errors.Join(errs, err)
 		}
 	}
+	if h.CredentialInjection != nil {
+		if err := h.CredentialInjection.Validate(); err != nil {
+			errs = errors.Join(errs, err)
+		}
+	}
 	if h.URLRewrite != nil {
 		if err := h.URLRewrite.Validate(); err != nil {
 			errs = errors.Join(errs, err)
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 93b757aa7..1073c5193 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -694,6 +694,36 @@ func (in *CoreListenerDetails) DeepCopy() *CoreListenerDetails {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialInjection) DeepCopyInto(out *CredentialInjection) {
+	*out = *in
+	if in.Header != nil {
+		in, out := &in.Header, &out.Header
+		*out = new(string)
+		**out = **in
+	}
+	if in.Overwrite != nil {
+		in, out := &in.Overwrite, &out.Overwrite
+		*out = new(bool)
+		**out = **in
+	}
+	if in.Credential != nil {
+		in, out := &in.Credential, &out.Credential
+		*out = make(PrivateBytes, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialInjection.
+func (in *CredentialInjection) DeepCopy() *CredentialInjection {
+	if in == nil {
+		return nil
+	}
+	out := new(CredentialInjection)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CustomResponse) DeepCopyInto(out *CustomResponse) {
 	*out = *in
@@ -1662,6 +1692,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
 		*out = new(URLRewrite)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CredentialInjection != nil {
+		in, out := &in.CredentialInjection, &out.CredentialInjection
+		*out = new(CredentialInjection)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.ExtensionRefs != nil {
 		in, out := &in.ExtensionRefs, &out.ExtensionRefs
 		*out = make([]*UnstructuredRef, len(*in))
diff --git a/internal/provider/kubernetes/filters.go b/internal/provider/kubernetes/filters.go
index 065ddb97d..1eaceb542 100644
--- a/internal/provider/kubernetes/filters.go
+++ b/internal/provider/kubernetes/filters.go
@@ -82,13 +82,45 @@ func (r *gatewayAPIReconciler) processRouteFilterConfigMapRef(
 			r.log.Error(err,
 				"failed to process DirectResponse ValueRef for HTTPRouteFilter",
 				"filter", filter, "ValueRef", filter.Spec.DirectResponse.Body.ValueRef.Name)
+			return
 		}
-
 		resourceMap.allAssociatedNamespaces.Insert(filter.Namespace)
 		if !resourceMap.allAssociatedConfigMaps.Has(utils.NamespacedName(configMap).String()) {
 			resourceMap.allAssociatedConfigMaps.Insert(utils.NamespacedName(configMap).String())
 			resourceTree.ConfigMaps = append(resourceTree.ConfigMaps, configMap)
 			r.log.Info("processing ConfigMap", "namespace", filter.Namespace, "name", string(filter.Spec.DirectResponse.Body.ValueRef.Name))
+
+		}
+	}
+}
+
+// processRouteFilterSecretRef adds the referenced Secret in a HTTPRouteFilter
+// to the resourceTree
+func (r *gatewayAPIReconciler) processRouteFilterSecretRef(
+	ctx context.Context, filter *egv1a1.HTTPRouteFilter,
+	resourceMap *resourceMappings, resourceTree *resource.Resources,
+) {
+	if filter.Spec.CredentialInjection != nil {
+		name := string(filter.Spec.CredentialInjection.Credential.ValueRef.Name)
+		secret := new(corev1.Secret)
+		err := r.client.Get(ctx, types.NamespacedName{Namespace: filter.Namespace, Name: name}, secret)
+		// we don't return an error here, because we want to continue
+		// reconciling the rest of the HTTPRouteFilter despite that this
+		// reference is invalid.
+		// This HTTPRouteFilter will be marked as invalid in its status
+		// when translating to IR because the referenced secret can't be
+		// found.
+		if err != nil {
+			r.log.Error(err,
+				"failed to process CredentialInjection ValueRef for HTTPRouteFilter",
+				"filter", filter, "ValueRef", filter.Spec.CredentialInjection.Credential.ValueRef.Name)
+			return
+		}
+		resourceMap.allAssociatedNamespaces.Insert(filter.Namespace)
+		if !resourceMap.allAssociatedSecrets.Has(utils.NamespacedName(secret).String()) {
+			resourceMap.allAssociatedSecrets.Insert(utils.NamespacedName(secret).String())
+			resourceTree.Secrets = append(resourceTree.Secrets, secret)
+			r.log.Info("processing Secret", "namespace", filter.Namespace, "name", name)
 		}
 	}
 }
diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go
index 9866f6d73..ce74e34c0 100644
--- a/internal/provider/kubernetes/indexers.go
+++ b/internal/provider/kubernetes/indexers.go
@@ -49,6 +49,7 @@ const (
 	httpRouteFilterHTTPRouteIndex    = "httpRouteFilterHTTPRouteIndex"
 	configMapBtpIndex                = "configMapBtpIndex"
 	configMapHTTPRouteFilterIndex    = "configMapHTTPRouteFilterIndex"
+	secretHTTPRouteFilterIndex       = "secretHTTPRouteFilterIndex"
 )
 
 func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error {
@@ -705,6 +706,12 @@ func addRouteFilterIndexers(ctx context.Context, mgr manager.Manager) error {
 		configMapHTTPRouteFilterIndex, configMapRouteFilterIndexFunc); err != nil {
 		return err
 	}
+
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &egv1a1.HTTPRouteFilter{},
+		secretHTTPRouteFilterIndex, secretRouteFilterIndexFunc); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -726,6 +733,21 @@ func configMapRouteFilterIndexFunc(rawObj client.Object) []string {
 	return configMapReferences
 }
 
+func secretRouteFilterIndexFunc(rawObj client.Object) []string {
+	filter := rawObj.(*egv1a1.HTTPRouteFilter)
+	var secretReferences []string
+	if filter.Spec.CredentialInjection != nil {
+		secretReferences = append(secretReferences,
+			types.NamespacedName{
+				Namespace: filter.Namespace,
+				Name:      string(filter.Spec.CredentialInjection.Credential.ValueRef.Name),
+			}.String(),
+		)
+	}
+
+	return secretReferences
+}
+
 // addBtlsIndexers adds indexing on BackendTLSPolicy, for ConfigMap and Secret objects that are
 // referenced in BackendTLSPolicy objects. This helps in querying for BackendTLSPolicies that are
 // affected by a particular ConfigMap CRUD.
diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go
index 2e019182b..e5bbafb2b 100644
--- a/internal/provider/kubernetes/predicates.go
+++ b/internal/provider/kubernetes/predicates.go
@@ -179,9 +179,31 @@ func (r *gatewayAPIReconciler) validateSecretForReconcile(obj client.Object) boo
 		}
 	}
 
+	if r.hrfCRDExists {
+		if r.isHTTPRouteFilterReferencingSecret(&nsName) {
+			return true
+		}
+	}
+
 	return false
 }
 
+func (r *gatewayAPIReconciler) isHTTPRouteFilterReferencingSecret(nsName *types.NamespacedName) bool {
+	routeFilterList := &egv1a1.HTTPRouteFilterList{}
+	if err := r.client.List(context.Background(), routeFilterList, &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(secretHTTPRouteFilterIndex, nsName.String()),
+	}); err != nil {
+		r.log.Error(err, "unable to find associated HTTPRouteFilter")
+		return false
+	}
+
+	if len(routeFilterList.Items) > 0 {
+		return true
+	}
+
+	return true
+}
+
 func (r *gatewayAPIReconciler) isBackendTLSPolicyReferencingSecret(nsName *types.NamespacedName) bool {
 	btlsList := &gwapiv1a3.BackendTLSPolicyList{}
 	if err := r.client.List(context.Background(), btlsList, &client.ListOptions{
diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go
index 1dd2afe5e..f9894858e 100644
--- a/internal/provider/kubernetes/routes.go
+++ b/internal/provider/kubernetes/routes.go
@@ -412,6 +412,7 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 							}
 							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)
 							}
diff --git a/internal/xds/translator/credentialInjector.go b/internal/xds/translator/credentialInjector.go
new file mode 100644
index 000000000..1df31c913
--- /dev/null
+++ b/internal/xds/translator/credentialInjector.go
@@ -0,0 +1,159 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package translator
+
+import (
+	"errors"
+	"fmt"
+
+	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
+	injectorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/credential_injector/v3"
+	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	genericv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/injected_credentials/generic/v3"
+	tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
+
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/ir"
+	"github.com/envoyproxy/gateway/internal/utils/proto"
+	"github.com/envoyproxy/gateway/internal/xds/types"
+)
+
+func init() {
+	registerHTTPFilter(&credentialInjector{})
+}
+
+type credentialInjector struct{}
+
+var _ httpFilter = &credentialInjector{}
+
+// patchHCM updates the HTTPConnectionManager with a Credential Injector HTTP filter for routes requiring credential injection.
+func (*credentialInjector) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
+	if mgr == nil {
+		return errors.New("hcm is nil")
+	}
+	if irListener == nil {
+		return errors.New("ir listener is nil")
+	}
+
+	var errs error
+
+	for _, route := range irListener.Routes {
+		if route.CredentialInjection == nil {
+			continue
+		}
+
+		if hcmContainsFilter(mgr, credentialInjectorFilterName(route.CredentialInjection)) {
+			continue
+		}
+
+		filter, err := buildHCMCredentialInjectorFilter(route.CredentialInjection)
+		if err != nil {
+			errs = errors.Join(errs, err)
+			continue
+		}
+
+		mgr.HttpFilters = append(mgr.HttpFilters, filter)
+	}
+	return errs
+}
+
+func credentialInjectorFilterName(credentialInjection *ir.CredentialInjection) string {
+	return perRouteFilterName(egv1a1.EnvoyFilterCredentialInjector, credentialInjection.Name)
+}
+
+// buildHCMCredentialInjectorFilter returns a credentialInjector HTTP filter from the provided IR HTTPRoute.
+func buildHCMCredentialInjectorFilter(credentialInjection *ir.CredentialInjection) (*hcmv3.HttpFilter, error) {
+	genericCredential := &genericv3.Generic{
+		Credential: &tlsv3.SdsSecretConfig{
+			Name:      credentialSecretName(credentialInjection),
+			SdsConfig: makeConfigSource(),
+		},
+	}
+	if credentialInjection.Header != nil && *credentialInjection.Header != "" {
+		genericCredential.Header = *credentialInjection.Header
+	}
+	genericCredentialAny, err := proto.ToAnyWithValidation(genericCredential)
+	if err != nil {
+		return nil, err
+	}
+
+	credentialInjector := &injectorv3.CredentialInjector{
+		Credential: &corev3.TypedExtensionConfig{
+			Name:        "envoy.http.injected_credentials.generic",
+			TypedConfig: genericCredentialAny,
+		},
+	}
+	if credentialInjection.Overwrite != nil {
+		credentialInjector.Overwrite = *credentialInjection.Overwrite
+	}
+
+	credentialInjectorAny, err := proto.ToAnyWithValidation(credentialInjector)
+	if err != nil {
+		return nil, err
+	}
+
+	return &hcmv3.HttpFilter{
+		Name: credentialInjectorFilterName(credentialInjection),
+		ConfigType: &hcmv3.HttpFilter_TypedConfig{
+			TypedConfig: credentialInjectorAny,
+		},
+		Disabled: true,
+	}, nil
+}
+
+func credentialSecretName(credentialInjection *ir.CredentialInjection) string {
+	return fmt.Sprintf("credential_injector/credential/%s", credentialInjection.Name)
+}
+
+func (*credentialInjector) patchResources(resource *types.ResourceVersionTable, routes []*ir.HTTPRoute) error {
+	var errs error
+
+	for _, route := range routes {
+		if route.CredentialInjection != nil {
+			secret := buildCredentialSecret(route.CredentialInjection)
+			if err := addXdsSecret(resource, secret); err != nil {
+				errs = errors.Join(errs, err)
+			}
+		}
+	}
+
+	return errs
+}
+
+func buildCredentialSecret(credentialInjection *ir.CredentialInjection) *tlsv3.Secret {
+	return &tlsv3.Secret{
+		Name: credentialSecretName(credentialInjection),
+		Type: &tlsv3.Secret_GenericSecret{
+			GenericSecret: &tlsv3.GenericSecret{
+				Secret: &corev3.DataSource{
+					Specifier: &corev3.DataSource_InlineBytes{
+						InlineBytes: credentialInjection.Credential,
+					},
+				},
+			},
+		},
+	}
+}
+
+// patchRoute patches the provided route with the credential injector filter if applicable.
+// Note: this method enables the corresponding credential injector filter for the provided route.
+func (*credentialInjector) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error {
+	if route == nil {
+		return errors.New("xds route is nil")
+	}
+	if irRoute == nil {
+		return errors.New("ir route is nil")
+	}
+	if irRoute.CredentialInjection == nil {
+		return nil
+	}
+	filterName := credentialInjectorFilterName(irRoute.CredentialInjection)
+	if err := enableFilterOnRoute(route, filterName); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go
index e291a3b46..1b64bccdc 100644
--- a/internal/xds/translator/httpfilters.go
+++ b/internal/xds/translator/httpfilters.go
@@ -130,10 +130,12 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter {
 		order = 303
 	case isFilterType(filter, egv1a1.EnvoyFilterCustomResponse):
 		order = 304
-	case isFilterType(filter, egv1a1.EnvoyFilterCompressor):
+	case isFilterType(filter, egv1a1.EnvoyFilterCredentialInjector):
 		order = 305
-	case isFilterType(filter, egv1a1.EnvoyFilterRouter):
+	case isFilterType(filter, egv1a1.EnvoyFilterCompressor):
 		order = 306
+	case isFilterType(filter, egv1a1.EnvoyFilterRouter):
+		order = 307
 	}
 
 	return &OrderedHTTPFilter{
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 8831cf243..b996dec10 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -25,6 +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 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
diff --git a/tools/linter/golangci-lint/.golangci.yml b/tools/linter/golangci-lint/.golangci.yml
index a2ab79bdf..74d1d4c16 100644
--- a/tools/linter/golangci-lint/.golangci.yml
+++ b/tools/linter/golangci-lint/.golangci.yml
@@ -59,6 +59,9 @@ linters:
       - linters:
           - bodyclose
         path: test/e2e
+      - linters:
+          - gosec
+        path: api/v1alpha1/envoyproxy_types.go  # EnvoyFilterCredentialInjector EnvoyFilter = "envoy.filters.http.credential_injector"
   settings:
     copyloopvar:
       check-alias: true


Test patch:
diff --git a/internal/gatewayapi/testdata/httproute-with-credential-injection.in.yaml b/internal/gatewayapi/testdata/httproute-with-credential-injection.in.yaml
new file mode 100644
index 000000000..f33e5e83c
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-credential-injection.in.yaml
@@ -0,0 +1,129 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: "*.envoyproxy.io"
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: ExtensionRef
+        extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-1
+    - matches:
+      - path:
+          value: "/bar"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: ExtensionRef
+        extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-2
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: ExtensionRef
+        extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-3
+httpFilters:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: HTTPRouteFilter
+  metadata:
+    name: credential-injection-1
+    namespace: default
+  spec:
+    credentialInjection:
+      credential:
+        valueRef:
+          name: credential-1
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: HTTPRouteFilter
+  metadata:
+    name: credential-injection-2
+    namespace: default
+  spec:
+    credentialInjection:
+      header: x-credential
+      overwrite: true
+      credential:
+        valueRef:
+          name: credential-2
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: HTTPRouteFilter
+  metadata:
+    name: credential-injection-3
+    namespace: default
+  spec:
+    credentialInjection:
+      header: x-credential
+      overwrite: true
+      credential:
+        valueRef:
+          name: non-existent-credential
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    name: credential-1
+    namespace: default
+  data:
+    credential: c2VjcmV0
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    name: credential-2
+    namespace: default
+  data:
+    credential: c2VjcmV0Mg==
diff --git a/internal/gatewayapi/testdata/httproute-with-credential-injection.out.yaml b/internal/gatewayapi/testdata/httproute-with-credential-injection.out.yaml
new file mode 100644
index 000000000..75f179348
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-credential-injection.out.yaml
@@ -0,0 +1,236 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-1
+        type: ExtensionRef
+      matches:
+      - path:
+          value: /foo
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-2
+        type: ExtensionRef
+      matches:
+      - path:
+          value: /bar
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - extensionRef:
+          group: gateway.envoyproxy.io
+          kind: HTTPRouteFilter
+          name: credential-injection-3
+        type: ExtensionRef
+      matches:
+      - path:
+          value: /foo
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: 'Invalid filter HTTPRouteFilter: secret default/non-existent-credential
+          does not exist'
+        reason: UnsupportedValue
+        status: "False"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      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:
+      - credentialInjection:
+          credential: '[redacted]'
+          name: httproutefilter/default/credential-injection-1
+        destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+      - credentialInjection:
+          credential: '[redacted]'
+          header: x-credential
+          name: httproutefilter/default/credential-injection-2
+          overwrite: true
+        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: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/1/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/xds/translator/testdata/in/xds-ir/credential-injection.yaml b/internal/xds/translator/testdata/in/xds-ir/credential-injection.yaml
new file mode 100644
index 000000000..0edc17e02
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/credential-injection.yaml
@@ -0,0 +1,90 @@
+http:
+- address: 0.0.0.0
+  hostnames:
+  - '*.envoyproxy.io'
+  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:
+  - credentialInjection:
+      name: httproutefilter/default/credential-injection-1
+      credential: '[redacted]'
+    destination:
+      name: httproute/default/httproute-1/rule/0
+      settings:
+      - addressType: IP
+        endpoints:
+        - host: 7.7.7.7
+          port: 8080
+        name: httproute/default/httproute-1/rule/0/backend/0
+        protocol: HTTP
+        weight: 1
+    hostname: gateway.envoyproxy.io
+    isHTTP2: false
+    metadata:
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+    name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+    pathMatch:
+      distinct: false
+      name: ""
+      prefix: /foo
+  - credentialInjection:
+      name: httproutefilter/default/credential-injection-2
+      credential: '[redacted]'
+      header: x-credential
+      overwrite: true
+    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: 1
+    hostname: gateway.envoyproxy.io
+    isHTTP2: false
+    metadata:
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+    name: httproute/default/httproute-1/rule/1/match/0/gateway_envoyproxy_io
+    pathMatch:
+      distinct: false
+      name: ""
+      prefix: /bar
+  - credentialInjection:
+      name: httproutefilter/default/credential-injection-1
+      credential: '[redacted]'
+    destination:
+      name: httproute/default/httproute-1/rule/2
+      settings:
+      - addressType: IP
+        endpoints:
+        - host: 7.7.7.7
+          port: 8080
+        name: httproute/default/httproute-1/rule/1/backend/0
+        protocol: HTTP
+        weight: 1
+    hostname: gateway.envoyproxy.io
+    isHTTP2: false
+    metadata:
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+    name: httproute/default/httproute-1/rule/2/match/0/gateway_envoyproxy_io
+    pathMatch:
+      distinct: false
+      name: ""
+      prefix: /baz
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection.clusters.yaml
new file mode 100644
index 000000000..c9c467430
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection.clusters.yaml
@@ -0,0 +1,51 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/1
+  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/2
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/2
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection.endpoints.yaml
new file mode 100644
index 000000000..8f3d6d4be
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection.endpoints.yaml
@@ -0,0 +1,36 @@
+- clusterName: httproute/default/httproute-1/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/0/backend/0
+- clusterName: httproute/default/httproute-1/rule/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/0
+- clusterName: httproute/default/httproute-1/rule/2
+  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/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection.listeners.yaml
new file mode 100644
index 000000000..b05991e95
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection.listeners.yaml
@@ -0,0 +1,62 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.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
+        - disabled: true
+          name: envoy.filters.http.credential_injector/httproutefilter/default/credential-injection-2
+          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-2
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+                header: x-credential
+            overwrite: true
+        - 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.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection.routes.yaml
new file mode 100644
index 000000000..5658b1aa6
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection.routes.yaml
@@ -0,0 +1,69 @@
+- ignorePortInHostMatching: true
+  name: envoy-gateway/gateway-1/http
+  virtualHosts:
+  - domains:
+    - gateway.envoyproxy.io
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: gateway-1
+            namespace: envoy-gateway
+            sectionName: http
+    name: envoy-gateway/gateway-1/http/gateway_envoyproxy_io
+    routes:
+    - match:
+        pathSeparatedPrefix: /foo
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+      route:
+        cluster: httproute/default/httproute-1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.credential_injector/httproutefilter/default/credential-injection-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
+    - match:
+        pathSeparatedPrefix: /bar
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/1/match/0/gateway_envoyproxy_io
+      route:
+        cluster: httproute/default/httproute-1/rule/1
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.credential_injector/httproutefilter/default/credential-injection-2:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
+    - match:
+        pathSeparatedPrefix: /baz
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/2/match/0/gateway_envoyproxy_io
+      route:
+        cluster: httproute/default/httproute-1/rule/2
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.credential_injector/httproutefilter/default/credential-injection-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/credential-injection.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/credential-injection.secrets.yaml
new file mode 100644
index 000000000..88ad69f03
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/credential-injection.secrets.yaml
@@ -0,0 +1,8 @@
+- genericSecret:
+    secret:
+      inlineBytes: W3JlZGFjdGVkXQ==
+  name: credential_injector/credential/httproutefilter/default/credential-injection-1
+- genericSecret:
+    secret:
+      inlineBytes: W3JlZGFjdGVkXQ==
+  name: credential_injector/credential/httproutefilter/default/credential-injection-2
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 23355bd2a..574b56b89 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1104,6 +1104,7 @@ _Appears in:_
 | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.<br /> | 
 | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.<br /> | 
 | `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.<br /> | 
+| `envoy.filters.http.credential_injector` | EnvoyFilterCredentialInjector defines the Envoy HTTP credential injector filter.<br /> | 
 | `envoy.filters.http.compressor` | EnvoyFilterCompressor defines the Envoy HTTP compressor filter.<br /> | 
 | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.<br /> | 
 
diff --git a/test/e2e/testdata/credential-injection.yaml b/test/e2e/testdata/credential-injection.yaml
new file mode 100644
index 000000000..7289ed9db
--- /dev/null
+++ b/test/e2e/testdata/credential-injection.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.go b/test/e2e/tests/credential_injection.go
new file mode 100644
index 000000000..39137c51b
--- /dev/null
+++ b/test/e2e/tests/credential_injection.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, CredentialInjectionTest)
+}
+
+var CredentialInjectionTest = suite.ConformanceTest{
+	ShortName:   "CredentialInjection",
+	Description: "Resource with CredentialInjection enabled",
+	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) {
+			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/fuzz/oss_fuzz_build.sh b/test/fuzz/oss_fuzz_build.sh
index 5aacf4f9a..73448a71f 100755
--- a/test/fuzz/oss_fuzz_build.sh
+++ b/test/fuzz/oss_fuzz_build.sh
@@ -34,4 +34,5 @@ go mod tidy
 
 # compile native-format fuzzers
 compile_native_go_fuzzer github.com/envoyproxy/gateway/test/fuzz FuzzGatewayAPIToXDS FuzzGatewayAPIToXDS
+compile_native_go_fuzzer github.com/envoyproxy/gateway/test/fuzz FuzzGatewayAPIToXDSWithGatewayClass FuzzGatewayAPIToXDSWithGatewayClass
 
diff --git a/test/fuzz/xds_fuzz_test.go b/test/fuzz/xds_fuzz_test.go
index 7bde70604..d5dca8138 100644
--- a/test/fuzz/xds_fuzz_test.go
+++ b/test/fuzz/xds_fuzz_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	fuzz "github.com/AdaLogics/go-fuzz-headers"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/cmd/egctl"
@@ -52,3 +54,21 @@ func FuzzGatewayAPIToXDS(f *testing.F) {
 		_, _ = egctl.TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType, rs)
 	})
 }
+
+func FuzzGatewayAPIToXDSWithGatewayClass(f *testing.F) {
+	f.Fuzz(func(t *testing.T, name, controllerName, namespace, dnsDomain, resourceType string) {
+		resources := &resource.Resources{
+			GatewayClass: &gwapiv1.GatewayClass{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      name,
+					Namespace: namespace,
+				},
+				Spec: gwapiv1.GatewayClassSpec{
+					ControllerName: gwapiv1.GatewayController(controllerName),
+				},
+			},
+		}
+
+		_, _ = egctl.TranslateGatewayAPIToXds(namespace, dnsDomain, resourceType, resources)
+	})
+}