diff --git a/api/v1alpha1/basic_auth_types.go b/api/v1alpha1/basic_auth_types.go
index cc98c1e6f0..ec12473bd7 100644
--- a/api/v1alpha1/basic_auth_types.go
+++ b/api/v1alpha1/basic_auth_types.go
@@ -33,6 +33,5 @@ type BasicAuth struct {
// If it is not specified, the username will not be forwarded.
//
// +optional
- // +notImplementedHide
ForwardUsernameHeader *string `json:"forwardUsernameHeader,omitempty"`
}
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 0bbd78fd7f..7d5e18e49b 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -1119,8 +1119,9 @@ func (t *Translator) buildBasicAuth(
}
return &ir.BasicAuth{
- Name: irConfigName(policy),
- Users: usersSecretBytes,
+ Name: irConfigName(policy),
+ Users: usersSecretBytes,
+ ForwardUsernameHeader: basicAuth.ForwardUsernameHeader,
}, nil
}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 4383ffb5d5..9980e99eaf 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -1083,6 +1083,12 @@ type BasicAuth struct {
// The username-password pairs in htpasswd format.
Users PrivateBytes `json:"users,omitempty" yaml:"users,omitempty"`
+
+ // This field specifies the header name to forward a successfully authenticated user to
+ // the backend. The header will be added to the request with the username as the value.
+ //
+ // If it is not specified, the username will not be forwarded.
+ ForwardUsernameHeader *string `json:"forwardUsernameHeader,omitempty" yaml:"forwardUsernameHeader,omitempty"`
}
// APIKeyAuth defines the schema for the API Key Authentication.
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 98cf2a9976..6119789062 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -360,6 +360,11 @@ func (in *BasicAuth) DeepCopyInto(out *BasicAuth) {
*out = make(PrivateBytes, len(*in))
copy(*out, *in)
}
+ if in.ForwardUsernameHeader != nil {
+ in, out := &in.ForwardUsernameHeader, &out.ForwardUsernameHeader
+ *out = new(string)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuth.
diff --git a/internal/xds/translator/basicauth.go b/internal/xds/translator/basicauth.go
index 4d22377c74..502f15e59c 100644
--- a/internal/xds/translator/basicauth.go
+++ b/internal/xds/translator/basicauth.go
@@ -29,8 +29,12 @@ type basicAuth struct{}
var _ httpFilter = &basicAuth{}
-// patchHCM builds and appends the basic_auth Filter to the HTTP Connection Manager
-// if applicable, and it does not already exist.
+// patchHCM updates the HTTPConnectionManager with a Basic Auth HTTP filter for routes requiring authentication.
+// It scans through all routes in the provided HTTPListener, and if a route has a BasicAuth configuration,
+// it checks for the presence of a corresponding filter in the manager. If the filter is not already present,
+// it generates and appends a new Basic Auth filter.
+// The function returns an error if either the HTTPConnectionManager or HTTPListener is nil, or if an error occurs
+// during the filter creation process.
func (*basicAuth) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
if mgr == nil {
return errors.New("hcm is nil")
@@ -38,35 +42,36 @@ func (*basicAuth) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTP
if irListener == nil {
return errors.New("ir listener is nil")
}
- if hcmContainsFilter(mgr, egv1a1.EnvoyFilterBasicAuth.String()) {
- return nil
- }
- var (
- irBasicAuth *ir.BasicAuth
- filter *hcmv3.HttpFilter
- err error
- )
+ var errs error
for _, route := range irListener.Routes {
- if route.Security != nil && route.Security.BasicAuth != nil {
- irBasicAuth = route.Security.BasicAuth
- break
+ if route.Security == nil || route.Security.BasicAuth == nil {
+ continue
}
- }
- if irBasicAuth == nil {
- return nil
- }
- // We use the first route that contains the basicAuth config to build the filter.
- // The HCM-level filter config doesn't matter since it is overridden at the route level.
- if filter, err = buildHCMBasicAuthFilter(irBasicAuth); err != nil {
- return err
+ // Only generates one Basic Auth Envoy filter for each unique name.
+ // For example, if there are two routes under the same gateway with the
+ // same BasicAuth config, only one BasicAuth filter will be generated.
+ if hcmContainsFilter(mgr, basicAuthFilterName(route.Security.BasicAuth)) {
+ continue
+ }
+
+ filter, err := buildHCMBasicAuthFilter(route.Security.BasicAuth)
+ if err != nil {
+ errs = errors.Join(errs, err)
+ continue
+ }
+
+ mgr.HttpFilters = append(mgr.HttpFilters, filter)
}
- mgr.HttpFilters = append(mgr.HttpFilters, filter)
- return err
+
+ return errs
}
+// patchHCM builds and appends the basic_auth Filter to the HTTP Connection Manager
+// if applicable, and it does not already exist.
+
// buildHCMBasicAuthFilter returns a basic_auth HTTP filter from the provided IR HTTPRoute.
func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth) (*hcmv3.HttpFilter, error) {
var (
@@ -82,13 +87,17 @@ func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth) (*hcmv3.HttpFilter, error)
},
},
}
+ // Set the ForwardUsernameHeader field if it is specified.
+ if basicAuth.ForwardUsernameHeader != nil && *basicAuth.ForwardUsernameHeader != "" {
+ basicAuthProto.ForwardUsernameHeader = *basicAuth.ForwardUsernameHeader
+ }
if basicAuthAny, err = proto.ToAnyWithValidation(basicAuthProto); err != nil {
return nil, err
}
return &hcmv3.HttpFilter{
- Name: egv1a1.EnvoyFilterBasicAuth.String(),
+ Name: basicAuthFilterName(basicAuth),
ConfigType: &hcmv3.HttpFilter_TypedConfig{
TypedConfig: basicAuthAny,
},
@@ -96,6 +105,10 @@ func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth) (*hcmv3.HttpFilter, error)
}, nil
}
+func basicAuthFilterName(basicAuth *ir.BasicAuth) string {
+ return perRouteFilterName(egv1a1.EnvoyFilterBasicAuth, basicAuth.Name)
+}
+
func (*basicAuth) patchResources(*types.ResourceVersionTable, []*ir.HTTPRoute) error {
return nil
}
@@ -118,9 +131,9 @@ func (*basicAuth) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error
basicAuthAny *anypb.Any
err error
)
-
+ filterName := basicAuthFilterName(irRoute.Security.BasicAuth)
perFilterCfg = route.GetTypedPerFilterConfig()
- if _, ok := perFilterCfg[egv1a1.EnvoyFilterBasicAuth.String()]; ok {
+ if _, ok := perFilterCfg[filterName]; ok {
// This should not happen since this is the only place where the filter
// config is added in a route.
return fmt.Errorf("route already contains filter config: %s, %+v",
@@ -136,7 +149,7 @@ func (*basicAuth) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error
if perFilterCfg == nil {
route.TypedPerFilterConfig = make(map[string]*anypb.Any)
}
- route.TypedPerFilterConfig[egv1a1.EnvoyFilterBasicAuth.String()] = basicAuthAny
+ route.TypedPerFilterConfig[filterName] = basicAuthAny
return nil
}
diff --git a/internal/xds/translator/testdata/in/xds-ir/basic-auth-username-header.yaml b/internal/xds/translator/testdata/in/xds-ir/basic-auth-username-header.yaml
new file mode 100644
index 0000000000..81ab698e95
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/basic-auth-username-header.yaml
@@ -0,0 +1,82 @@
+http:
+- address: 0.0.0.0
+ hostnames:
+ - '*'
+ isHTTP2: false
+ name: default/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - name: httproute/default/httproute-1/rule/0/match/0/www_foo_com
+ hostname: www.foo.com
+ isHTTP2: false
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo1
+ backendWeights:
+ invalid: 0
+ valid: 0
+ destination:
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ security:
+ basicAuth:
+ name: securitypolicy/default/policy-for-http-route-1
+ users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ forwardUsernameHeader: x-username
+ - name: httproute/default/httproute-1/rule/1/match/0/www_foo_com
+ backendWeights:
+ hostname: www.foo.com
+ isHTTP2: false
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo2
+ invalid: 0
+ valid: 0
+ destination:
+ name: httproute/default/httproute-1/rule/1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ security:
+ basicAuth:
+ name: securitypolicy/default/policy-for-http-route-1
+ users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ - name: httproute/default/httproute-2/rule/0/match/0/www_bar_com
+ hostname: www.bar.com
+ isHTTP2: false
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /bar
+ backendWeights:
+ invalid: 0
+ valid: 0
+ destination:
+ name: httproute/default/httproute-2/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ security:
+ basicAuth:
+ name: securitypolicy/default/policy-for-gateway-1
+ users: Zm9vOntTSEF9WXMyM0FnLzVJT1dxWkN3OVFHYVZEZEh3SDAwPQpmb28xOntTSEF9ZGpaMTFxSFkwS09pamV5bUs3YUt2WXV2aHZNPQo=
+ forwardUsernameHeader: x-user-id
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.clusters.yaml
new file mode 100644
index 0000000000..c60ba5e19a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.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-2/rule/0
+ ignoreHealthOnHostRemoval: true
+ lbPolicy: LEAST_REQUEST
+ name: httproute/default/httproute-2/rule/0
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.endpoints.yaml
new file mode 100644
index 0000000000..d8aa94029d
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.endpoints.yaml
@@ -0,0 +1,33 @@
+- clusterName: httproute/default/httproute-1/rule/0
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.7
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality: {}
+- clusterName: httproute/default/httproute-1/rule/1
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.7
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality: {}
+- clusterName: httproute/default/httproute-2/rule/0
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.7
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.listeners.yaml
new file mode 100644
index 0000000000..1ab0be3569
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.listeners.yaml
@@ -0,0 +1,48 @@
+- 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.basic_auth/securitypolicy/default/policy-for-http-route-1
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
+ forwardUsernameHeader: x-username
+ users:
+ inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ - disabled: true
+ name: envoy.filters.http.basic_auth/securitypolicy/default/policy-for-gateway-1
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
+ forwardUsernameHeader: x-user-id
+ users:
+ inlineBytes: Zm9vOntTSEF9WXMyM0FnLzVJT1dxWkN3OVFHYVZEZEh3SDAwPQpmb28xOntTSEF9ZGpaMTFxSFkwS09pamV5bUs3YUt2WXV2aHZNPQo=
+ - 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: default/gateway-1/http
+ serverHeaderTransformation: PASS_THROUGH
+ statPrefix: http-10080
+ useRemoteAddress: true
+ name: default/gateway-1/http
+ name: default/gateway-1/http
+ perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.routes.yaml
new file mode 100644
index 0000000000..114de4cb02
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth-username-header.routes.yaml
@@ -0,0 +1,47 @@
+- ignorePortInHostMatching: true
+ name: default/gateway-1/http
+ virtualHosts:
+ - domains:
+ - www.foo.com
+ name: default/gateway-1/http/www_foo_com
+ routes:
+ - match:
+ pathSeparatedPrefix: /foo1
+ name: httproute/default/httproute-1/rule/0/match/0/www_foo_com
+ route:
+ cluster: httproute/default/httproute-1/rule/0
+ upgradeConfigs:
+ - upgradeType: websocket
+ typedPerFilterConfig:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
+ users:
+ inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ - match:
+ pathSeparatedPrefix: /foo2
+ name: httproute/default/httproute-1/rule/1/match/0/www_foo_com
+ route:
+ cluster: httproute/default/httproute-1/rule/1
+ upgradeConfigs:
+ - upgradeType: websocket
+ typedPerFilterConfig:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
+ users:
+ inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ - domains:
+ - www.bar.com
+ name: default/gateway-1/http/www_bar_com
+ routes:
+ - match:
+ pathSeparatedPrefix: /bar
+ name: httproute/default/httproute-2/rule/0/match/0/www_bar_com
+ route:
+ cluster: httproute/default/httproute-2/rule/0
+ upgradeConfigs:
+ - upgradeType: websocket
+ typedPerFilterConfig:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-gateway-1:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
+ users:
+ inlineBytes: Zm9vOntTSEF9WXMyM0FnLzVJT1dxWkN3OVFHYVZEZEh3SDAwPQpmb28xOntTSEF9ZGpaMTFxSFkwS09pamV5bUs3YUt2WXV2aHZNPQo=
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth.listeners.yaml
index a7accc0ef6..686a1d85b7 100644
--- a/internal/xds/translator/testdata/out/xds-ir/basic-auth.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth.listeners.yaml
@@ -15,11 +15,17 @@
maxConcurrentStreams: 100
httpFilters:
- disabled: true
- name: envoy.filters.http.basic_auth
+ name: envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
+ - disabled: true
+ name: envoy.filters.http.basic_auth/securitypolicy/default/policy-for-gateway-1
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
+ users:
+ inlineBytes: Zm9vOntTSEF9WXMyM0FnLzVJT1dxWkN3OVFHYVZEZEh3SDAwPQpmb28xOntTSEF9ZGpaMTFxSFkwS09pamV5bUs3YUt2WXV2aHZNPQo=
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
diff --git a/internal/xds/translator/testdata/out/xds-ir/basic-auth.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/basic-auth.routes.yaml
index 7e51086d2e..114de4cb02 100644
--- a/internal/xds/translator/testdata/out/xds-ir/basic-auth.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/basic-auth.routes.yaml
@@ -13,7 +13,7 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
- envoy.filters.http.basic_auth:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
@@ -25,7 +25,7 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
- envoy.filters.http.basic_auth:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
@@ -41,7 +41,7 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
- envoy.filters.http.basic_auth:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-gateway-1:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
users:
inlineBytes: Zm9vOntTSEF9WXMyM0FnLzVJT1dxWkN3OVFHYVZEZEh3SDAwPQpmb28xOntTSEF9ZGpaMTFxSFkwS09pamV5bUs3YUt2WXV2aHZNPQo=
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.listeners.yaml
index 9758fe7f17..7003a2ea33 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.listeners.yaml
@@ -18,7 +18,7 @@
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- disabled: true
- name: envoy.filters.http.basic_auth
+ name: envoy.filters.http.basic_auth/securitypolicy/envoy-gateway/policy-for-gateway
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.routes.yaml
index 5299f2ff4f..c6e9c1d93d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-filter-order.routes.yaml
@@ -11,7 +11,7 @@
pathSeparatedPrefix: /foo
name: httproute/envoy-gateway/httproute-1/rule/0/match/0/www_example_com
typedPerFilterConfig:
- envoy.filters.http.basic_auth:
+ envoy.filters.http.basic_auth/securitypolicy/envoy-gateway/policy-for-gateway:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
index 39bfe9f587..5c6552fd10 100755
--- a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
@@ -37,7 +37,7 @@
uri: http://http-backend.envoy-gateway:80/auth
transportApiVersion: V3
- disabled: true
- name: envoy.filters.http.basic_auth
+ name: envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
@@ -99,7 +99,7 @@
uri: http://http-backend.envoy-gateway:80/auth
transportApiVersion: V3
- disabled: true
- name: envoy.filters.http.basic_auth
+ name: envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml
index ae520e2676..5ce26e0d75 100755
--- a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml
@@ -18,7 +18,7 @@
upgradeConfigs:
- upgradeType: websocket
typedPerFilterConfig:
- envoy.filters.http.basic_auth:
+ envoy.filters.http.basic_auth/securitypolicy/default/policy-for-http-route-1:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 913c715c04..3c9d1d5e5c 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -16,6 +16,7 @@ new features: |
Added support for defining Lua EnvoyExtensionPolicies
Added RequestID field in ClientTrafficPolicy.HeaderSettings to configure Envoy X-Request-ID behavior.
Added support for HorizontalPodAutoscaler to helm chart
+ Added support for forwarding the authenticated username to the backend via a configurable header in BasicAuth
bug fixes: |
Fix traffic splitting when filters are attached to the backendRef.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 32c07223e3..a57c390b4e 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -499,6 +499,7 @@ _Appears in:_
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `users` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | | The Kubernetes secret which contains the username-password pairs in
htpasswd format, used to verify user credentials in the "Authorization"
header.
This is an Opaque secret. The username-password pairs should be stored in
the key ".htpasswd". As the key name indicates, the value needs to be the
htpasswd format, for example: "user1:\{SHA\}hashed_user1_password".
Right now, only SHA hash algorithm is supported.
Reference to https://httpd.apache.org/docs/2.4/programs/htpasswd.html
for more details.
Note: The secret must be in the same namespace as the SecurityPolicy. |
+| `forwardUsernameHeader` | _string_ | false | | This field specifies the header name to forward a successfully authenticated user to
the backend. The header will be added to the request with the username as the value.
If it is not specified, the username will not be forwarded. |
#### BodyToExtAuth
diff --git a/test/e2e/testdata/basic-auth.yaml b/test/e2e/testdata/basic-auth.yaml
index 28f8afc3f0..8910702c7a 100644
--- a/test/e2e/testdata/basic-auth.yaml
+++ b/test/e2e/testdata/basic-auth.yaml
@@ -93,3 +93,43 @@ spec:
basicAuth:
users:
name: "basic-auth-users-secret-2"
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ namespace: gateway-conformance-infra
+ name: basic-auth-users-secret-3
+data:
+ .htpasswd: "dXNlcjM6e1NIQX1QcitqQWR4WkdXOFlXVHhGNVJrb2VpTXBkWWs9CnVzZXI0OntTSEF9SC9LemNFcnQ0RTdzdFI1UXltbU8vVkNoTjVzPQ=="
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: http-with-basic-auth-3
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: same-namespace
+ rules:
+ - matches:
+ - path:
+ type: Exact
+ value: /basic-auth-3
+ backendRefs:
+ - name: infra-backend-v1
+ port: 8080
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: basic-auth-3
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: http-with-basic-auth-3
+ basicAuth:
+ users:
+ name: "basic-auth-users-secret-3"
+ forwardUsernameHeader: "X-Authenticated-User"
diff --git a/test/e2e/tests/basic_auth.go b/test/e2e/tests/basic_auth.go
index 298da1c2db..5045265174 100644
--- a/test/e2e/tests/basic_auth.go
+++ b/test/e2e/tests/basic_auth.go
@@ -209,5 +209,49 @@ var BasicAuthTest = suite.ConformanceTest{
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
})
+
+ // This test verifies that when a user successfully authenticates using BasicAuth,
+ // their username is forwarded to the backend as a request header.
+ // The SecurityPolicy "basic-auth-3" is configured with the field "forwardUsernameHeader" set to "X-Authenticated-User".
+ // The request uses valid credentials, and the test ensures that the response contains
+ // the expected status code (200) and includes the "X-Authenticated-User" header with the correct username.
+ t.Run("username forwarded as header", func(t *testing.T) {
+ ns := "gateway-conformance-infra"
+ routeNN := types.NamespacedName{Name: "http-with-basic-auth-3", Namespace: ns}
+ gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+ gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+ ancestorRef := gwapiv1a2.ParentReference{
+ Group: gatewayapi.GroupPtr(gwapiv1.GroupName),
+ Kind: gatewayapi.KindPtr(resource.KindGateway),
+ Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+ Name: gwapiv1.ObjectName(gwNN.Name),
+ }
+ SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "basic-auth-3", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+ expectedResponse := http.ExpectedResponse{
+ Request: http.Request{
+ Path: "/basic-auth-3",
+ Headers: map[string]string{
+ "Authorization": "Basic dXNlcjQ6dGVzdDQ=", // user4:test4
+ },
+ },
+ // Verify that the http header X-Authenticated-User added before sending it to the backend
+ ExpectedRequest: &http.ExpectedRequest{
+ Request: http.Request{
+ Path: "/basic-auth-3",
+ Headers: map[string]string{
+ "X-Authenticated-User": "user4",
+ },
+ },
+ },
+ Response: http.Response{
+ StatusCode: 200,
+ },
+ Namespace: ns,
+ }
+
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+ })
},
}