Rate limiting i NGINX

Marcus Olsson,

Häromdagen stötte jag på ett intressant problem där en endpoint på en av servrarna jag hanterar helt plötsligt blev helt överflödad med trafik – tusentals anrop per sekund. En DDoS-attack helt enkelt.

Exakt varför just denna webbplatsen blev attackerad förstår jag dock inte, troligtvis av misstag eller någon typ av test, men inte desto mindre irriterande då det kom i vågor. Och i varje våg så sänktes servern på bara ett par sekunder (och där felloggarna växte till ett par GB på bara någon minut...)

I vanliga fall kan man snabbt och enkelt skydda sig genom att t.ex. använda Cloudflares fantastiska tjänster, men just i detta fallet så var inte det ett alternativ, istället började jag läsa på om hur rate limiting fungerade i NGINX.

Visade sig att det var väldigt enkelt och effektivt.

Syntax och uppsättning

Det finns egentligen två primära metoder att rikta in sig på när man vill begränsa anropen till servern. Antingen genom just anrop (requests) eller genom anslutningar (connections). I det här fallet så körde jag anrop.

En förtitt på slutresultatet:

1limit_req_zone $binary_remote_addr zone=limit:10m rate=2r/s;
2 
3server {
4 location / {
5 limit_req zone=limit;
6 try_files $uri $uri/ /index.php?$query_string;
7 }
8}
1limit_req_zone $binary_remote_addr zone=limit:10m rate=2r/s;
2 
3server {
4 location / {
5 limit_req zone=limit;
6 try_files $uri $uri/ /index.php?$query_string;
7 }
8}

Detta är då en enkel PHP-applikation där all trafik "routas" genom index.php där jag ville fånga upp alla anrop och sedan begränsa dem.

Om vi kikar på de olika parametrarna:

Med limit_req_zone sätter vi upp en ny "zon" där vi fångar upp klientens IP-adress genom $binary_remote_addr. Med zone=limit:10m sätter vi dels namnet på zonen till limit och tilldelar den 10MB minne (mer info om detta nedanför), och tillåter 2 anrop i sekunden (rate=2r/s).

Denna definitionen är utanför server-blocket, detta gör att vi kan återanvända den för varje location som vi tycker passar. I det här fallet location / genom limit_req zone=limit (där igen limit är namnet på vår zon.)

Svårare än så är det inte. I nästa våg av anrop fastnade de direkt (NGINX skickar en 503:a till klienten då limiten kickar in):

12020/02/12 10:04:30 [error] 159240: *92781 limiting requests, excess: 0.002 by zone "limit", client: 104.251.122.194, server: xxx, request: "GET zzz HTTP/1.1", host: "zzz"
12020/02/12 10:04:30 [error] 159240: *92781 limiting requests, excess: 0.002 by zone "limit", client: 104.251.122.194, server: xxx, request: "GET zzz HTTP/1.1", host: "zzz"

Från NGINX error-loggen.

Rate limiting och minnesanvändning

Då man gärna inte vill att en sådan stor mängd anrop ändå förbrukar allt minne på servern så gjorde jag lite research kring hur det tilldelade minnet egentligen fungerar, och NGINX gör det väldigt smart.

Genom zone=limit:10m så satte vi 10MB på zonen limit. Detta innebär att zonen kan hantera runt 160 000 IP-adresser (beror lite på om det är IPv4 eller IPv6). I mitt fall räckte detta gott och väl då det egentligen handlade om kanske 3-400 unika IP-adresser.

Om minnet mot förmodan tar slut så raderas den äldsta adressen och den nya förs in. Dessutom när en ny adress förs in så kontrollerar NGINX om det finns några adresser som inte har används de senaste 60 sekundrarna och raderar upp till två stycken av dessa.

Det fina är att allt detta görs per automatik. Ingen handpåläggning alls behövs här.

Mer att göra och läsa

NGINXs rate limit-modul har ännu fler möjligheter, till exempel att endast begränsa "bursts" av anslutningar och liknande. Kan vara väl värt att kika på i framtiden.

Läs annars mer om rate limiting i dokumentationen och i deras guide.