We are trying to implement a request rate limit on our Django NGINX server. I went through some articles about how to do this, such as https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http/.
I can see that there's the ability to put a specific rate limit on an IP address, and also on a specific "location", that is an API endpoint. But is it also possible to group together some IP addresses and apply a cumulative rate limit.
For example, we have a partner that has 5 IP addresses and we want to apply a rate limit of let's say 10 rps on a specific API (/users/), but this 10 rps is a cumulative limit on all the 5 IP addresses, and NOT 10 rps for EACH IP address. I couldn't find on the official NGINX docs or anywhere on Google if this is possible or not.
For more clarity, let's consider these examples
123.123.123.123
- /users
- 10 rps
123.123.123.123
- /products
- 15 rps
45.45.45.45
, 65.65.65.65
- /users
- 20 rps
45.45.45.45
, 65.65.65.65
- /products
- 30 rps
So API requests to /users
endpoint from IP 123.123.123.123
should be limited to 10 rps
, and /products
endpoint from the same IP should have a limit of 15 rps
.
API requests to /users
endpoint from both 45.45.45.45
and 65.65.65.65
COMBINED should be limited to 20 rps
.
Similarly, for /products
endpoint from both those IPs should be limited to 30 rps
.
Any other IP will have let's say a default rate limit of 5 rps, no matter what API endpoint is called.
Hope this clears up any confusion! Thanks!
Your conditions are quite complicated. If, by example, requirements would be to apply a fixed 10 rps rate for the group of 5 IP addresses, and the same 10 rps rate for every other IP, you could do this with a single limit_req
zone:
limit_req_zone $users_key zone=users:10m rate=10r/s;
geo $users_key {
<ip1> unique_customer_id;
<ip2> unique_customer_id;
<ip3> unique_customer_id;
<ip4> unique_customer_id;
<ip5> unique_customer_id;
default $binary_remote_addr;
}
server {
...
location /users/ {
limit_req zone=users;
... proxy request to the backend
}
}
That unique_customer_id
can be any string that won't be equal to any possible $binary_remote_addr
value (which is a binary-packed string of 4 or 16 bytes length, so using something as simple as 1
will be enough; don't use a long id string to minimize the shared zone memory footprint).
However since your requirements are more complicated, including different rates for different users, we need to declare many different zones to achieve the result you want. The solution is based upon a fact that an empty zone key won't be accounted to limit requests with a single limit_req
rule.
Here is the possible solution.
# zones definitions
limit_req_zone $unknown_customer zone=users_5:10m rate=5r/s;
limit_req_zone $customer_limited_group_1 zone=users_10:1m rate=10r/s;
limit_req_zone $customer_limited_group_2 zone=users_20:1m rate=20r/s;
limit_req_zone $unknown_customer zone=products_5:10m rate=5r/s;
limit_req_zone $customer_limited_group_1 zone=products_15:1m rate=15r/s;
limit_req_zone $customer_limited_group_2 zone=products_30:1m rate=30r/s;
# calculating keys
map $remote_addr $unknown_customer {
123.123.123.123 '';
45.45.45.45 '';
65.65.65.65 '';
# exclude any other IP for customers with different rates here, e.g.
# 124.124.124.124 '';
# 85.85.85.85 '';
default $binary_remote_addr;
}
map $remote_addr $customer_limited_group_1 {
123.123.123.123 1;
# here you can add another customers limited to 10/15 rps with another id, e.g.
# 124.124.124.124 2;
# the default value will be an empty string
}
map $remote_addr $customer_limited_group_2 {
# this will be a key for a different zone;
# value '1' does not interfere with the '1' from the previous map block in any way
45.45.45.45 1;
65.65.65.65 1;
# another customers limited to 20/30 rps can be added here with different ids, e.g.
# 85.85.85.85 2;
}
server {
...
location /users/ {
limit_req zone=users_5;
limit_req zone=users_10;
limit_req zone=users_20;
... proxy request to the backend
}
location /products/ {
limit_req zone=products_5;
limit_req zone=products_15;
limit_req zone=products_30;
... proxy request to the backend
}
}