diff --git a/deployment/proxy/nginx-cms.conf b/deployment/proxy/nginx-cms.conf index e21659da..a9ed287a 100644 --- a/deployment/proxy/nginx-cms.conf +++ b/deployment/proxy/nginx-cms.conf @@ -192,11 +192,10 @@ server { keepalive_timeout 30s; send_timeout 10s; - # Abuse limits. Applied per server, not per location, so cached assets and - # proxied API routes are all covered by the same budget. nodelay keeps the - # 60-request burst responsive: allowed requests pass immediately, only the - # excess is rejected with 503 instead of being queued. - limit_req zone=cms_req_per_ip burst=60 nodelay; + # Abuse limits. Deliberately NOT set at server scope: a room load and a page + # load are not the same request profile, so each location picks its own zone. + # Locations without an explicit limit_req inherit nothing and are unlimited — + # the page/API routes below carry the budget instead. limit_conn cms_conn_per_ip 30; # Traefik health-check route herstellen @@ -210,6 +209,7 @@ server { location ^~ /client/ { alias /var/www/Octane/dist/; try_files $uri $uri/ =404; + limit_req zone=cms_static_per_ip burst=1000 nodelay; location ~* \.(js|json|css|html|wasm|ttf|woff|woff2|gif|webp|png|jpg|jpeg|svg|dat)$ { add_header Cache-Control "public, max-age=2592000"; @@ -223,6 +223,7 @@ server { location ^~ /nitro-client/ { alias /var/www/Octane/dist/; try_files $uri $uri/ =404; + limit_req zone=cms_static_per_ip burst=1000 nodelay; location ~* \.(js|json|css|html|wasm|ttf|woff|woff2|gif|webp|png|jpg|jpeg|svg|dat)$ { add_header Cache-Control "public, max-age=2592000"; @@ -265,6 +266,7 @@ server { add_header Cache-Control "public, max-age=300, must-revalidate"; access_log off; add_header Cache-Tag "cms-gamedata"; + limit_req zone=cms_static_per_ip burst=1000 nodelay; add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; @@ -280,6 +282,7 @@ server { add_header Cache-Control "public, max-age=3600, must-revalidate"; access_log off; add_header Cache-Tag "cms-gamedata"; + limit_req zone=cms_static_per_ip burst=1000 nodelay; add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; @@ -454,6 +457,7 @@ server { # ─── Hoofd-routering ─── location / { proxy_pass http://cms_app; + limit_req zone=cms_req_per_ip burst=60 nodelay; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/deployment/proxy/nginx.conf b/deployment/proxy/nginx.conf index dae1800f..0aba6433 100644 --- a/deployment/proxy/nginx.conf +++ b/deployment/proxy/nginx.conf @@ -44,10 +44,17 @@ http { client_header_buffer_size 1k; large_client_header_buffers 4 8k; - # Rate limiting per client IP. The Nitro client fetches gamedata and icons in - # bursts when booting a room, so the burst is deliberately generous: it caps - # sustained floods without punishing a normal room load. + # Rate limiting per client IP. + # + # Two zones, because a room load and a page load are not the same thing. + # Loading a Nitro room fires several hundred gamedata icons in one burst; + # at the page rate that produced 503s on real players. Static assets + # therefore get their own, much higher allowance. These are small immutable + # files, so a request rate is not what protects them anyway — nginx already + # serves them with must-revalidate, and the CMS upstream stays behind + # cms_req_per_ip for the expensive routes. limit_req_zone $binary_remote_addr zone=cms_req_per_ip:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=cms_static_per_ip:10m rate=1000r/s; limit_conn_zone $binary_remote_addr zone=cms_conn_per_ip:10m; # Blue/green cutover: ci-deploy.sh writes the active upstream here, and