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) + }) }, }