diff --git a/apis/projectcontour/v1/detailedconditions.go b/apis/projectcontour/v1/detailedconditions.go index f3a8cc1e89a..2bfb7251405 100644 --- a/apis/projectcontour/v1/detailedconditions.go +++ b/apis/projectcontour/v1/detailedconditions.go @@ -144,6 +144,9 @@ const ( // ConditionTypeCORSError describes an error condition related to CORS. ConditionTypeCORSError = "CORSError" + // ConditionTypeIPFilterError describes an error condition related to IP filters. + ConditionTypeIPFilterError = "IPFilterError" + // ConditionTypeJWTVerificationError describes an error condition related to JWT verification. ConditionTypeJWTVerificationError = "JWTVerificationError" diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index 8e199c514bb..18030282222 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -295,6 +295,18 @@ type VirtualHost struct { // Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. // +optional JWTProviders []JWTProvider `json:"jwtProviders,omitempty"` + + // IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching + // requests should be allowed. All other requests will be denied. + // Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. + // The rules defined here may be overridden in a Route. + IPAllowFilterPolicy []IPFilterPolicy `json:"ipAllowPolicy,omitempty"` + + // IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching + // requests should be denied. All other requests will be allowed. + // Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. + // The rules defined here may be overridden in a Route. + IPDenyFilterPolicy []IPFilterPolicy `json:"ipDenyPolicy,omitempty"` } // JWTProvider defines how to verify JWTs on requests. @@ -531,6 +543,18 @@ type Route struct { // The policy for verifying JWTs for requests to this route. // +optional JWTVerificationPolicy *JWTVerificationPolicy `json:"jwtVerificationPolicy,omitempty"` + + // IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching + // requests should be allowed. All other requests will be denied. + // Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. + // The rules defined here override any rules set on the root HTTPProxy. + IPAllowFilterPolicy []IPFilterPolicy `json:"ipAllowPolicy,omitempty"` + + // IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching + // requests should be denied. All other requests will be allowed. + // Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. + // The rules defined here override any rules set on the root HTTPProxy. + IPDenyFilterPolicy []IPFilterPolicy `json:"ipDenyPolicy,omitempty"` } type JWTVerificationPolicy struct { @@ -550,6 +574,29 @@ type JWTVerificationPolicy struct { Disabled bool `json:"disabled,omitempty"` } +// IPFilterSource indicates which IP should be considered for filtering +// +kubebuilder:validation:Enum=Peer;Remote +type IPFilterSource string + +const ( + IPFilterSourcePeer IPFilterSource = "Peer" + IPFilterSourceRemote IPFilterSource = "Remote" +) + +type IPFilterPolicy struct { + // Source indicates how to determine the ip address to filter on, and can be + // one of two values: + // - `Remote` filters on the ip address of the client, accounting for PROXY and + // X-Forwarded-For as needed. + // - `Peer` filters on the ip of the network request, ignoring PROXY and + // X-Forwarded-For. + Source IPFilterSource `json:"source"` + + // CIDR is a CIDR block of ipv4 or ipv6 addresses to filter on. This can also be + // a bare IP address (without a mask) to filter on exactly one address. + CIDR string `json:"cidr"` +} + type HTTPDirectResponsePolicy struct { // StatusCode is the HTTP response status to be returned. // +required diff --git a/apis/projectcontour/v1/zz_generated.deepcopy.go b/apis/projectcontour/v1/zz_generated.deepcopy.go index 7333b6dd5db..54d7bc982d3 100644 --- a/apis/projectcontour/v1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1/zz_generated.deepcopy.go @@ -607,6 +607,21 @@ func (in *HeadersPolicy) DeepCopy() *HeadersPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPFilterPolicy) DeepCopyInto(out *IPFilterPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPFilterPolicy. +func (in *IPFilterPolicy) DeepCopy() *IPFilterPolicy { + if in == nil { + return nil + } + out := new(IPFilterPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Include) DeepCopyInto(out *Include) { *out = *in @@ -1088,6 +1103,16 @@ func (in *Route) DeepCopyInto(out *Route) { *out = new(JWTVerificationPolicy) **out = **in } + if in.IPAllowFilterPolicy != nil { + in, out := &in.IPAllowFilterPolicy, &out.IPAllowFilterPolicy + *out = make([]IPFilterPolicy, len(*in)) + copy(*out, *in) + } + if in.IPDenyFilterPolicy != nil { + in, out := &in.IPDenyFilterPolicy, &out.IPDenyFilterPolicy + *out = make([]IPFilterPolicy, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route. @@ -1432,6 +1457,16 @@ func (in *VirtualHost) DeepCopyInto(out *VirtualHost) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.IPAllowFilterPolicy != nil { + in, out := &in.IPAllowFilterPolicy, &out.IPAllowFilterPolicy + *out = make([]IPFilterPolicy, len(*in)) + copy(*out, *in) + } + if in.IPDenyFilterPolicy != nil { + in, out := &in.IPDenyFilterPolicy, &out.IPDenyFilterPolicy + *out = make([]IPFilterPolicy, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualHost. diff --git a/changelogs/unreleased/5008-ecordell-major.md b/changelogs/unreleased/5008-ecordell-major.md new file mode 100644 index 00000000000..d48a15d80e2 --- /dev/null +++ b/changelogs/unreleased/5008-ecordell-major.md @@ -0,0 +1,13 @@ +## IP Filter Support + +Contour's HTTPProxy now supports configuring Envoy's [RBAC filter](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto) for allowing or denying requests by IP. + +An HTTPProxy can optionally include one or more IP filter rules, which define CIDR ranges to allow or deny requests based on origin IP. +Filters can indicate whether the direct IP should be used or whether a reported IP from `PROXY` or `X-Forwarded-For` should be used instead. +If the latter, Contour's `numTrustedHops` setting will be respected when determining the source IP. +Filters defined at the VirtualHost level apply to all routes, unless overridden by a route-specific filter. + +For more information, see: +- [HTTPProxy API documentation](https://projectcontour.io/docs/main/config/api/#projectcontour.io/v1.HTTPProxy) +- [IPFilterPolicy API documentation](https://projectcontour.io/docs/main/config/api/#projectcontour.io/v1.IPFilterPolicy) +- [Envoy RBAC filter documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto) diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index b3d13356256..75d2919923d 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -4903,6 +4903,64 @@ spec: type: integer type: array type: object + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter + rules for which matching requests should be allowed. All other + requests will be denied. Only one of IPAllowFilterPolicy and + IPDenyFilterPolicy can be defined. The rules defined here + override any rules set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here override any rules + set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtVerificationPolicy: description: The policy for verifying JWTs for requests to this route. @@ -6207,6 +6265,62 @@ spec: to the fqdn. pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be allowed. All other requests + will be denied. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtProviders: description: Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index 373e97d221c..c8b4171c02c 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -5116,6 +5116,64 @@ spec: type: integer type: array type: object + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter + rules for which matching requests should be allowed. All other + requests will be denied. Only one of IPAllowFilterPolicy and + IPDenyFilterPolicy can be defined. The rules defined here + override any rules set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here override any rules + set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtVerificationPolicy: description: The policy for verifying JWTs for requests to this route. @@ -6420,6 +6478,62 @@ spec: to the fqdn. pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be allowed. All other requests + will be denied. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtProviders: description: Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 35a41a00019..cc012629d43 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -4917,6 +4917,64 @@ spec: type: integer type: array type: object + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter + rules for which matching requests should be allowed. All other + requests will be denied. Only one of IPAllowFilterPolicy and + IPDenyFilterPolicy can be defined. The rules defined here + override any rules set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here override any rules + set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtVerificationPolicy: description: The policy for verifying JWTs for requests to this route. @@ -6221,6 +6279,62 @@ spec: to the fqdn. pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be allowed. All other requests + will be denied. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtProviders: description: Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index bb8712a0f28..ac21d38f25d 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -5122,6 +5122,64 @@ spec: type: integer type: array type: object + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter + rules for which matching requests should be allowed. All other + requests will be denied. Only one of IPAllowFilterPolicy and + IPDenyFilterPolicy can be defined. The rules defined here + override any rules set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here override any rules + set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtVerificationPolicy: description: The policy for verifying JWTs for requests to this route. @@ -6426,6 +6484,62 @@ spec: to the fqdn. pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be allowed. All other requests + will be denied. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtProviders: description: Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 08808b01b1c..bc22002d22b 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -5116,6 +5116,64 @@ spec: type: integer type: array type: object + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter + rules for which matching requests should be allowed. All other + requests will be denied. Only one of IPAllowFilterPolicy and + IPDenyFilterPolicy can be defined. The rules defined here + override any rules set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here override any rules + set on the root HTTPProxy. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip + address to filter on, and can be one of two values: + - `Remote` filters on the ip address of the client, + accounting for PROXY and X-Forwarded-For as needed. + - `Peer` filters on the ip of the network request, ignoring + PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtVerificationPolicy: description: The policy for verifying JWTs for requests to this route. @@ -6420,6 +6478,62 @@ spec: to the fqdn. pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + ipAllowPolicy: + description: IPAllowFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be allowed. All other requests + will be denied. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array + ipDenyPolicy: + description: IPDenyFilterPolicy is a list of ipv4/6 filter rules + for which matching requests should be denied. All other requests + will be allowed. Only one of IPAllowFilterPolicy and IPDenyFilterPolicy + can be defined. The rules defined here may be overridden in + a Route. + items: + properties: + cidr: + description: CIDR is a CIDR block of ipv4 or ipv6 addresses + to filter on. This can also be a bare IP address (without + a mask) to filter on exactly one address. + type: string + source: + description: 'Source indicates how to determine the ip address + to filter on, and can be one of two values: - `Remote` + filters on the ip address of the client, accounting for + PROXY and X-Forwarded-For as needed. - `Peer` filters + on the ip of the network request, ignoring PROXY and X-Forwarded-For.' + enum: + - Peer + - Remote + type: string + required: + - cidr + - source + type: object + type: array jwtProviders: description: Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host. diff --git a/go.mod b/go.mod index 7b04767aa70..afcdb480c79 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/projectcontour/contour go 1.19 +// remove once https://github.com/cert-manager/cert-manager/issues/5953 is fixed +replace github.com/Venafi/vcert/v4 => github.com/jetstack/vcert/v4 v4.9.6-0.20230127103832-3aa3dfd6613d + require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 diff --git a/internal/dag/dag.go b/internal/dag/dag.go index e06d3018988..2bebe839c69 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -18,6 +18,7 @@ package dag import ( "errors" "fmt" + "net" "regexp" "strconv" "strings" @@ -351,6 +352,16 @@ type Route struct { // InternalRedirectPolicy defines if envoy should handle redirect // response internally instead of sending it downstream. InternalRedirectPolicy *InternalRedirectPolicy + + // IPFilterAllow determines how the IPFilterRules should be applied. + // If true, traffic is allowed only if it matches a rule. + // If false, traffic is allowed only if it doesn't match any rule. + IPFilterAllow bool + + // IPFilterRules is a list of ipv4/6 filter rules for which matching + // requests should be filtered. The behavior of the filters is governed + // by IPFilterAllow. + IPFilterRules []IPFilterRule } // HasPathPrefix returns whether this route has a PrefixPathCondition. @@ -678,6 +689,16 @@ type VirtualHost struct { // are rate limited. RateLimitPolicy *RateLimitPolicy + // IPFilterAllow determines how the IPFilterRules should be applied. + // If true, traffic is allowed only if it matches a rule. + // If false, traffic is allowed only if it doesn't match any rule. + IPFilterAllow bool + + // IPFilterRules is a list of ipv4/6 filter rules for which matching + // requests should be filtered. The behavior of the filters is governed + // by IPFilterAllow. + IPFilterRules []IPFilterRule + Routes map[string]*Route } @@ -762,6 +783,16 @@ type JWTRule struct { ProviderName string } +type IPFilterRule struct { + // Remote determines what ip to filter on. + // If true, filters on the remote address. If false, filters on the + // immediate network address. + Remote bool + + // CIDR is a CIDR block of a ipv4 or ipv6 addresses to filter on. + CIDR net.IPNet +} + // ExternalAuthorization contains the configuration for enabling // the ExtAuthz filter. type ExternalAuthorization struct { diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 396c8f49644..a16597b0985 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -16,6 +16,7 @@ package dag import ( "errors" "fmt" + "net" "net/http" "net/url" "regexp" @@ -179,6 +180,12 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { return } + if len(proxy.Spec.VirtualHost.IPAllowFilterPolicy) > 0 && len(proxy.Spec.VirtualHost.IPDenyFilterPolicy) > 0 { + validCond.AddError(contour_api_v1.ConditionTypeIPFilterError, "IncompatibleIPAddressFilters", + "Spec.VirtualHost.IPAllowFilterPolicy and Spec.VirtualHost.IPDepnyFilterPolicy cannot both be defined.") + return + } + var tlsEnabled bool if tls := proxy.Spec.VirtualHost.TLS; tls != nil { if tls.Passthrough && tls.EnableFallbackCertificate { @@ -489,6 +496,13 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { p.computeVirtualHostAuthorization(p.GlobalExternalAuthorization, validCond, proxy) } + insecure.IPFilterAllow, insecure.IPFilterRules, err = toIPFilterRules(proxy.Spec.VirtualHost.IPAllowFilterPolicy, proxy.Spec.VirtualHost.IPDenyFilterPolicy, validCond) + if err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeIPFilterError, "IPFilterPolicyNotValid", + "Spec.VirtualHost.IPAllowFilterPolicy or Spec.VirtualHost.IPDenyFilterPolicy is invalid: %s", err) + return + } + addRoutes(insecure, routes) // if TLS is enabled for this virtual host and there is no tcp proxy defined, @@ -505,6 +519,13 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { } secure.RateLimitPolicy = rlp + secure.IPFilterAllow, secure.IPFilterRules, err = toIPFilterRules(proxy.Spec.VirtualHost.IPAllowFilterPolicy, proxy.Spec.VirtualHost.IPDenyFilterPolicy, validCond) + if err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeIPFilterError, "IPFilterPolicyNotValid", + "Spec.VirtualHost.IPAllowFilterPolicy or Spec.VirtualHost.IPDenyFilterPolicy is invalid: %s", err) + return + } + addRoutes(secure, routes) // Process JWT verification requirements. @@ -975,6 +996,11 @@ func (p *HTTPProxyProcessor) computeRoutes( r.JWTProvider = defaultJWTProvider } + r.IPFilterAllow, r.IPFilterRules, err = toIPFilterRules(route.IPAllowFilterPolicy, route.IPDenyFilterPolicy, validCond) + if err != nil { + return nil + } + routes = append(routes, r) } @@ -983,6 +1009,56 @@ func (p *HTTPProxyProcessor) computeRoutes( return routes } +// toIPFilterRules converts ip filter settings from the api into the +// dag representation +func toIPFilterRules(allowPolicy, denyPolicy []contour_api_v1.IPFilterPolicy, validCond *contour_api_v1.DetailedCondition) (allow bool, filters []IPFilterRule, err error) { + var ipPolicies []contour_api_v1.IPFilterPolicy + switch { + case len(allowPolicy) > 0 && len(denyPolicy) > 0: + validCond.AddError(contour_api_v1.ConditionTypeIPFilterError, "IncompatibleIPAddressFilters", + "cannot specify both `ipAllowPolicy` and `ipDenyPolicy`") + err = fmt.Errorf("invalid ip filter") + return + case len(allowPolicy) > 0: + allow = true + ipPolicies = allowPolicy + case len(denyPolicy) > 0: + allow = false + ipPolicies = denyPolicy + } + if ipPolicies == nil { + return + } + filters = make([]IPFilterRule, 0, len(ipPolicies)) + for _, p := range ipPolicies { + // convert bare IPs to CIDRs + unparsedCIDR := p.CIDR + if !strings.Contains(unparsedCIDR, "/") { + if strings.Contains(unparsedCIDR, ":") { + unparsedCIDR += "/128" + } else { + unparsedCIDR += "/32" + } + } + var cidr *net.IPNet + _, cidr, err = net.ParseCIDR(unparsedCIDR) + if err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeIPFilterError, "InvalidCIDR", + "%s failed to parse: %s", p.CIDR, err) + continue + } + filters = append(filters, IPFilterRule{ + Remote: p.Source == contour_api_v1.IPFilterSourceRemote, + CIDR: *cidr, + }) + } + if err != nil { + allow = false + filters = nil + } + return +} + // processHTTPProxyTCPProxy processes the spec.tcpproxy stanza in a HTTPProxy document // following the chain of spec.tcpproxy.include references. It returns true if processing // was successful, otherwise false if an error was encountered. The details of the error diff --git a/internal/dag/httpproxy_processor_test.go b/internal/dag/httpproxy_processor_test.go index 93958ba9b81..404a8096a4c 100644 --- a/internal/dag/httpproxy_processor_test.go +++ b/internal/dag/httpproxy_processor_test.go @@ -14,6 +14,7 @@ package dag import ( + "net" "testing" "time" @@ -871,3 +872,120 @@ func TestDetermineExternalAuthTimeout(t *testing.T) { }) } } + +func TestToIPFilterRule(t *testing.T) { + tests := map[string]struct { + allowPolicy []contour_api_v1.IPFilterPolicy + denyPolicy []contour_api_v1.IPFilterPolicy + want []IPFilterRule + wantAllow bool + wantErr bool + wantConditionErrs []contour_api_v1.SubCondition + }{ + "no ip policy": { + allowPolicy: nil, + denyPolicy: []contour_api_v1.IPFilterPolicy{}, + want: nil, + }, + "both allow and deny rules not supported": { + allowPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "1.1.1.1/24", + }}, + denyPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "2.2.2.2/24", + }}, + wantErr: true, + wantConditionErrs: []contour_api_v1.SubCondition{{ + Type: "IPFilterError", + Status: "True", + Reason: "IncompatibleIPAddressFilters", + Message: "cannot specify both `ipAllowPolicy` and `ipDenyPolicy`", + }}, + }, + "reports invalid cidr ranges": { + allowPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "!@#$!@#$", + }, { + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "2.2.2.2/512", + }}, + wantErr: true, + wantConditionErrs: []contour_api_v1.SubCondition{ + { + Type: "IPFilterError", + Status: "True", + Reason: "InvalidCIDR", + Message: "!@#$!@#$ failed to parse: invalid CIDR address: !@#$!@#$/32", + }, + { + Type: "IPFilterError", + Status: "True", + Reason: "InvalidCIDR", + Message: "2.2.2.2/512 failed to parse: invalid CIDR address: 2.2.2.2/512", + }, + }, + }, + "parses multiple allow rules": { + allowPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "1.1.1.1", + }, { + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "2001:db8::68/24", + }}, + wantAllow: true, + want: []IPFilterRule{{ + Remote: true, + CIDR: net.IPNet{ + IP: net.ParseIP("1.1.1.1").To4(), + Mask: net.CIDRMask(32, 32), + }, + }, { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:d00::"), + Mask: net.CIDRMask(24, 128), + }, + }}, + }, + "parses multiple deny rules": { + denyPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "1.1.1.1/24", + }, { + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "2001:db8::68", + }}, + wantAllow: false, + want: []IPFilterRule{{ + Remote: true, + CIDR: net.IPNet{ + IP: net.ParseIP("1.1.1.0").To4(), + Mask: net.CIDRMask(24, 32), + }, + }, { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(128, 128), + }, + }}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + cond := contour_api_v1.DetailedCondition{} + gotAllow, got, gotErr := toIPFilterRules(tc.allowPolicy, tc.denyPolicy, &cond) + if tc.wantErr { + require.Error(t, gotErr) + } + require.Equal(t, tc.want, got) + require.Equal(t, tc.wantAllow, gotAllow) + require.Equal(t, tc.wantConditionErrs, cond.Errors) + }) + } +} diff --git a/internal/dag/status_test.go b/internal/dag/status_test.go index 25f7dec4cf5..4a6d2c586c9 100644 --- a/internal/dag/status_test.go +++ b/internal/dag/status_test.go @@ -4114,6 +4114,153 @@ func TestDAGStatus(t *testing.T) { }, }) + ipFilterVirtualHostValidProxy := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "roots", + Name: "ip-filter-valid-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "example.com", + IPAllowFilterPolicy: []contour_api_v1.IPFilterPolicy{ + { + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "10.8.8.8/0", + }, + { + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "10.8.8.8/0", + }, + }, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/foo", + }}, + Services: []contour_api_v1.Service{{ + Name: "home", + Port: 8080, + }}, + }, + }, + }, + } + + run(t, "virtualhost ip-filter valid proxy", testcase{ + objs: []interface{}{ + ipFilterVirtualHostValidProxy, + fixture.ServiceRootsHome, + }, + want: map[types.NamespacedName]contour_api_v1.DetailedCondition{ + k8s.NamespacedNameOf(ipFilterVirtualHostValidProxy): fixture.NewValidCondition().Valid(), + }, + }) + + ipFilterVirtualHostAllowAndDenyInvalidProxy := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "roots", + Name: "ip-filter-invalid-allow-and-deny-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "example.com", + IPAllowFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "10.8.8.8/0", + }}, + IPDenyFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "10.8.8.8/0", + }}, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/foo", + }}, + Services: []contour_api_v1.Service{{ + Name: "home", + Port: 8080, + }}, + }, + }, + }, + } + + run(t, "virtualhost ip-filter invalid allow and deny proxy", testcase{ + objs: []interface{}{ + ipFilterVirtualHostAllowAndDenyInvalidProxy, + fixture.ServiceRootsHome, + }, + want: map[types.NamespacedName]contour_api_v1.DetailedCondition{ + k8s.NamespacedNameOf(ipFilterVirtualHostAllowAndDenyInvalidProxy): fixture.NewValidCondition(). + WithError( + contour_api_v1.ConditionTypeIPFilterError, + "IncompatibleIPAddressFilters", + "Spec.VirtualHost.IPAllowFilterPolicy and Spec.VirtualHost.IPDepnyFilterPolicy cannot both be defined.", + ), + }, + }) + + ipFilterVirtualHostFilterRulesInvalidProxy := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "roots", + Name: "ip-filter-invalid-filter-rules-proxy", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "example.com", + IPAllowFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "abcd", + }}, + }, + Routes: []contour_api_v1.Route{ + { + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/foo", + }}, + Services: []contour_api_v1.Service{{ + Name: "home", + Port: 8080, + }}, + }, + }, + }, + } + + run(t, "virtualhost ip-filter invalid filter rules proxy", testcase{ + objs: []interface{}{ + ipFilterVirtualHostFilterRulesInvalidProxy, + fixture.ServiceRootsHome, + }, + want: map[types.NamespacedName]contour_api_v1.DetailedCondition{ + k8s.NamespacedNameOf(ipFilterVirtualHostFilterRulesInvalidProxy): { + Condition: contour_api_v1.Condition{ + Type: contour_api_v1.ValidConditionType, + Status: contour_api_v1.ConditionFalse, + Reason: "ErrorPresent", + Message: "At least one error present, see Errors for details", + }, + Errors: []contour_api_v1.SubCondition{ + { + Type: contour_api_v1.ConditionTypeIPFilterError, + Status: contour_api_v1.ConditionTrue, + Reason: "InvalidCIDR", + Message: "abcd failed to parse: invalid CIDR address: abcd/32", + }, + { + Type: contour_api_v1.ConditionTypeIPFilterError, + Status: contour_api_v1.ConditionTrue, + Reason: "IPFilterPolicyNotValid", + Message: "Spec.VirtualHost.IPAllowFilterPolicy or Spec.VirtualHost.IPDenyFilterPolicy is invalid: invalid CIDR address: abcd/32", + }, + }, + }, + }, + }) + // proxyWithInvalidSlowStartWindow is invalid because it has invalid window size syntax. proxyWithInvalidSlowStartWindow := &contour_api_v1.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index a700468a317..9012e8a8c41 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -32,6 +32,7 @@ import ( envoy_jwt_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" envoy_config_filter_http_local_ratelimit_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" lua "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3" + envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" envoy_router_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" envoy_proxy_protocol_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/proxy_protocol/v3" envoy_tls_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" @@ -345,6 +346,12 @@ func (b *httpConnectionManagerBuilder) DefaultFilters() *httpConnectionManagerBu }), }, }, + &http.HttpFilter{ + Name: "envoy.filters.http.rbac", + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_rbac_v3.RBAC{}), + }, + }, &http.HttpFilter{ Name: "router", ConfigType: &http.HttpFilter_TypedConfig{ diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index e6e4ac25c08..a38d70b9d5c 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -28,6 +28,7 @@ import ( envoy_grpc_web_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_web/v3" envoy_config_filter_http_local_ratelimit_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" lua "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3" + envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" envoy_router_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" @@ -585,6 +586,11 @@ func TestHTTPConnectionManager(t *testing.T) { }, }), }, + }, { + Name: "envoy.filters.http.rbac", + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_rbac_v3.RBAC{}), + }, }, { Name: "router", ConfigType: &http.HttpFilter_TypedConfig{ @@ -1783,6 +1789,12 @@ func TestAddFilter(t *testing.T) { }), }, }, + { + Name: "envoy.filters.http.rbac", + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_rbac_v3.RBAC{}), + }, + }, FilterExternalAuthz(&dag.ExternalAuthorization{ AuthorizationService: &dag.ExtensionCluster{ Name: "test", @@ -1879,6 +1891,12 @@ func TestAddFilter(t *testing.T) { }), }, }, + { + Name: "envoy.filters.http.rbac", + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_rbac_v3.RBAC{}), + }, + }, { Name: "envoy.filters.http.ext_authz", ConfigType: &http.HttpFilter_TypedConfig{ diff --git a/internal/envoy/v3/route.go b/internal/envoy/v3/route.go index 974b7b7fa8a..9fe6e2caea1 100644 --- a/internal/envoy/v3/route.go +++ b/internal/envoy/v3/route.go @@ -23,11 +23,13 @@ import ( "text/template" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" envoy_config_filter_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" envoy_jwt_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" lua "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3" + envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" envoy_internal_redirect_previous_routes_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/previous_routes/v3" envoy_internal_redirect_safe_cross_scheme_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/safe_cross_scheme/v3" matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" @@ -66,6 +68,15 @@ func VirtualHostAndRoutes(vh *dag.VirtualHost, dagRoutes []*dag.Route, secure bo evh.RateLimits = GlobalRateLimits(vh.RateLimitPolicy.Global.Descriptors) } + if len(vh.IPFilterRules) > 0 { + if evh.TypedPerFilterConfig == nil { + evh.TypedPerFilterConfig = map[string]*anypb.Any{} + } + evh.TypedPerFilterConfig["envoy.filters.http.rbac"] = protobuf.MustMarshalAny( + ipFilterConfig(vh.IPFilterAllow, vh.IPFilterRules), + ) + } + return evh } @@ -138,6 +149,16 @@ func buildRoute(dagRoute *dag.Route, vhostName string, secure bool) *envoy_route }) } + // If IP filtering is enabled, add per-route filtering + if len(dagRoute.IPFilterRules) > 0 { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*anypb.Any{} + } + rt.TypedPerFilterConfig["envoy.filters.http.rbac"] = protobuf.MustMarshalAny( + ipFilterConfig(dagRoute.IPFilterAllow, dagRoute.IPFilterRules), + ) + } + return rt } } @@ -167,6 +188,60 @@ func routeAuthzContext(settings map[string]string) *anypb.Any { ) } +func ipFilterConfig(allow bool, rules []dag.IPFilterRule) *envoy_rbac_v3.RBACPerRoute { + action := envoy_config_rbac_v3.RBAC_ALLOW + if !allow { + action = envoy_config_rbac_v3.RBAC_DENY + } + + principals := make([]*envoy_config_rbac_v3.Principal, 0, len(rules)) + + for _, f := range rules { + var principal *envoy_config_rbac_v3.Principal + + prefixLen, _ := f.CIDR.Mask.Size() + cidr := &envoy_core_v3.CidrRange{ + AddressPrefix: f.CIDR.IP.String(), + PrefixLen: wrapperspb.UInt32(uint32(prefixLen)), + } + + if f.Remote { + principal = &envoy_config_rbac_v3.Principal{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: cidr, + }, + } + } else { + principal = &envoy_config_rbac_v3.Principal{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: cidr, + }, + } + } + // Note that `source_ip` is not supported: `source_ip` respects + // PROXY, but not X-Forwarded-For. + principals = append(principals, principal) + } + + return &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: action, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: principals, + }, + }, + }, + }, + } +} + // RouteMatch creates a *envoy_route_v3.RouteMatch for the supplied *dag.Route. func RouteMatch(route *dag.Route) *envoy_route_v3.RouteMatch { routeMatch := PathRouteMatch(route.PathMatchCondition) diff --git a/internal/envoy/v3/route_test.go b/internal/envoy/v3/route_test.go index 38c41177db9..d9ce5b193d5 100644 --- a/internal/envoy/v3/route_test.go +++ b/internal/envoy/v3/route_test.go @@ -14,12 +14,15 @@ package v3 import ( + "net" "testing" "time" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" + envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" envoy_internal_redirect_previous_routes_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/previous_routes/v3" envoy_internal_redirect_safe_cross_scheme_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/safe_cross_scheme/v3" matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" @@ -1387,6 +1390,445 @@ func TestCORSPolicy(t *testing.T) { } } +func TestIPFilters(t *testing.T) { + tests := map[string]struct { + ipRules []dag.IPFilterRule + allow bool + want *envoy_rbac_v3.RBACPerRoute + }{ + "allow remote ipv4": { + ipRules: []dag.IPFilterRule{ + { + Remote: true, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: true, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "deny remote ipv4": { + ipRules: []dag.IPFilterRule{ + { + Remote: true, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: false, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "allow remote ipv6": { + ipRules: []dag.IPFilterRule{ + { + Remote: true, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + }, + allow: true, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "deny remote ipv6": { + ipRules: []dag.IPFilterRule{ + { + Remote: true, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + }, + allow: false, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "allow local ipv4": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: true, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "deny local ipv4": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: false, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "allow local ipv6": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + }, + allow: true, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "deny local ipv6": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + }, + allow: false, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }, + }, + }, + "allow multiple rules": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db6::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + { + Remote: true, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: true, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{ + { + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + { + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db6::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + { + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "deny multiple rules": { + ipRules: []dag.IPFilterRule{ + { + Remote: false, + CIDR: net.IPNet{ + IP: net.ParseIP("2001:db8::68"), + Mask: net.CIDRMask(24, 128), + }, + }, + { + Remote: true, + CIDR: net.IPNet{ + IP: net.IPv4(192, 168, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + { + Remote: true, + CIDR: net.IPNet{ + IP: net.IPv4(192, 165, 0, 0), + Mask: net.CIDRMask(24, 32), + }, + }, + }, + allow: false, + want: &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{ + { + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + { + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.168.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + { + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &envoy_core_v3.CidrRange{ + AddressPrefix: "192.165.0.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := ipFilterConfig(tc.allow, tc.ipRules) + protobuf.ExpectEqual(t, tc.want, got) + }) + } +} + func TestUpgradeHTTPS(t *testing.T) { got := UpgradeHTTPS() want := &envoy_route_v3.Route_Redirect{ diff --git a/internal/featuretests/v3/ipfilter_test.go b/internal/featuretests/v3/ipfilter_test.go new file mode 100644 index 00000000000..6522802af1b --- /dev/null +++ b/internal/featuretests/v3/ipfilter_test.go @@ -0,0 +1,223 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3 + +import ( + "testing" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" + envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/fixture" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/wrapperspb" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestIPFilterPolicy(t *testing.T) { + rh, c, done := setup(t) + defer done() + + s1 := fixture.NewService("backend"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}) + rh.OnAdd(s1) + + hp1 := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vhfilter", + Namespace: s1.Namespace, + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "test1.test.com", + IPAllowFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "8.8.8.8/24", + }}, + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: s1.Name, + Port: 80, + }}, + }}, + }, + } + rh.OnAdd(hp1) + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + envoy_v3.RouteConfiguration("ingress_http", virtualHostWithFilters(envoy_v3.VirtualHost(hp1.Spec.VirtualHost.Fqdn, + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeCluster("default/backend/80/da39a3ee5e"), + }, + ), withFilterConfig("envoy.filters.http.rbac", &envoy_rbac_v3.RBACPerRoute{Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &corev3.CidrRange{ + AddressPrefix: "8.8.8.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }}), + )), + ), + TypeUrl: routeType, + }) + + hp2 := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vhfilter", + Namespace: s1.Namespace, + ResourceVersion: "2", + Generation: 2, + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "test1.test.com", + IPAllowFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourceRemote, + CIDR: "8.8.8.8/24", + }}, + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: s1.Name, + Port: 80, + }}, + IPDenyFilterPolicy: []contour_api_v1.IPFilterPolicy{{ + Source: contour_api_v1.IPFilterSourcePeer, + CIDR: "2001:db8::68", + }}, + }}, + }, + } + rh.OnUpdate(hp1, hp2) + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + envoy_v3.RouteConfiguration("ingress_http", virtualHostWithFilters(envoy_v3.VirtualHost(hp1.Spec.VirtualHost.Fqdn, + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeCluster("default/backend/80/da39a3ee5e"), + TypedPerFilterConfig: withFilterConfig("envoy.filters.http.rbac", &envoy_rbac_v3.RBACPerRoute{ + Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_DENY, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_DirectRemoteIp{ + DirectRemoteIp: &corev3.CidrRange{ + AddressPrefix: "2001:db8::68", + PrefixLen: wrapperspb.UInt32(128), + }, + }, + }}, + }, + }, + }, + }, + }), + }, + ), withFilterConfig("envoy.filters.http.rbac", &envoy_rbac_v3.RBACPerRoute{Rbac: &envoy_rbac_v3.RBAC{ + Rules: &envoy_config_rbac_v3.RBAC{ + Action: envoy_config_rbac_v3.RBAC_ALLOW, + Policies: map[string]*envoy_config_rbac_v3.Policy{ + "ip-rules": { + Permissions: []*envoy_config_rbac_v3.Permission{ + { + Rule: &envoy_config_rbac_v3.Permission_Any{Any: true}, + }, + }, + Principals: []*envoy_config_rbac_v3.Principal{{ + Identifier: &envoy_config_rbac_v3.Principal_RemoteIp{ + RemoteIp: &corev3.CidrRange{ + AddressPrefix: "8.8.8.0", + PrefixLen: wrapperspb.UInt32(24), + }, + }, + }}, + }, + }, + }, + }}), + )), + ), + TypeUrl: routeType, + }) + + hp3 := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vhfilter", + Namespace: s1.Namespace, + ResourceVersion: "3", + Generation: 3, + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "test1.test.com", + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: s1.Name, + Port: 80, + }}, + }}, + }, + } + rh.OnUpdate(hp2, hp3) + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + envoy_v3.RouteConfiguration("ingress_http", envoy_v3.VirtualHost(hp1.Spec.VirtualHost.Fqdn, + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeCluster("default/backend/80/da39a3ee5e"), + }, + ))), + TypeUrl: routeType, + }) + rh.OnDelete(hp3) +} + +func virtualHostWithFilters(vh *envoy_route_v3.VirtualHost, typedPerFilterConfig map[string]*anypb.Any) *envoy_route_v3.VirtualHost { + vh.TypedPerFilterConfig = typedPerFilterConfig + return vh +} diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 14e8169f9af..c0ad47f5981 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -2013,6 +2013,79 @@

HeadersPolicy +

IPFilterPolicy +

+

+(Appears on: +Route, +VirtualHost) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+source +
+ + +IPFilterSource + + +
+

Source indicates how to determine the ip address to filter on, and can be +one of two values: +- Remote filters on the ip address of the client, accounting for PROXY and +X-Forwarded-For as needed. +- Peer filters on the ip of the network request, ignoring PROXY and +X-Forwarded-For.

+
+cidr +
+ +string + +
+

CIDR is a CIDR block of ipv4 or ipv6 addresses to filter on. This can also be +a bare IP address (without a mask) to filter on exactly one address.

+
+

IPFilterSource +(string alias)

+

+(Appears on: +IPFilterPolicy) +

+

+

IPFilterSource indicates which IP should be considered for filtering

+

+ + + + + + + + + + + + +
ValueDescription

"Peer"

"Remote"

Include

@@ -3586,6 +3659,40 @@

Route

The policy for verifying JWTs for requests to this route.

+ + +ipAllowPolicy +
+ + +[]IPFilterPolicy + + + + +

IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be allowed. All other requests will be denied. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here override any rules set on the root HTTPProxy.

+ + + + +ipDenyPolicy +
+ + +[]IPFilterPolicy + + + + +

IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be denied. All other requests will be allowed. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here override any rules set on the root HTTPProxy.

+ +

Service @@ -4530,6 +4637,40 @@

VirtualHost

Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host.

+ + +ipAllowPolicy +
+ + +[]IPFilterPolicy + + + + +

IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be allowed. All other requests will be denied. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here may be overridden in a Route.

+ + + + +ipDenyPolicy +
+ + +[]IPFilterPolicy + + + + +

IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be denied. All other requests will be allowed. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here may be overridden in a Route.

+ +
diff --git a/site/content/docs/main/config/ip-filtering.md b/site/content/docs/main/config/ip-filtering.md new file mode 100644 index 00000000000..161d39bc228 --- /dev/null +++ b/site/content/docs/main/config/ip-filtering.md @@ -0,0 +1,80 @@ +# IP Filtering + +Contour supports filtering requests based on the incoming ip address using Envoy's [RBAC Filter][1]. + +Requests can be either allowed or denied based on a CIDR range specified on the virtual host and/or individual routes. + +If the request's IP address is allowed, the request will be proxied to the appropriate upstream. +If the request's IP address is denied, an HTTP 403 (Forbidden) will be returned to the client. + +## Specifying Rules + +Rules are specified with the `ipAllowPolicy` and `ipDenyPolicy` fields on `virtualhost` and `route`: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: basic +spec: + virtualhost: + fqdn: foo-basic.bar.com + ipAllowPolicy: + # traffic is allowed if it came from localhost (i.e. co-located reverse proxy) + - cidr: 127.0.0.1/32 + source: Peer + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + # route-level ip filters override the virtualhost-level filters + ipAllowPolicy: + # traffic is allowed if it came from localhost (i.e. co-located reverse proxy) + - cidr: 127.0.0.1/32 + source: Peer + # and the request originated from an IP in this range + - cidr: 99.99.0.0/16 + source: Remote +``` + +### Specifying CIDR Ranges + +CIDR ranges may be ipv4 or ipv6. Bare IP addresses are interpreted as the CIDR range containing that one ip address only. + +Examples: +- `1.1.1.1/24` +- `127.0.0.1` +- `2001:db8::68/24` +- `2001:db8::68` + +### Allow vs Deny + +Filters are specified as either allow or deny: + +- `ipAllowPolicy` only allows requests that match the ip filters. +- `ipDenyPolicy` denies all requests unless they match the ip filters. + +Allow and deny policies cannot both be specified at the same time for a virtual host or route. + +### IP Source + +The `source` field controls how the ip address is selected from the request for filtering. + +- `source: Peer` filter rules will filter using Envoy's [direct_remote_ip][2], which is always the physical peer. +- `source: Remote` filter rules will filter using Envoy's [remote_ip][3], which may be inferred from the X-Forwarded-For header or proxy protocol. + +If using `source: Remote` with `X-Forwarded-For`, it may be necessary to configure Contour's `numTrustedHops` in [Network Parameters][4]. + +### Virtual Host and Route Filter Precedence + +IP filters on the virtual host apply to all routes included in the virtual host, unless the route specifies its own rules. + +Rules specified on a route override any rules defined on the virtual host, they are not additive. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rbac_filter.html +[2]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#envoy-v3-api-field-config-rbac-v3-principal-direct-remote-ip +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#envoy-v3-api-field-config-rbac-v3-principal-remote-ip +[4]: api/#projectcontour.io/v1.NetworkParameters + diff --git a/site/data/docs/main-toc.yml b/site/data/docs/main-toc.yml index f41a377bc90..7f05edae68d 100644 --- a/site/data/docs/main-toc.yml +++ b/site/data/docs/main-toc.yml @@ -45,6 +45,8 @@ toc: url: /config/overload-manager - page: JWT Verification url: /config/jwt-verification + - page: IP Filtering + url: /config/ip-filtering - page: Annotations Reference url: /config/annotations - page: Slow Start Mode diff --git a/test/e2e/httpproxy/httpproxy_test.go b/test/e2e/httpproxy/httpproxy_test.go index 3b8827bb527..449742c6b5f 100644 --- a/test/e2e/httpproxy/httpproxy_test.go +++ b/test/e2e/httpproxy/httpproxy_test.go @@ -282,6 +282,18 @@ var _ = Describe("HTTPProxy", func() { f.NamespacedTest("httpproxy-host-header-rewrite", testHostHeaderRewrite) + f.NamespacedTest("httpproxy-ip-filters", func(namespace string) { + // ip filter tests rely on the ability to forge x-forwarded-for + Context("with trusted xff hops", func() { + BeforeEach(func() { + contourConfig.Network.XffNumTrustedHops = 1 + contourConfiguration.Spec.Envoy.Network.XffNumTrustedHops = ref.To(uint32(1)) + }) + + testIPFilterPolicy(namespace) + }) + }) + f.NamespacedTest("httpproxy-multiple-ingress-classes-field", func(namespace string) { Context("with more than one ingress ClassName set", func() { BeforeEach(func() { diff --git a/test/e2e/httpproxy/ip_filtering_test.go b/test/e2e/httpproxy/ip_filtering_test.go new file mode 100644 index 00000000000..4e0db6fbd99 --- /dev/null +++ b/test/e2e/httpproxy/ip_filtering_test.go @@ -0,0 +1,399 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e +// +build e2e + +package httpproxy + +import ( + "context" + "net/http" + + . "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" + "github.com/projectcontour/contour/test/e2e" +) + +func testIPFilterPolicy(namespace string) { + Specify("requests can be filtered by ip address", func() { + t := f.T() + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + + f.Fixtures.Echo.Deploy(namespace, "echo") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ipfilter1", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "ipfilter1.projectcontour.io", + }, + Routes: []contourv1.Route{ + { + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + p, _ = f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + // Wait until we get a 200 from the proxy confirming + // the pods are up and serving traffic. + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // Deny all ips so that the next request fails + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.Routes[0].IPDenyFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourcePeer, + CIDR: "10.8.8.8/0", + }, + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.8.8.8/0", + }, + } + + return f.Client.Update(ctx, p) + })) + + // Make a request against the proxy, it should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + + // Only allow requests from 10.10.10.10 + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.Routes[0].IPAllowFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.10.10.10/32", + }, + } + p.Spec.Routes[0].IPDenyFilterPolicy = nil + + return f.Client.Update(ctx, p) + })) + + // Add an X-Forwarded-For header to match the allowed ip, it should succeed + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.10"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // A request with the wrong ip should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.0"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + }) + + Specify("per-route ip filters override virtualhost ipfilters", func() { + t := f.T() + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + + f.Fixtures.Echo.Deploy(namespace, "echo") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ipfilter2", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "ipfilter2.projectcontour.io", + }, + Routes: []contourv1.Route{ + { + Conditions: []contourv1.MatchCondition{{ + Prefix: "/one", + }}, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Conditions: []contourv1.MatchCondition{{ + Prefix: "/other", + }}, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + p, _ = f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + // Wait until we get a 200 from the proxy confirming + // the pods are up and serving traffic. + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/one", + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // Deny all ips so that the next request fails + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.VirtualHost.IPDenyFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourcePeer, + CIDR: "10.8.8.8/0", + }, + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.8.8.8/0", + }, + } + + return f.Client.Update(ctx, p) + })) + + // Make a request against the proxy, it should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/one", + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + + // Allow requests from 10.10.10.10 on the route + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.Routes[0].IPAllowFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.10.10.10", + }, + } + p.Spec.Routes[0].IPDenyFilterPolicy = nil + + return f.Client.Update(ctx, p) + })) + + // Add an X-Forwarded-For header to match the allowed ip, it should succeed + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/one", + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.10"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // A request with the wrong ip should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/one", + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.0"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + + // A request against the other route should fail (virtualhost-level filter applies) + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Path: "/other", + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.0"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + }) + + Specify("requests can be filtered by ip address in included routes", func() { + t := f.T() + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + + f.Fixtures.Echo.Deploy(namespace, "echo") + + r := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ipfilter3-root", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "ipfilter3.projectcontour.io", + }, + Includes: []contourv1.Include{{ + Namespace: namespace, + Name: "ipfilter3-child", + }}, + }, + } + // root will be missing an include when created + r, _ = f.CreateHTTPProxyAndWaitFor(r, e2e.HTTPProxyInvalid) + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ipfilter3-child", + }, + Spec: contourv1.HTTPProxySpec{ + Routes: []contourv1.Route{ + { + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + p, _ = f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + // Wait until we get a 200 from the proxy confirming + // the pods are up and serving traffic. + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: r.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // Deny all ips so that the next request fails + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.Routes[0].IPDenyFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourcePeer, + CIDR: "10.8.8.8/0", + }, + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.8.8.8/0", + }, + } + + return f.Client.Update(ctx, p) + })) + + // Make a request against the proxy, it should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: r.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + + // Only allow requests from 10.10.10.10 + require.NoError(t, retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(p), p); err != nil { + return err + } + + p.Spec.Routes[0].IPAllowFilterPolicy = []contourv1.IPFilterPolicy{ + { + Source: contourv1.IPFilterSourceRemote, + CIDR: "10.10.10.10/32", + }, + } + p.Spec.Routes[0].IPDenyFilterPolicy = nil + + return f.Client.Update(ctx, p) + })) + + // Add an X-Forwarded-For header to match the allowed ip, it should succeed + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: r.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.10"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // A request with the wrong ip should fail + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: r.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(403), + RequestOpts: []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{"X-Forwarded-For": "10.10.10.0"}), + }, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 403 response code, got %d", res.StatusCode) + }) +}