Stop Spamming with Nginx
Learn how to limit client requests using the Nginx limit_req and limit_req_zone directives. We will also create a live demonstration by writing a script to spam an Nginx server with requests.
Table of Contents 📖
- Rate Limiting Requests
- Nginx Leaky Bucket Algorithm
- Configuring Rate Limiting
- Testing Nginx Rate Limiting
Rate Limiting Requests
Rate limiting requests allows us to limit the amount of HTTP requests a client can make in a given period of time. This helps preventing brute-force password attacks, DDOS attacks, and protect upstream servers from being overloaded. For example, we can spam an Nginx server with requests using a simple script.
for i in {1..100}; do
curl localhost
done
This script sends 100 requests to the Nginx server in a matter of milliseconds. If we had an upstream server that handled this load, it could experience performance issues. We can prevent this by having Nginx drop these requests before they reach the upstream server.
Nginx Leaky Bucket Algorithm
Nginx limits requests using the leaky bucket algorithm. In this analogy, the water is the requests, the bucket represents a queue for processing the requests, and the leaking water is the requests leaving the queue to be processed. If the queue is full and more requests come in, then the requests are dropped. In other words, the bucket overflows. To add more requests to the queue, we need to wait for the bucket to leak enough water.
Configuring Rate Limiting
We can configure Nginx to limit requests by using the limit_req_zone and limit_req directives.
http {
...
limit_req_zone $binary_remote_addr zone=myzone:10m rate=10r/s;
server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
location /limit {
limit_req zone=myzone;
try_files $uri /index.html;
}
}
}
This configuration limits each IP address to 10 requests per second for the /limit route. If a client exceeds this then Nginx will return a 503 status code. The limit_req_zone directive sets up a rate limiting zone. It takes 3 arguments:
- Key - The request characteristic that the limit is applied to. Here we set it to $binary_remoate_addr which is a binary representation of a client's IP address.
- Zone - The shared memory zone used to store the state of each request. Here we are storing how many times an IP address has accessed the /limit route in a 10 MB zone called myzone.
- Rate - The rate limit in requests per second (r/s). Here we set it to 10 requests per second or 1 request every 100 ms. If a request arrives in less than 100 ms after the previous one, it will be rejected.
INFO: Shared memory zones are shared among Nginx worker processes.
To apply the limit we need to use the limit_req directive. Specifically, we place it in the desired location or server block.
Testing Nginx Rate Limiting
Lets test this rate limiting now by sending some cURL requests to Nginx.
for i in {1..100}; do
curl localhost/limit
done
INFO: We can make the file executable for all users by running chmod a+x limit.sh!
Now lets run this file and look at the output.
./limit.sh
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx/1.25.5</center>
</body>
</html>
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx/1.25.5</center>
</body>
</html>
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx/1.25.5</center>
</body>
</html>
Notice how the first request works fine but the others are blocked. We can also check out Nginx error logs and look at the output.
[03/Jun/2024:14:28:52 +0000] "GET /limit HTTP/1.1" URI - /index.html
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 32#32: *35 limiting requests, excess: 0.880 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
2024/06/03 14:25:35 [error] 32#32: *36 limiting requests, excess: 0.780 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 32#32: *37 limiting requests, excess: 0.690 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
2024/06/03 14:25:35 [error] 32#32: *38 limiting requests, excess: 0.590 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 31#31: *39 limiting requests, excess: 0.480 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 31#31: *40 limiting requests, excess: 0.400 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 31#31: *41 limiting requests, excess: 0.320 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
2024/06/03 14:25:35 [error] 31#31: *42 limiting requests, excess: 0.250 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit
2024/06/03 14:25:35 [error] 31#31: *43 limiting requests, excess: 0.170 by zone "myzone", client: 172.17.0.1, server: localhost, request: "GET /limit HTTP/1.1", host: "localhost"
[03/Jun/2024:14:25:35 +0000] "GET /limit HTTP/1.1" URI - /limit