Nginx Rate Limiting en DDoS-bescherming
Configureer Nginx rate limiting met limit_req, limit_conn en fail2ban om je server te beschermen tegen brute-force-aanvallen en applicatielaag-DDoS, zonder afhankelijkheid van externe diensten.
Rate limiting is je eerste verdedigingslinie tegen brute-force-aanvallen, API-misbruik en applicatielaag-DDoS. Deze tutorial bouwt drie beschermingslagen op met alleen Nginx en fail2ban. Geen externe DDoS-diensten, geen verkeer dat je server verlaat.
Je configureert request-rate-limiting (limit_req), verbindingsbeperking (limit_conn) en automatische IP-blokkering (fail2ban) op Debian 12 of Ubuntu 24.04.
Vereisten:
- Nginx geïnstalleerd en actief
- Basiskennis van de Nginx-configuratiestructuur
- Root- of sudo-toegang
Hoe werkt Nginx rate limiting?
Nginx rate limiting gebruikt het leaky bucket-algoritme via de limit_req_zone- en limit_req-directieven. Inkomende requests vullen een emmer met willekeurige snelheid. De emmer leegt zich met een vast tempo dat jij bepaalt. Als de emmer overloopt, weigert Nginx de overtollige requests. Dit vlakt verkeerspieken af terwijl de verwerkingssnelheid constant blijft.
De implementatie omvat twee directieven. limit_req_zone definieert de gedeelde geheugenzone die de clientstatus bijhoudt over alle workerprocessen. limit_req past de limiet toe op specifieke locations.
# In het http-blok: zone definiëren
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# In een server- of location-blok: limiet toepassen
limit_req zone=api;
De sleutel $binary_remote_addr slaat elk client-IP op in compact binair formaat (4 bytes voor IPv4, 16 bytes voor IPv6). Een zone van 10 MB past ongeveer 160.000 IPv4-adressen of 80.000 IPv6-adressen. Voor de meeste servers is 10m voldoende.
De rate-parameter accepteert requests per seconde (r/s) of per minuut (r/m). Nginx volgt dit intern in milliseconden. Een rate van 10r/s betekent één request per 100 ms.
Hoe configureer ik limit_req_zone en limit_req?
Maak een apart configuratiebestand voor rate limiting:
sudo nano /etc/nginx/conf.d/rate-limiting.conf
# Gedeelde geheugenzones - gedefinieerd op http-niveau
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
# 429 retourneren in plaats van de standaard 503
limit_req_status 429;
# Rate limit-gebeurtenissen loggen op warn-niveau (vertragingen op notice)
limit_req_log_level warn;
Pas vervolgens de zones toe in je server-blok:
sudo nano /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
# Algemene rate-limiet voor alle requests
limit_req zone=general burst=20 nodelay;
location /login {
# Strikte limiet op het login-endpoint
limit_req zone=login burst=3 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
# Hogere limiet voor API-gebruikers
limit_req zone=api burst=50 delay=30;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Test de configuratie en herlaad:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Wanneer een limit_req-directief in een location-blok staat, overschrijft het elke limit_req die van het server-niveau wordt geërfd. De general-zone geldt voor / maar niet voor /login of /api/ omdat die locations eigen limit_req-directieven hebben. Als je beide zones wilt laten gelden, voeg dan meerdere limit_req-regels toe in hetzelfde blok.
Wat doen burst, nodelay en delay?
De burst-parameter bepaalt hoeveel overtollige requests Nginx in de wachtrij plaatst in plaats van direct te weigeren. Zonder burst krijgt elk request boven de rate een 429. Met burst houdt Nginx overtollige requests in een wachtrij en geeft ze vrij op het basistempo.
| Parameter | Directe requests | Wachtrij | Geweigerd | Toepassing |
|---|---|---|---|---|
burst=0 (standaard) |
1 per interval | Geen | Alles boven de rate | Strikte API-limieten |
burst=5 |
1 per interval | Tot 5, vrijgegeven op basistempo | Boven burst+1 | Formulierinzendingen |
burst=5 nodelay |
Tot 6 tegelijk | Geen in wachtrij, maar burst-slots vullen bij op basistempo | Boven burst+1 tot slots bijgevuld | Loginpagina's, algemeen verkeer |
burst=20 delay=10 |
Tot 11 tegelijk | Requests 12-21 vertraagd op basistempo | Boven burst+1 | API's met incidentele pieken |
Met burst=5 (zonder nodelay) worden bij 6 gelijktijdige requests request 1 direct verwerkt. Requests 2-6 staan in de wachtrij en komen vrij per interval (elke 100 ms bij 10r/s). Het laatste request wacht 500 ms. Dit voegt latentie toe maar verwerpt nooit legitieme bursts.
Met burst=5 nodelay worden alle 6 requests direct verwerkt. Maar de 5 burst-slots hebben 500 ms nodig om bij te vullen. Als 200 ms later nog 6 requests binnenkomen, zijn er maar 3 slots bijgevuld, dus 3 overtollige requests worden geweigerd.
Met burst=20 delay=10 worden de eerste 11 requests (1 basis + 10 delay-drempel) zonder wachttijd verwerkt. Requests 12-21 worden vertraagd op het basistempo. Alles boven 21 wordt geweigerd. Deze hybride aanpak werkt goed voor API's die periodieke bursts van legitieme batchclients ontvangen.
Hoe beperk ik verschillende endpoints apart?
Definieer aparte zones met verschillende sleutels voor onafhankelijke limieten. Het bovenstaande voorbeeld gebruikt al drie zones. Je kunt ook per URI-pad beperken door $uri als sleutel te gebruiken:
# Per-URI rate limiting: beperkt het totale aantal requests per unieke URI
limit_req_zone $uri zone=per_uri:10m rate=50r/s;
Dit is handig wanneer bepaalde endpoints (zoals een zoekpagina of een exportfunctie) globale throttling nodig hebben, ongeacht welke client ze aanroept.
Voor API-key-gebaseerde rate limiting gebruik je map om de sleutel uit een header te halen:
map $http_x_api_key $api_key_limit {
default $binary_remote_addr;
"~^.+$" $http_x_api_key;
}
limit_req_zone $api_key_limit zone=api_keyed:10m rate=100r/s;
Als de client een X-API-Key-header stuurt, is de rate-limiet gebaseerd op die sleutel. Anders valt het terug op IP-gebaseerde beperking.
Hoe beperk ik verbindingen met limit_conn?
Terwijl limit_req het requesttempo regelt, beperkt limit_conn het aantal gelijktijdige verbindingen per client. Het werkt goed tegen slowloris-aanvallen en downloadmisbruik.
# In het http-blok
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# In een server- of location-blok
server {
# Max 20 gelijktijdige verbindingen per IP
limit_conn addr 20;
location /downloads/ {
# Max 2 gelijktijdige downloads per IP
limit_conn addr 2;
limit_rate 1m; # Bandbreedte ook beperken tot 1 MB/s per verbinding
}
}
Let op bij HTTP/2 en HTTP/3: elk concurrent request telt als een aparte verbinding. Een browser die een pagina met 30 assets laadt via één HTTP/2-verbinding telt als 30 verbindingen voor limit_conn. Stel de limiet hoger in dan bij HTTP/1.1.
limit_conn en limit_req vullen elkaar aan. Gebruik beide. limit_req stopt snelvuur-requests. limit_conn stopt verbindingsoverstromingen.
Hoe retourneer ik een aangepaste 429-foutpagina?
Standaard ontvangen rate-limited requests een generieke foutpagina. Een aangepaste 429-pagina kan een Retry-After-header en een leesbaar bericht bevatten.
Maak de foutpagina:
sudo mkdir -p /var/www/error
sudo nano /var/www/error/429.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>429 - Too Many Requests</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 5rem 1rem; }
h1 { font-size: 2rem; }
p { color: #555; }
</style>
</head>
<body>
<h1>429 - Too Many Requests</h1>
<p>You have exceeded the request limit. Wait a moment and try again.</p>
</body>
</html>
sudo chmod 644 /var/www/error/429.html
Voeg de foutpagina en Retry-After-header toe aan je server-blok:
server {
#... rate limiting-directieven...
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
}
Het internal-directief voorkomt directe toegang tot de foutpagina. Het always-sleutelwoord bij add_header zorgt ervoor dat de header ook bij foutresponses wordt verzonden. De Retry-After-waarde (in seconden) vertelt brave clients wanneer ze het opnieuw kunnen proberen.
Hoe test ik rate limits veilig met dry_run?
Schakel limit_req_dry_run on in om rate limiting te simuleren zonder requests te weigeren. Nginx logt wat het zou hebben gedaan, maar alle requests gaan door. Deze optie is beschikbaar sinds Nginx 1.17.1.
server {
limit_req zone=general burst=20 nodelay;
limit_req_dry_run on; # Alleen loggen, niet afdwingen
# Rate limit-status toevoegen aan access logs
#...
}
Voeg $limit_req_status toe aan je logformaat om dry-run-gebeurtenissen in access logs te volgen:
# In het http-blok
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# In het server-blok
access_log /var/log/nginx/access.log ratelimit;
De dry_run-workflow:
- Voeg
limit_req_dry_run on;toe aan je configuratie - Herlaad Nginx
- Genereer testverkeer (zie het testgedeelte hieronder)
- Controleer het foutenlog op dry-run-vermeldingen:
sudo grep "dry run" /var/log/nginx/error.log
2026/03/19 14:22:31 [warn] 1234#1234: *567 limiting requests, dry run, excess: 1.532 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Het logniveau is hier [warn] vanwege het limit_req_log_level warn-directief. Zorg ervoor dat je error_log-directief niveau warn of lager bevat, anders verschijnen deze berichten niet. De productieconfiguratie met error_log /var/log/nginx/example.error.log warn; regelt dit.
- Controleer de access logs op de
$limit_req_status-variabele:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log
De $limit_req_status-variabele geeft een van de volgende waarden terug: PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN of REJECTED_DRY_RUN.
- Als de rates er goed uitzien, verwijder de
limit_req_dry_run-regel en herlaad.
Hoe stel ik een allowlist in voor vertrouwde IP's?
Gebruik een geo-blok om monitoringsystemen, load balancers of kantoor-IP's uit te sluiten van rate limiting:
# In het http-blok
geo $limit {
default 1;
10.0.0.0/8 0; # Intern netwerk
192.168.0.0/16 0; # Intern netwerk
203.0.113.10 0; # Monitoringserver
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=general:10m rate=10r/s;
Als $limit_key een lege string is, slaat Nginx rate limiting volledig over voor dat request. IP's die overeenkomen met het geo-blok krijgen $limit = 0, wat wordt omgezet naar een lege sleutel.
Als je wilt dat IP's op de allowlist een hogere rate krijgen in plaats van geen limiet:
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=trusted:1m rate=100r/s;
server {
limit_req zone=general burst=20 nodelay;
limit_req zone=trusted burst=100 nodelay;
}
Alle IP's komen overeen met trusted, maar alleen niet-geliste IP's komen overeen met general. De meest beperkende limiet geldt, dus IP's op de allowlist zijn gelimiteerd tot 100 r/s terwijl alle anderen 10 r/s krijgen.
Hoe blokkeer ik herhaaldelijke overtreders met fail2ban?
Rate limiting weigert individuele requests, maar volhardende aanvallers blijven terugkomen. fail2ban bewaakt het Nginx-foutenlog en blokkeert IP's op firewall-niveau na herhaalde overtredingen.
Installeer fail2ban als je dat nog niet hebt gedaan:
sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-03-19 14:30:00 UTC; 2s ago
fail2ban bevat een ingebouwd nginx-limit-req-filter. De regex van het filter matcht regels als:
limiting requests, excess: 1.532 by zone "general", client: 203.0.113.50
Maak de jail-configuratie. Bewerk nooit .conf-bestanden direct; gebruik .local-overrides:
sudo nano /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 600
Dit blokkeert een IP voor 10 minuten na 10 rate-limit-overtredingen binnen 60 seconden.
Voor oplopende blokkades voeg je een tweede jail toe in hetzelfde bestand:
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 30
findtime = 3600
bantime = 86400
De eerste jail vangt korte bursts (10 hits in een minuut = 10-minuten-blokkade). De tweede vangt volhardende overtreders (30 hits in een uur = 24-uur-blokkade).
Herstart fail2ban en controleer de jail-status:
sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req
Op Debian 12 en oudere systemen ziet de uitvoer er zo uit:
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/nginx/error.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Op Ubuntu 24.04 gebruikt fail2ban standaard de systemd journal-backend (backend = auto wordt systemd). De uitvoer toont Journal matches: in plaats van File list::
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=nginx.service + _COMM=nginx
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Beide backends werken. De journal-backend leest dezelfde Nginx-logberichten via systemd. Als je liever bestandsgebaseerde monitoring gebruikt, voeg dan backend = pyinotify toe aan de jail-sectie.
Om een IP handmatig te deblokkeren tijdens tests:
sudo fail2ban-client set nginx-limit-req unbanip 203.0.113.50
Het jail-log vind je via:
sudo journalctl -u fail2ban -f
Hoe controleer ik of rate limiting werkt?
Test vanaf een machine buiten de server. Test nooit vanaf localhost omdat 127.0.0.1 op je allowlist kan staan.
Snelle test met een curl-lus:
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
done
Met een rate van 10r/s en burst=20 nodelay retourneren de eerste 21 requests 200. Zodra de burst is uitgeput, schakelen de responses over naar 429.
Belastingtest met wrk:
sudo apt install -y wrk
wrk -t2 -c10 -d10s https://example.com/
Running 10s test @ https://example.com/
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.23ms 2.11ms 28.44ms 75.32%
Req/Sec 1.02k 121.33 1.34k 68.00%
20384 requests in 10.01s, 15.22MB read
Non-2xx or 3xx responses: 18241
Requests/sec: 2036.36
Transfer/sec: 1.52MB
De Non-2xx or 3xx responses-teller toont hoeveel requests Nginx heeft beperkt. Hier ontvingen 18.241 van de 20.384 requests een 429.
Controleer het foutenlog tijdens de test:
sudo tail -f /var/log/nginx/error.log
2026/03/19 14:45:12 [warn] 1234#1234: *890 limiting requests, excess: 9.876 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
De excess-waarde toont hoeveel het request de limiet overschreed. Hogere waarden wijzen op agressiever verkeer.
Volledige productieconfiguratie
De complete rate limiting-configuratie die alle drie de lagen combineert:
# /etc/nginx/conf.d/rate-limiting.conf
# --- Allowlist ---
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/16 0;
# Voeg hier je monitoring-/kantoor-IP's toe
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
# --- Request rate-zones ---
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=1r/s;
limit_req_zone $limit_key zone=api:10m rate=30r/s;
# --- Verbindingszone ---
limit_conn_zone $binary_remote_addr zone=addr:10m;
# --- Responscodes en logging ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;
# --- Access log met rate limit-status ---
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.access.log ratelimit;
error_log /var/log/nginx/example.error.log warn;
# Globale limieten
limit_req zone=general burst=20 nodelay;
limit_conn addr 30;
# Aangepaste 429-pagina
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
location /login {
limit_req zone=login burst=3 nodelay;
limit_conn addr 5;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=50 delay=30;
limit_conn addr 20;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 10
findtime = 60
bantime = 600
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 30
findtime = 3600
bantime = 86400
Na het schrijven van alle configuratiebestanden:
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban
Voor een breder beveiligingssetup inclusief headers, TLS en andere hardening-maatregelen, zie.
Directivereferentie
| Directief | Context | Standaard | Sinds |
|---|---|---|---|
limit_req_zone |
http | - | 0.7.21 |
limit_req |
http, server, location | - | 0.7.21 |
limit_req_status |
http, server, location | 503 | 1.3.15 |
limit_req_log_level |
http, server, location | error | 0.8.18 |
limit_req_dry_run |
http, server, location | off | 1.17.1 |
limit_conn_zone |
http | - | 1.1.8 |
limit_conn |
http, server, location | - | 0.7.21 |
limit_conn_status |
http, server, location | 503 | 1.3.15 |
limit_conn_log_level |
http, server, location | error | 0.8.18 |
limit_conn_dry_run |
http, server, location | off | 1.17.6 |
Probleemoplossing
Rate limiting werkt helemaal niet: Controleer of limit_req_zone in het http-blok staat, niet in een server-blok. Zones moeten worden gedefinieerd voordat ze worden gerefereerd. Als je configuratie include-directieven gebruikt, zorg er dan voor dat het zonebestand vóór de server-blokken wordt opgenomen.
Legitieme gebruikers krijgen 429: Verlaag de rate, verhoog de burst, of schakel over van geen burst naar burst=N nodelay. Gebruik de dry_run-modus om werkelijke verkeerspatronen te meten voordat je limieten instelt. Controleer of HTTP/2-multiplexing de limit_conn-tellers opblaast.
fail2ban blokkeert niet: Bevestig dat het logpath overeenkomt met waar Nginx daadwerkelijk foutenlogs schrijft. Controleer of limit_req_log_level is ingesteld op warn of error (de standaard). Verifieer of de jail actief is met sudo fail2ban-client status nginx-limit-req. Als je vanaf localhost test, merk op dat fail2ban het IP van de server zelf standaard negeert (ignoreself = true). Test vanaf een externe machine.
Rate limit-berichten verschijnen niet in het foutenlog: Het error_log-directief staat standaard op error-niveau, wat warn-berichten van limit_req_log_level warn filtert. Stel error_log /var/log/nginx/error.log warn; in in je server-blok om rate limit-gebeurtenissen te zien.
Zonegeheugen uitgeput: Een 10m-zone bevat ongeveer 160.000 IPv4-states. Als je could not allocate node in de logs ziet, vergroot de zone. Monitor het zonegebruik over tijd met $limit_req_status in je access logs.
Achter een load balancer of CDN: Als Nginx alleen het IP van de load balancer ziet, geldt rate limiting voor dat ene IP. Gebruik $http_x_forwarded_for of $realip_remote_addr als zonesleutel in plaats van $binary_remote_addr. Je moet ook set_real_ip_from configureren met het IP-bereik van de load balancer. Vertrouw X-Forwarded-For alleen van bekende proxies, want clients kunnen het vervalsen.
Logs zijn je belangrijkste debuggingtool:
# Rate limit-gebeurtenissen in realtime volgen
sudo tail -f /var/log/nginx/error.log | grep "limiting"
# fail2ban-acties controleren
sudo journalctl -u fail2ban -f
Copyright 2026 Virtua.Cloud. Alle rechten voorbehouden. Deze inhoud is een origineel werk van het Virtua.Cloud-team. Reproductie, herpublicatie of herdistributie zonder schriftelijke toestemming is verboden.
Klaar om het zelf te proberen?
Deploy uw eigen server in seconden. Linux, Windows of FreeBSD.
Bekijk VPS-aanbod