Skip to content

Tiered rate limits with variables #7884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ spec:
default:
description: sets the rate limit in this policy to be the
default if no conditions are met. In a group of policies
with the same JWT condition, only one policy can be the
default.
with the same condition, only one policy can be the default.
type: boolean
jwt:
description: defines a JWT condition to rate limit against.
Expand All @@ -207,6 +206,26 @@ spec:
- claim
- match
type: object
variables:
description: defines a Variables condition to rate limit against.
items:
description: VariableCondition defines a condition to rate
limit by a variable.
properties:
match:
description: the value of the variable to match against.
pattern: ^([^\s"'])*$
type: string
name:
description: the name of the variable to match against.
pattern: ^([^\s"'])*$
type: string
required:
- match
- name
type: object
maxItems: 1
type: array
type: object
delay:
type: integer
Expand Down
23 changes: 21 additions & 2 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,7 @@ spec:
default:
description: sets the rate limit in this policy to be the
default if no conditions are met. In a group of policies
with the same JWT condition, only one policy can be the
default.
with the same condition, only one policy can be the default.
type: boolean
jwt:
description: defines a JWT condition to rate limit against.
Expand All @@ -369,6 +368,26 @@ spec:
- claim
- match
type: object
variables:
description: defines a Variables condition to rate limit against.
items:
description: VariableCondition defines a condition to rate
limit by a variable.
properties:
match:
description: the value of the variable to match against.
pattern: ^([^\s"'])*$
type: string
name:
description: the name of the variable to match against.
pattern: ^([^\s"'])*$
type: string
required:
- match
- name
type: object
maxItems: 1
type: array
type: object
delay:
type: integer
Expand Down
158 changes: 158 additions & 0 deletions examples/custom-resources/rate-limit-tiered-apikey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Tiered Rate Limits with API Keys

In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and apply two rate
limit Policies, grouped in a tier, using the API Key client name as the key to the rate limit and using a regex of the client name to determine which rate limit Policy is applied. One rate limit policy will be the default ratelimit for the group.

> Note: This example makes use of the NGINX variables `$apikey_auth_token` & `apikey_client_name` which are made available by applying an API Key authentication Policy to your VirtualServer resource.

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
instructions to deploy NGINX Ingress Controller.
2. Save the public IP address of NGINX Ingress Controller into a shell variable:

```shell
IC_IP=XXX.YYY.ZZZ.III
```
<!-- markdownlint-disable MD029 -->
3. Save the HTTP port of NGINX Ingress Controller into a shell variable:
<!-- markdownlint-enable MD029 -->
```shell
IC_HTTP_PORT=<port number>
```

## Deploy a web application

Create the application deployments and services:

```shell
kubectl apply -f coffee.yaml
```

## Deploy the rate limit Policies

In this step, we create three Policies:

- `api-key-policy` which defines the API Key Policy
- `rate-limit-apikey-premium`, that allows 5 requests per second coming from a request containing an API Key with a client name that ends with `premium`
- `rate-limit-apikey-basic` that allows 1 request per second coming from a request containing an API Key with a client name that ends with `basic`

The `rate-limit-apikey-basic` Policy is also the default policy if the API Key client name does not match a tier.

Create the Policies:

```shell
kubectl apply -f api-key-policy.yaml
kubectl apply -f rate-limits.yaml
```

## Deploy the API key authentication Secret

Create a Secret of type `nginx.org/apikey` with the name `api-key-client-secret` that will be used for authorization on the server level.

This Secret will contain a mapping of client names to base64 encoded API Keys.

```shell
kubectl apply -f api-key-secret.yaml
```

## Configure load balancing

Create a VirtualServer resource for the web application:

```shell
kubectl apply -f cafe-virtual-server.yaml
```

Note that the VirtualServer references the policies `api-key-policy`, `rate-limit-apikey-premium` & `rate-limit-apikey-basic` created in Step 2.

## Test the premium configuration

Let's test the configuration. If you access the application with an API Key in an expected header at a rate that exceeds 5 requests per second, NGINX will
start rejecting your requests:

```shell
while true; do
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP -H "X-header-name: client1premium" http://cafe.example.com:$IC_HTTP_PORT/coffee;
sleep 0.1;
done
```

```text
Server address: 10.8.1.19:8080
Server name: coffee-dc88fc766-zr7f8

. . .

<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx/1.27.5</center>
</body>
</html>
```

> Note: The command result is truncated for the clarity of the example.

## Test the basic configuration

This test is similar to the previous step, however, this time we will be setting the API Key in the header to a value that maps to the `client1-basic` client name.

Let's test the configuration. If you access the application at a rate that exceeds 1 request per second, NGINX will
start rejecting your requests:

```shell
while true; do
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP -H "X-header-name: client1basic" http://cafe.example.com:$IC_HTTP_PORT/coffee;
sleep 0.5;
done
```

```text
Server address: 10.8.1.19:8080
Server name: coffee-dc88fc766-zr7f8

. . .

<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx/1.27.5</center>
</body>
</html>
```

> Note: The command result is truncated for the clarity of the example.

## Test the default configuration

This test is similar to the previous two steps, however, this time we will setting the API Key in the header to a value that maps to the `random` client name, which matches neither of the regex patterns configured in the Policies. However, we will still be seeing the default `rate-limit-apikey-basic` Policy applied.

Let's test the configuration. If you access the application at a rate that exceeds 1 request per second, NGINX will
start rejecting your requests:

```shell
while true; do
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP -H "X-header-name: random" http://cafe.example.com:$IC_HTTP_PORT/coffee;
sleep 0.5;
done
```

```text
Server address: 10.8.1.19:8080
Server name: coffee-dc88fc766-zr7f8

. . .

<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx/1.27.5</center>
</body>
</html>
```

> Note: The command result is truncated for the clarity of the example.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: api-key-policy
spec:
apiKey:
suppliedIn:
header:
- "X-header-name"
clientSecret: api-key-client-secret
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: api-key-client-secret
type: nginx.org/apikey
data:
client1-premium: Y2xpZW50MXByZW1pdW0= # client1premium
client2-premium: Y2xpZW50MnByZW1pdW0= # client2premium
client1-basic: Y2xpZW50MWJhc2lj # client1basic
client2-basic: Y2xpZW50MmJhc2lj # client2basic
random: cmFuZG9t # random
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: cafe
spec:
host: cafe.example.com
upstreams:
- name: coffee
service: coffee-svc
port: 80
policies:
- name: api-key-policy
- name: rate-limit-apikey-premium
- name: rate-limit-apikey-basic
routes:
- path: /coffee
action:
pass: coffee
32 changes: 32 additions & 0 deletions examples/custom-resources/rate-limit-tiered-apikey/coffee.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 1
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: rate-limit-apikey-premium
spec:
rateLimit:
rate: 5r/s
key: ${apikey_auth_token}
zoneSize: 10M
condition:
variables:
- match: "~^.*-premium$"
name: $apikey_client_name
rejectCode: 429
---
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: rate-limit-apikey-basic
spec:
rateLimit:
rate: 1r/s
key: ${apikey_auth_token}
zoneSize: 10M
condition:
variables:
- match: "~^.*-basic$"
name: $apikey_client_name
default: true
rejectCode: 429
Loading