From 4ae4ecfd51a697607d0ba5f61db5f5c6d2209cc0 Mon Sep 17 00:00:00 2001 From: Rudrakh Panigrahi Date: Mon, 11 Nov 2024 14:37:37 +0530 Subject: [PATCH] feat:support configuring xff trusted cidrs Signed-off-by: Rudrakh Panigrahi --- internal/ir/xds.go | 93 +++++++++++-------- internal/xds/translator/listener.go | 37 +++++++- .../in/xds-ir/client-ip-detection.yaml | 20 ++++ .../xds-ir/client-ip-detection.clusters.yaml | 17 ++++ .../xds-ir/client-ip-detection.endpoints.yaml | 12 +++ .../xds-ir/client-ip-detection.listeners.yaml | 54 ++++++++++- .../xds-ir/client-ip-detection.routes.yaml | 14 +++ ...authorization-client-ip-trusted-cidrs.yaml | 53 +++++++++++ test/e2e/tests/authorization_client_ip.go | 90 ++++++++++++++++++ 9 files changed, 345 insertions(+), 45 deletions(-) create mode 100644 test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d99969f532c..46ee1e22c80 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -35,47 +35,49 @@ const ( ) var ( - ErrListenerNameEmpty = errors.New("field Name must be specified") - ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") - ErrListenerPortInvalid = errors.New("field Port specified is invalid") - ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") - ErrTCPRouteSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") - ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") - ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") - ErrRouteNameEmpty = errors.New("field Name must be specified") - ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") - ErrDestinationNameEmpty = errors.New("field Name must be specified") - ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") - ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") - ErrDestEndpointUDSPortInvalid = errors.New("field Port must not be specified for Unix Domain Socket address") - ErrDestEndpointUDSHostInvalid = errors.New("field Host must not be specified for Unix Domain Socket address") - ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") - ErrStringMatchInvertDistinctInvalid = errors.New("only one of the Invert or Distinct fields can be set") - ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") - ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") - ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") - ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") - ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") - ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header and Backend") - ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") - ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") - ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") - ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") - ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") - ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") - ErrHealthCheckUnhealthyThresholdInvalid = errors.New("field HealthCheck.UnhealthyThreshold should be greater than 0") - ErrHealthCheckHealthyThresholdInvalid = errors.New("field HealthCheck.HealthyThreshold should be greater than 0") - ErrHealthCheckerInvalid = errors.New("health checker setting is invalid, only one health checker can be set") - ErrHCHTTPHostInvalid = errors.New("field HTTPHealthChecker.Host should be specified") - ErrHCHTTPPathInvalid = errors.New("field HTTPHealthChecker.Path should be specified") - ErrHCHTTPMethodInvalid = errors.New("only one of the GET, HEAD, POST, DELETE, OPTIONS, TRACE, PATCH of HTTPHealthChecker.Method could be set") - ErrHCHTTPExpectedStatusesInvalid = errors.New("field HTTPHealthChecker.ExpectedStatuses should be specified") - ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") - ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") - ErrOutlierDetectionBaseEjectionTimeInvalid = errors.New("field OutlierDetection.BaseEjectionTime must be specified") - ErrOutlierDetectionIntervalInvalid = errors.New("field OutlierDetection.Interval must be specified") + ErrListenerNameEmpty = errors.New("field Name must be specified") + ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") + ErrListenerPortInvalid = errors.New("field Port specified is invalid") + ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") + ErrTCPRouteSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") + ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") + ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") + ErrRouteNameEmpty = errors.New("field Name must be specified") + ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") + ErrDestinationNameEmpty = errors.New("field Name must be specified") + ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") + ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") + ErrDestEndpointUDSPortInvalid = errors.New("field Port must not be specified for Unix Domain Socket address") + ErrDestEndpointUDSHostInvalid = errors.New("field Host must not be specified for Unix Domain Socket address") + ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") + ErrStringMatchInvertDistinctInvalid = errors.New("only one of the Invert or Distinct fields can be set") + ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") + ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") + ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") + ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") + ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") + ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") + ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") + ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header and Backend") + ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") + ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") + ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") + ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") + ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") + ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") + ErrHealthCheckUnhealthyThresholdInvalid = errors.New("field HealthCheck.UnhealthyThreshold should be greater than 0") + ErrHealthCheckHealthyThresholdInvalid = errors.New("field HealthCheck.HealthyThreshold should be greater than 0") + ErrHealthCheckerInvalid = errors.New("health checker setting is invalid, only one health checker can be set") + ErrHCHTTPHostInvalid = errors.New("field HTTPHealthChecker.Host should be specified") + ErrHCHTTPPathInvalid = errors.New("field HTTPHealthChecker.Path should be specified") + ErrHCHTTPMethodInvalid = errors.New("only one of the GET, HEAD, POST, DELETE, OPTIONS, TRACE, PATCH of HTTPHealthChecker.Method could be set") + ErrHCHTTPExpectedStatusesInvalid = errors.New("field HTTPHealthChecker.ExpectedStatuses should be specified") + ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") + ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") + ErrOutlierDetectionBaseEjectionTimeInvalid = errors.New("field OutlierDetection.BaseEjectionTime must be specified") + ErrOutlierDetectionIntervalInvalid = errors.New("field OutlierDetection.Interval must be specified") + 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") redacted = []byte("[redacted]") ) @@ -345,6 +347,15 @@ func (h HTTPListener) Validate() error { errs = errors.Join(errs, err) } } + if h.ClientIPDetection != nil { + if h.ClientIPDetection.XForwardedFor != nil && h.ClientIPDetection.CustomHeader != nil { + errs = errors.Join(errs, ErrBothXForwardedForAndCustomHeaderInvalid) + } else if h.ClientIPDetection.XForwardedFor != nil { + if h.ClientIPDetection.XForwardedFor.NumTrustedHops != nil && h.ClientIPDetection.XForwardedFor.TrustedCIDRs != nil { + errs = errors.Join(errs, ErrBothNumTrustedHopsAndTrustedCIDRsInvalid) + } + } + } return errs } diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 36cf9a8953b..6bae78ddab1 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -23,12 +23,14 @@ import ( early_header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3" preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3" customheaderv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/original_ip_detection/custom_header/v3" + xffv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/original_ip_detection/xff/v3" quicv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" "k8s.io/utils/ptr" @@ -108,9 +110,12 @@ func http2ProtocolOptions(opts *ir.HTTP2Settings) *corev3.Http2ProtocolOptions { return out } +// xffNumTrustedHops returns the number of hops to be configured in proxy +// Need to decrement number of hops configured by EGW user by 1 for backward compatibility +// See for more: https://github.com/envoyproxy/envoy/issues/34241 func xffNumTrustedHops(clientIPDetection *ir.ClientIPDetectionSettings) uint32 { if clientIPDetection != nil && clientIPDetection.XForwardedFor != nil && clientIPDetection.XForwardedFor.NumTrustedHops != nil { - return *clientIPDetection.XForwardedFor.NumTrustedHops + return *clientIPDetection.XForwardedFor.NumTrustedHops - 1 } return 0 } @@ -141,6 +146,35 @@ func originalIPDetectionExtensions(clientIPDetection *ir.ClientIPDetectionSettin Name: "envoy.extensions.http.original_ip_detection.custom_header", TypedConfig: customHeaderConfigAny, }) + } else if clientIPDetection.XForwardedFor != nil { + var xffHeaderConfigAny *anypb.Any + if clientIPDetection.XForwardedFor.TrustedCIDRs != nil { + trustedCidrs := make([]*corev3.CidrRange, 0) + for _, cidr := range clientIPDetection.XForwardedFor.TrustedCIDRs { + parsedCidr := strings.Split(string(cidr), "/") + addressPrefix := parsedCidr[0] + prefixLen, _ := strconv.ParseUint(parsedCidr[1], 10, 32) + trustedCidrs = append(trustedCidrs, &corev3.CidrRange{ + AddressPrefix: addressPrefix, + PrefixLen: wrapperspb.UInt32(uint32(prefixLen)), + }) + } + xffHeaderConfigAny, _ = protocov.ToAnyWithValidation(&xffv3.XffConfig{ + XffTrustedCidrs: &xffv3.XffTrustedCidrs{ + Cidrs: trustedCidrs, + }, + SkipXffAppend: wrapperspb.Bool(false), + }) + } else if clientIPDetection.XForwardedFor.NumTrustedHops != nil { + xffHeaderConfigAny, _ = protocov.ToAnyWithValidation(&xffv3.XffConfig{ + XffNumTrustedHops: xffNumTrustedHops(clientIPDetection), + SkipXffAppend: wrapperspb.Bool(false), + }) + } + extensionConfig = append(extensionConfig, &corev3.TypedExtensionConfig{ + Name: "envoy.extensions.http.original_ip_detection.xff", + TypedConfig: xffHeaderConfigAny, + }) } return extensionConfig @@ -292,7 +326,6 @@ func (t *Translator) addHCMToXDSListener(xdsListener *listenerv3.Listener, irLis Http2ProtocolOptions: http2ProtocolOptions(irListener.HTTP2), // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for UseRemoteAddress: &wrapperspb.BoolValue{Value: useRemoteAddress}, - XffNumTrustedHops: xffNumTrustedHops(irListener.ClientIPDetection), OriginalIpDetectionExtensions: originalIPDetectionExtensions, // normalize paths according to RFC 3986 NormalizePath: &wrapperspb.BoolValue{Value: true}, diff --git a/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml b/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml index 1894902a0ba..9dcdae06a4c 100644 --- a/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml @@ -52,3 +52,23 @@ http: customHeader: name: "x-my-custom-header" failClosed: true +- name: "fourth-listener" + address: "::" + port: 8084 + hostnames: + - "*" + routes: + - name: "fourth-route" + hostname: "*" + destination: + name: "fourth-route-dest" + settings: + - endpoints: + - host: "4.4.4.4" + port: 8084 + clientIPDetection: + xForwardedFor: + trustedCidrs: + - "192.168.1.0/24" + - "10.0.0.0/16" + - "172.16.0.0/12" diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml index 121c8aad8bb..dba5fc4a3cd 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml @@ -49,3 +49,20 @@ name: third-route-dest perConnectionBufferLimitBytes: 32768 type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: fourth-route-dest + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: fourth-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml index 59545ddec3a..ad653a1de59 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml @@ -34,3 +34,15 @@ loadBalancingWeight: 1 locality: region: third-route-dest/backend/0 +- clusterName: fourth-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 4.4.4.4 + portValue: 8084 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: fourth-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml index 4515aa70761..58d7f823aee 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml @@ -19,6 +19,12 @@ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router suppressEnvoyHeaders: true normalizePath: true + originalIpDetectionExtensions: + - name: envoy.extensions.http.original_ip_detection.xff + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + skipXffAppend: false + xffNumTrustedHops: 1 rds: configSource: ads: {} @@ -26,8 +32,7 @@ routeConfigName: first-listener serverHeaderTransformation: PASS_THROUGH statPrefix: http-8081 - useRemoteAddress: true - xffNumTrustedHops: 2 + useRemoteAddress: false name: first-listener name: first-listener perConnectionBufferLimitBytes: 32768 @@ -109,3 +114,48 @@ name: third-listener name: third-listener perConnectionBufferLimitBytes: 32768 +- address: + socketAddress: + address: '::' + portValue: 8084 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + normalizePath: true + originalIpDetectionExtensions: + - name: envoy.extensions.http.original_ip_detection.xff + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + skipXffAppend: false + xffTrustedCidrs: + cidrs: + - addressPrefix: 192.168.1.0 + prefixLen: 24 + - addressPrefix: 10.0.0.0 + prefixLen: 16 + - addressPrefix: 172.16.0.0 + prefixLen: 12 + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: fourth-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-8084 + useRemoteAddress: false + name: fourth-listener + name: fourth-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml index 12a38a14ef8..a0e9171307d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml @@ -40,3 +40,17 @@ cluster: third-route-dest upgradeConfigs: - upgradeType: websocket +- ignorePortInHostMatching: true + name: fourth-listener + virtualHosts: + - domains: + - '*' + name: fourth-listener/* + routes: + - match: + prefix: / + name: fourth-route + route: + cluster: fourth-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml b/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml new file mode 100644 index 00000000000..5b56ddbab27 --- /dev/null +++ b/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml @@ -0,0 +1,53 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-client-ip-trusted-cidr + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /protected1 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-client-ip-trusted-cidr + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-client-ip-trusted-cidr + authorization: + defaultAction: Allow + rules: + - name: "deny-location-1" + action: Deny + principal: + clientCIDRs: + - 192.168.1.0/24 +--- +# This is a client traffic policy that enables client IP detection using the XFF header. +# So, the client IP can be detected from the XFF header and used for authorization. +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: enable-client-ip-detection-trusted-cidr + namespace: gateway-conformance-infra +spec: + clientIPDetection: + xForwardedFor: + trustedCIDRs: + - "172.16.0.0/12" + - "10.0.1.0/24" + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace diff --git a/test/e2e/tests/authorization_client_ip.go b/test/e2e/tests/authorization_client_ip.go index 698a4d73a6a..7f766fe81ca 100644 --- a/test/e2e/tests/authorization_client_ip.go +++ b/test/e2e/tests/authorization_client_ip.go @@ -23,6 +23,7 @@ import ( func init() { ConformanceTests = append(ConformanceTests, AuthorizationClientIPTest) + ConformanceTests = append(ConformanceTests, AuthorizationClientIPTrustedCidrsTest) } var AuthorizationClientIPTest = suite.ConformanceTest{ @@ -150,3 +151,92 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ }) }, } + +var AuthorizationClientIPTrustedCidrsTest = suite.ConformanceTest{ + ShortName: "AuthzWithClientIPTrustedCIDRs", + Description: "Authorization with client IP Allow/Deny list using trusted CIDRs", + Manifests: []string{"testdata/authorization-client-ip-trusted-cidrs.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + route1NN := types.NamespacedName{Name: "http-with-authorization-client-ip-trusted-cidr", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), route1NN) + + 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: "authorization-client-ip-trusted-cidr", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("first route-allowed IP no proxy", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "X-Forwarded-For": "192.168.2.1", // not in deny list + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/protected1", + Headers: nil, // don't check headers since Envoy will append the client IP to the X-Forwarded-For header + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + + t.Run("first route-allowed IP with trusted proxies", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "X-Forwarded-For": "192.168.2.1,172.16.1.17,10.0.1.12", // first untrusted ip 192.168.2.1 not in deny list + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/protected1", + Headers: nil, // don't check headers since Envoy will append the client IP to the X-Forwarded-For header + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + + t.Run("first route-allowed IP with an untrusted proxy", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "X-Forwarded-For": "192.168.2.1,192.168.1.1,10.0.1.12", // first untrusted ip 192.168.1.1 in deny list + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/protected1", + Headers: nil, // don't check headers since Envoy will append the client IP to the X-Forwarded-For header + }, + }, + Response: http.Response{ + StatusCode: 403, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +}