diff --git a/Makefile b/Makefile index 0aea955a405..172418ae58b 100644 --- a/Makefile +++ b/Makefile @@ -322,6 +322,7 @@ integration: ## Run integration tests against a real k8s cluster ./_integration/testsuite/install-service-apis.sh ./_integration/testsuite/install-contour-working.sh ./_integration/testsuite/install-fallback-certificate.sh + ./_integration/testsuite/install-ratelimit-service.sh ./_integration/testsuite/run-test-case.sh ./_integration/testsuite/httpproxy/*.yaml ./_integration/testsuite/cleanup.sh diff --git a/_integration/testsuite/httpproxy/019-local-rate-limiting.yaml b/_integration/testsuite/httpproxy/019-local-rate-limiting.yaml index 62cb5519f4e..a05cb541817 100644 --- a/_integration/testsuite/httpproxy/019-local-rate-limiting.yaml +++ b/_integration/testsuite/httpproxy/019-local-rate-limiting.yaml @@ -37,6 +37,21 @@ $apply: --- +# Wait for the service to have endpoints before trying to make +# a request. + +import data.contour.resources + +error_endpoints_not_ready[msg] { + ep := resources.get("endpoints", "echo") + + not ep.subsets[0].addresses + + msg := "endpoints for svc/ingress-conformance-echo are not ready" +} + +--- + # This proxy has a local rate limit on the virtual host. apiVersion: projectcontour.io/v1 kind: HTTPProxy @@ -55,21 +70,6 @@ spec: port: 80 --- -# Wait for the service to have endpoints before trying to make -# a request. - -import data.contour.resources - -error_endpoints_not_ready[msg] { - ep := resources.get("endpoints", "echo") - - not ep.subsets[0].addresses - - msg := "endpoints for svc/ingress-conformance-echo are not ready" -} - ---- - # Make a request against the proxy, confirm a 200 response # is returned. diff --git a/_integration/testsuite/httpproxy/020-global-rate-limiting.yaml b/_integration/testsuite/httpproxy/020-global-rate-limiting.yaml new file mode 100644 index 00000000000..8df2489665d --- /dev/null +++ b/_integration/testsuite/httpproxy/020-global-rate-limiting.yaml @@ -0,0 +1,203 @@ +# 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. + +# This check depends on the `--watch=endpoints` argument being given +# to integration-tester. + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-conformance-echo +$apply: + fixture: + as: echo + +--- + +apiVersion: v1 +kind: Service +metadata: + name: ingress-conformance-echo +$apply: + fixture: + as: echo + +--- + +# Wait for the service to have endpoints. + +import data.contour.resources + +error_endpoints_not_ready[msg] { + ep := resources.get("endpoints", "echo") + + not ep.subsets[0].addresses + + msg := "endpoints for svc/ingress-conformance-echo are not ready" +} + +--- + +# This proxy has a global rate limit on the virtual host. +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: vhostratelimit +spec: + virtualhost: + fqdn: vhostratelimit.projectcontour.io + rateLimitPolicy: + global: + descriptors: + - entries: + - genericKey: + value: vhostlimit + routes: + - services: + - name: echo + port: 80 +--- + +# Make a request against the proxy, confirm a 200 response +# is returned. + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +Response := client.Get({ + "url": url.http("/"), + "headers": { + "Host": "vhostratelimit.projectcontour.io", + "User-Agent": client.ua("global-rate-limit"), + }, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 200) +} + +--- + +# Make another request against the proxy, confirm a 429 +# response is now gotten since we've exceeded the rate +# limit. + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +Response := client.Get({ + "url": url.http("/"), + "headers": { + "Host": "vhostratelimit.projectcontour.io", + "User-Agent": client.ua("global-rate-limit"), + }, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 429) +} + +--- + +# This proxy has a global rate limit on a route. +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: routeratelimit +spec: + virtualhost: + fqdn: routeratelimit.projectcontour.io + routes: + - services: + - name: echo + port: 80 + rateLimitPolicy: + global: + descriptors: + - entries: + - genericKey: + value: routelimit + - conditions: + - prefix: /unlimited + services: + - name: echo + port: 80 +--- + +# Make a request against the proxy, confirm a 200 response +# is returned. + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +Response := client.Get({ + "url": url.http("/"), + "headers": { + "Host": "routeratelimit.projectcontour.io", + "User-Agent": client.ua("global-rate-limit"), + }, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 200) +} + +--- + +# Make another request against the proxy, confirm a 429 +# response is now gotten since we've exceeded the rate +# limit. + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +Response := client.Get({ + "url": url.http("/"), + "headers": { + "Host": "routeratelimit.projectcontour.io", + "User-Agent": client.ua("global-rate-limit"), + }, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 429) +} + +--- + +# Make a request against the route that doesn't have +# rate limiting to confirm we still get a 200 for that +# route. + +import data.contour.http.client +import data.contour.http.client.url +import data.contour.http.expect + +Response := client.Get({ + "url": url.http("/unlimited"), + "headers": { + "Host": "routeratelimit.projectcontour.io", + "User-Agent": client.ua("global-rate-limit"), + }, +}) + +check_for_status_code [msg] { + msg := expect.response_status_is(Response, 200) +} diff --git a/_integration/testsuite/install-ratelimit-service.sh b/_integration/testsuite/install-ratelimit-service.sh new file mode 100755 index 00000000000..a14a43e2641 --- /dev/null +++ b/_integration/testsuite/install-ratelimit-service.sh @@ -0,0 +1,90 @@ +#! /usr/bin/env bash + +# 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. + +# install-ratelimit-service.sh: Install a rate limit service and configuration +# for Contour. + +set -o pipefail +set -o errexit +set -o nounset + +readonly KIND=${KIND:-kind} +readonly KUBECTL=${KUBECTL:-kubectl} + +readonly WAITTIME=${WAITTIME:-5m} + +readonly HERE=$(cd $(dirname $0) && pwd) +readonly REPO=$(cd ${HERE}/../.. && pwd) + +# Define some rate limiting policies to correspond to +# testsuite/httpproxy/020-global-rate-limiting.yaml. +${KUBECTL} apply -f - </. + ExtensionService string `yaml:"extensionService,omitempty"` + + // Domain is passed to the Rate Limit Service. + Domain string `yaml:"domain,omitempty"` + + // FailOpen defines whether to allow requests to proceed when the + // Rate Limit Service fails to respond properly. + FailOpen bool `yaml:"failOpen,omitempty"` } // Validate verifies that the parameter values do not have any syntax errors. diff --git a/site/_guides/global-rate-limiting.md b/site/_guides/global-rate-limiting.md new file mode 100644 index 00000000000..7a9f08f0648 --- /dev/null +++ b/site/_guides/global-rate-limiting.md @@ -0,0 +1,435 @@ +--- +title: Global Rate Limiting +layout: page +--- + +Starting in version 1.13, Contour supports [Envoy global rate limiting][1]. +In global rate limiting, Envoy communicates with an external Rate Limit Service (RLS) over gRPC to make rate limit decisions for each request. +Envoy is configured to produce 1+ descriptors for incoming requests, containing things like the client IP, header values, and more. +Envoy sends descriptors to the RLS, and the RLS returns a rate limiting decision to Envoy based on the descriptors and the RLS's configured rate limits. + +In this guide, we'll walk through deploying an RLS, configuring it in Contour, and configuring an `HTTPProxy` to use it for rate limiting. + +**NOTE: you should not consider the RLS deployment in this guide to be production-ready.** +The instructions and example YAML below are intended to be a demonstration of functionality only. +Each user will have their own unique production requirements for their RLS deployment. + +## Prerequisites + +This guide assumes that you have: + +- A local KinD cluster created using [the Contour guide][2]. +- Contour installed and running in the cluster using the [quick start][3]. + +## Deploy an RLS + +For this guide, we'll deploy the [Envoy rate limit service][4] as our RLS. +However, any service that implements the [RateLimitService gRPC interface][5] is supported by Contour/Envoy. + +Create a config map with [the ratelimit service configuration][6]: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: ratelimit-config + namespace: projectcontour +data: + ratelimit-config.yaml: | + domain: contour + descriptors: + + # requests with a descriptor of ["generic_key": "foo"] + # are limited to one per minute. + - key: generic_key + value: foo + rate_limit: + unit: minute + requests_per_unit: 1 + + # each unique remote address (i.e. client IP) + # is limited to three requests per minute. + - key: remote_address + rate_limit: + unit: minute + requests_per_unit: 3 +``` + +Create a deployment for the RLS that mounts the config map as a volume. +**This configuration is for demonstration purposes only and is not a production-ready deployment.** +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: ratelimit + name: ratelimit + namespace: projectcontour +spec: + replicas: 1 + selector: + matchLabels: + app: ratelimit + template: + metadata: + labels: + app: ratelimit + spec: + containers: + - name: redis + image: redis:alpine + env: + - name: REDIS_SOCKET_TYPE + value: tcp + - name: REDIS_URL + value: redis:6379 + - name: ratelimit + image: docker.io/envoyproxy/ratelimit:6f5de117 + ports: + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8081 + name: grpc + protocol: TCP + volumeMounts: + - name: ratelimit-config + mountPath: /data/ratelimit/config + readOnly: true + env: + - name: USE_STATSD + value: "false" + - name: LOG_LEVEL + value: debug + - name: REDIS_SOCKET_TYPE + value: tcp + - name: REDIS_URL + value: localhost:6379 + - name: RUNTIME_ROOT + value: /data + - name: RUNTIME_SUBDIRECTORY + value: ratelimit + - name: RUNTIME_WATCH_ROOT + value: "false" + # need to set RUNTIME_IGNOREDOTFILES to true to avoid issues with + # how Kubernetes mounts configmaps into pods. + - name: RUNTIME_IGNOREDOTFILES + value: "true" + command: ["/bin/ratelimit"] + livenessProbe: + httpGet: + path: /healthcheck + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: ratelimit-config + configMap: + name: ratelimit-config +``` + +Create a service: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: ratelimit + namespace: projectcontour +spec: + ports: + - port: 8081 + name: grpc + protocol: TCP + selector: + app: ratelimit + type: ClusterIP +``` + +Check the progress of the deployment: + +```bash +$ kubectl -n projectcontour get pods -l app=ratelimit +NAME READY STATUS RESTARTS AGE +ratelimit-658f4b8f6b-2hnrf 2/2 Running 0 12s +``` + +Once the pod is `Running` with `2/2` containers ready, move onto the next step. + +## Configure the RLS with Contour + +Create a Contour extension service for the RLS: + +```yaml +apiVersion: projectcontour.io/v1alpha1 +kind: ExtensionService +metadata: + namespace: projectcontour + name: ratelimit +spec: + protocol: h2c + # The service name and port correspond to + # the service we created in the previous + # step. + services: + - name: ratelimit + port: 8081 + timeoutPolicy: + response: 100ms +``` + +Update the Contour config map to have the following RLS configuration: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: contour + namespace: projectcontour +data: + contour.yaml: | + rateLimitService: + # extensionService is the / + # of the ExtensionService we created in the + # previous step. + extensionService: projectcontour/ratelimit + # domain corresponds to the domain in the + # projectcontour/ratelimit-config config map. + domain: contour + # failOpen is whether to allow requests through + # if there's an error connecting to the RLS. + failOpen: false +``` + +## Deploy a sample app + +To demonstrate how to use global rate limiting in a `HTTPProxy` resource, we first need to deploy a simple echo application. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-conformance-echo +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ingress-conformance-echo + template: + metadata: + labels: + app.kubernetes.io/name: ingress-conformance-echo + spec: + containers: + - name: conformance-echo + image: agervais/ingress-conformance-echo:latest + ports: + - name: http-api + containerPort: 3000 + readinessProbe: + httpGet: + path: /health + port: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-conformance-echo +spec: + ports: + - name: http + port: 80 + targetPort: http-api + selector: + app.kubernetes.io/name: ingress-conformance-echo +``` + +This echo server will respond with a JSON object that reports information about the HTTP request it received, including the request headers. + +```bash +$ kubectl apply -f echo.yaml +deployment.apps/ingress-conformance-echo created +service/ingress-conformance-echo created +``` + +Once the application is running, we can expose it to Contour with a `HTTPProxy` resource. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 + - conditions: + - prefix: /foo + services: + - name: ingress-conformance-echo + port: 80 + +``` + +```bash +$ kubectl apply -f echo-proxy.yaml +httpproxy.projectcontour.io/echo created +$ kubectl get proxies echo +NAME FQDN TLS SECRET STATUS STATUS DESCRIPTION +echo local.projectcontour.io valid valid HTTPProxy +``` + +We can verify that the application is working by requesting any path: + +```bash +$ curl -k http://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/12707","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Content-Length":["0"],"User-Agent":["curl/7.64.1"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["7b87d5d1-8ee8-40e3-81ac-7d74dfd4d50b"],"X-Request-Start":["t=1601596511.489"]}} +``` + +## Add global rate limit policies + +Now that we have a working application exposed by a `HTTPProxy` resource, we can add add global rate limiting to it. + +Edit the `HTTPProxy` that we created in the previous step to add rate limit policies to both routes: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 + rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} + - conditions: + - prefix: /foo + services: + - name: ingress-conformance-echo + port: 80 + rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} + - entries: + - genericKey: + value: foo +``` + +## Make requests + +Before making requests to our `HTTPProxy`, let's quickly revisit the `ratelimit-config` config map. +Here's what we defined: + +```yaml +... +descriptors: + # requests with a descriptor of ["generic_key": "foo"] + # are limited to one per minute. + - key: generic_key + value: foo + rate_limit: + unit: minute + requests_per_unit: 1 + + # each unique remote address (i.e. client IP) + # is limited to three total requests per minute. + - key: remote_address + rate_limit: + unit: minute + requests_per_unit: 3 +``` + +The first entry says that requests with a descriptor of `["generic_key": "foo"]` should be limited to one per minute. +The second entry says that each unique remote address (client IP) should be allowed three total requests per minute. +All relevant rate limits are applied for each request, and requests that result in a `429 (Too Many Requests)` count against limits. + +So, we should be able to make: +- a first request to `local.projectcontour.io/foo` that get a `200 (OK)` response +- a second request to `local.projectcontour.io/foo` that gets a `429 (Too Many Requests)` response (due to the first rate limit) +- a third request to `local.projectcontour.io/bar`that gets a `200 (OK)` response +- a fourth request to `local.projectcontour.io/bar`that gets a `429 (Too Many Requests)` response (due to the second rate limit) + +Let's try it out: + +Request #1: +``` +$ curl -I local.projectcontour.io/foo + +HTTP/1.1 200 OK +content-type: application/json +date: Mon, 08 Feb 2021 22:25:06 GMT +content-length: 403 +x-envoy-upstream-service-time: 4 +vary: Accept-Encoding +server: envoy +``` + +Request #2: + +``` +$ curl -I local.projectcontour.io/foo + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 08 Feb 2021 22:59:10 GMT +server: envoy +transfer-encoding: chunked +``` + +Request #3: + +``` +$ curl -I local.projectcontour.io/bar + +HTTP/1.1 200 OK +content-type: application/json +date: Mon, 08 Feb 2021 22:59:54 GMT +content-length: 404 +x-envoy-upstream-service-time: 2 +vary: Accept-Encoding +server: envoy +``` + +Request #4: + +``` +$ curl -I local.projectcontour.io/bar + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 08 Feb 2021 23:00:28 GMT +server: envoy +transfer-encoding: chunked +``` + +## Wrapping up + +For more information, see the [Contour rate limiting documentation][7] and the [API reference documentation][8]. + +The YAML used in this guide is available [in the Contour repository][9]. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[2]: /docs/{{site.latest}}/deploy-options/#kind +[3]: https://projectcontour.io/getting-started/#option-1-quickstart +[4]: https://github.com/envoyproxy/ratelimit +[5]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ratelimit/v3/rls.proto +[6]: https://github.com/envoyproxy/ratelimit#configuration +[7]: /docs/{{site.latest}}/config/rate-limiting/ +[8]: /docs/{{site.latest}}/config/api/ +[9]: {{site.github.repository_url}}/tree/main/examples/ratelimit diff --git a/site/docs/main/config/api-reference.html b/site/docs/main/config/api-reference.html index 3119d3e2236..dceb1158710 100644 --- a/site/docs/main/config/api-reference.html +++ b/site/docs/main/config/api-reference.html @@ -9,7 +9,7 @@

projectcontour.io/v1

-

This package holds the specification for the projectcontour.io Custom Resource Definitions (CRDs).

+

Package v1 holds the specification for the projectcontour.io Custom Resource Definitions (CRDs).

In building this CRD, we’ve inadvertently overloaded the word “Condition”, so we’ve tried to make this spec clear as to which types of condition are which.

MatchConditions are used by Routes and Includes to specify rules to match requests against for either @@ -742,6 +742,87 @@

ExtensionServiceReferenc +

GenericKeyDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

GenericKeyDescriptor defines a descriptor entry with a static key and +value.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+key +
+ +string + +
+(Optional) +

Key defines the key of the descriptor entry. If not set, the +key is set to “generic_key”.

+
+value +
+ +string + +
+

Value defines the value of the descriptor entry.

+
+

GlobalRateLimitPolicy +

+

+(Appears on: +RateLimitPolicy) +

+

+

GlobalRateLimitPolicy defines global rate limiting parameters.

+

+ + + + + + + + + + + + + +
FieldDescription
+descriptors +
+ + +[]RateLimitDescriptor + + +
+

Descriptors defines the list of descriptors that will +be generated and sent to the rate limit service. Each +descriptor contains 1+ key-value pair entries.

+

HTTPHealthCheckPolicy

@@ -1536,6 +1617,107 @@

PathRewritePolicy +

RateLimitDescriptor +

+

+(Appears on: +GlobalRateLimitPolicy) +

+

+

RateLimitDescriptor defines a list of key-value pair generators.

+

+ + + + + + + + + + + + + +
FieldDescription
+entries +
+ + +[]RateLimitDescriptorEntry + + +
+

Entries is the list of key-value pair generators.

+
+

RateLimitDescriptorEntry +

+

+(Appears on: +RateLimitDescriptor) +

+

+

RateLimitDescriptorEntry is a key-value pair generator. Exactly +one field on this struct must be non-nil.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+genericKey +
+ + +GenericKeyDescriptor + + +
+(Optional) +

GenericKey defines a descriptor entry with a static key and value.

+
+requestHeader +
+ + +RequestHeaderDescriptor + + +
+(Optional) +

RequestHeader defines a descriptor entry that’s populated only if +a given header is present on the request. The descriptor key is static, +and the descriptor value is equal to the value of the header.

+
+remoteAddress +
+ + +RemoteAddressDescriptor + + +
+(Optional) +

RemoteAddress defines a descriptor entry with a key of “remote_address” +and a value equal to the client’s IP address (from x-forwarded-for).

+

RateLimitPolicy

@@ -1565,13 +1747,42 @@

RateLimitPolicy +(Optional)

Local defines local rate limiting parameters, i.e. parameters for rate limiting that occurs within each Envoy pod as requests are handled.

+ + +global +
+ + +GlobalRateLimitPolicy + + + + +(Optional) +

Global defines global rate limiting parameters, i.e. parameters +defining descriptors that are sent to an external rate limit +service (RLS) for a rate limit decision on each request.

+ + +

RemoteAddressDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

RemoteAddressDescriptor defines a descriptor entry with a key of +“remote_address” and a value equal to the client’s IP address +(from x-forwarded-for).

+

ReplacePrefix

@@ -1676,6 +1887,51 @@

RequestHashPolicy +

RequestHeaderDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

RequestHeaderDescriptor defines a descriptor entry that’s populated only +if a given header is present on the request. The value of the descriptor +entry is equal to the value of the header (if present).

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+headerName +
+ +string + +
+

HeaderName defines the name of the header to look for on the request.

+
+descriptorKey +
+ +string + +
+

DescriptorKey defines the key to use on the descriptor entry.

+

RetryOn (string alias)

diff --git a/site/docs/main/config/rate-limiting.md b/site/docs/main/config/rate-limiting.md index dc325e200b7..0697965a7f0 100644 --- a/site/docs/main/config/rate-limiting.md +++ b/site/docs/main/config/rate-limiting.md @@ -1,17 +1,37 @@ # Rate Limiting +- [Overview](#overview) +- [Local Rate Limiting](#local-rate-limiting) +- [Global Rate Limiting](#global-rate-limiting) + +## Overview + +Rate limiting is a means of protecting backend services against unwanted traffic. +This can be useful for a variety of different scenarios: + +- Protecting against denial-of-service (DoS) attacks by malicious actors +- Protecting against DoS incidents due to bugs in client applications/services +- Enforcing usage quotas for different classes of clients, e.g. free vs. paid tiers +- Controlling resource consumption/cost + +Envoy supports two forms of HTTP rate limiting: **local** and **global**. + +In local rate limiting, rate limits are enforced by each Envoy instance, without any communication with other Envoys or any external service. + +In global rate limiting, an external rate limit service (RLS) is queried by each Envoy via gRPC for rate limit decisions. + +Contour supports both forms of Envoy's rate limiting. + ## Local Rate Limiting The `HTTPProxy` API supports defining local rate limit policies that can be applied to either individual routes or entire virtual hosts. Local rate limit policies define a maximum number of requests per unit of time that an Envoy should proxy to the upstream service. Requests beyond the defined limit will receive a `429 (Too Many Requests)` response by default. -Local rate limit policies program Envoy's [HTTP local rate limit filter](https://www.envoyproxy.io/docs/envoy/v1.17.0/configuration/http/http_filters/local_rate_limit_filter#config-http-filters-local-rate-limit). +Local rate limit policies program Envoy's [HTTP local rate limit filter][1]. It's important to note that local rate limit policies apply *per Envoy pod*. For example, a local rate limit policy of 100 requests per second for a given route will result in *each Envoy pod* allowing up to 100 requests per second for that route. -By contrast, **global** rate limiting (which will be added in a future Contour release), uses a shared external rate limit service, allowing rate limits to apply across *all* Envoy pods. - ### Defining a local rate limit Local rate limit policies can be defined for either routes or virtual hosts. A local rate limit policy requires a `requests` and a `units` field, defining the *number of requests per unit of time* that are allowed. `Requests` must be a positive integer, and `units` can be `second`, `minute`, or `hour`. Optionally, a `burst` parameter can also be provided, defining the number of requests above the baseline rate that are allowed in a short period of time. This would allow occasional larger bursts of traffic not to be rate limited. @@ -27,10 +47,10 @@ spec: virtualhost: fqdn: local.projectcontour.io rateLimitPolicy: - local: - requests: 100 - unit: hour - burst: 20 + local: + requests: 100 + unit: hour + burst: 20 routes: - conditions: - prefix: /s1 @@ -128,3 +148,118 @@ spec: - name: x-contour-ratelimited value: "true" ``` + +## Global Rate Limiting + +The `HTTPProxy` API also supports defining global rate limit policies on routes and virtual hosts. + +In order to use global rate limiting, you must first select and deploy an external rate limit service (RLS). +There is an [Envoy rate limit service implementation][2], but any service that implements the [RateLimitService gRPC interface][3] is supported. + +### Configuring an exernal RLS with Contour + +Once you have deployed your RLS, you must configure it with Contour. + +Define an extension service for it (substituting values as appropriate): +```yaml +apiVersion: projectcontour.io/v1alpha1 +kind: ExtensionService +metadata: + namespace: projectcontour + name: ratelimit +spec: + protocol: h2 + services: + - name: ratelimit + port: 8081 +``` + +Now add a reference to it in the Contour config file: +```yaml +rateLimitService: + extensionService: projectcontour/ratelimit + domain: contour + failOpen: true +``` + +### Defining a global rate limit policy + +Global rate limit policies can be defined for either routes or virtual hosts. Unlike local rate limit policies, global rate limit policies do not directly define a rate limit. Instead, they define a set of request descriptors that will be generated and sent to the external RLS for each request. The external RLS then makes the rate limit decision based on the descriptors and returns a response to Envoy. + +A global rate limit policy for the virtual host: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-vhost +spec: + virtualhost: + fqdn: local.projectcontour.io + rateLimitPolicy: + global: + descriptors: + # the first descriptor has a single key-value pair: + # [ remote_address= ]. + - entries: + - remoteAddress: {} + # the second descriptor has two key-value pairs: + # [ remote_address=, vhost=local.projectcontour.io ]. + - entries: + - remoteAddress: {} + - genericKey: + key: vhost + value: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +A global rate limit policy for the route: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-route +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + rateLimitPolicy: + global: + descriptors: + # the first descriptor has a single key-value pair: + # [ remote_address= ]. + - entries: + - remoteAddress: {} + # the second descriptor has two key-value pairs: + # [ remote_address=, prefix=/s1 ]. + - entries: + - remoteAddress: {} + - genericKey: + key: prefix + value: /s1 + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +[1]: https://www.envoyproxy.io/docs/envoy/v1.17.0/configuration/http/http_filters/local_rate_limit_filter#config-http-filters-local-rate-limit +[2]: https://github.com/envoyproxy/ratelimit +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ratelimit/v3/rls.proto