upstream sapl_server { server unix:/var/interlegis/sapl/run/gunicorn.sock fail_timeout=0; } # Reuse X-Request-ID from ingress if present; otherwise generate one. map $http_x_request_id $req_id { default $http_x_request_id; "" $request_id; } server { listen 80; server_name sapl.prod; client_max_body_size 4G; # ---------------------------------------------------------------- # Block known scraper ASNs (datacenter traffic) — zero Python cost. # ---------------------------------------------------------------- if ($bot_asn = 1) { return 429 "Too Many Requests"; } # ---------------------------------------------------------------- # Block known bots by User-Agent — zero Python cost. # ---------------------------------------------------------------- if ($bot_ua_blocked = 1) { return 429 "Too Many Requests"; } # ---------------------------------------------------------------- # robots.txt served directly by nginx. # ---------------------------------------------------------------- location = /robots.txt { alias /var/interlegis/sapl/collected_static/robots.txt; } # ---------------------------------------------------------------- # Static files — no rate limiting, no proxy. # ---------------------------------------------------------------- location /static/ { alias /var/interlegis/sapl/collected_static/; expires 90m; add_header Cache-Control "public, max-age=5400"; } # ---------------------------------------------------------------- # Media files — routed through Django for auth, rate counting, # and content-type caching; served from disk via X-Accel-Redirect. # ---------------------------------------------------------------- location /media/ { limit_req zone=sapl_general burst=${NGINX_BURST_GENERAL} nodelay; limit_req_status 429; proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://sapl_server; } # Internal location used exclusively by X-Accel-Redirect responses # from serve_media(). Not reachable by external clients. location /internal/media/ { internal; alias /var/interlegis/sapl/media/; sendfile on; etag on; } # ---------------------------------------------------------------- # /relatorios/ — heaviest endpoint (PDF generation). # Tighter rate limit; extended timeout for uncached generation. # ---------------------------------------------------------------- location /relatorios/ { limit_req zone=sapl_heavy burst=${NGINX_BURST_HEAVY} nodelay; limit_req_status 429; proxy_read_timeout 180s; proxy_send_timeout 180s; proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://sapl_server; } # ---------------------------------------------------------------- # /api/ — rate limited, CORS maintained from original config. # ---------------------------------------------------------------- location /api/ { limit_req zone=sapl_general burst=${NGINX_BURST_API} nodelay; limit_req_status 429; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Access-Control-Allow-Origin,XMLHttpRequest,Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,XMLHttpRequest,Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://sapl_server; } # ---------------------------------------------------------------- # General traffic — moderate rate limit. # ---------------------------------------------------------------- location / { limit_req zone=sapl_general burst=${NGINX_BURST_GENERAL} nodelay; limit_req_status 429; proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://sapl_server; } error_page 429 /429.html; location = /429.html { add_header Retry-After 60 always; root /var/interlegis/sapl/sapl/static/; internal; } error_page 500 502 503 504 /500.html; location = /500.html { root /var/interlegis/sapl/sapl/static/; internal; } }