# Rate limiting (per IP) — must be at http level, not inside server block # api_limit mirrors the backend's own per-IP cap (mw.RateLimit(120, time.Minute)). limit_req_zone $binary_remote_addr zone=api_limit:10m rate=120r/m; limit_req_zone $binary_remote_addr zone=dav_limit:10m rate=100r/m; # Webhook bursts (Square retry storms) need a higher ceiling than the API limit limit_req_zone $binary_remote_addr zone=webhook_limit:10m rate=120r/m; # Real client IP resolution behind Cloudflare (FAULT A4): Cloudflare sets # CF-Connecting-IP BEFORE nginx, so stripping it (as the old config did) hid # the real client from the backend and collapsed per-IP rate limiting to # per-Cloudflare-edge-IP buckets. Instead the real_ip module rewrites # $remote_addr to the CF-Connecting-IP value ONLY when the actual TCP peer # matches one of the trusted Cloudflare ranges below — a direct client cannot # spoof it, because its own address is never in these ranges. The rewritten # $remote_addr then flows into X-Real-IP (and CF-Connecting-IP) downstream. # Must be at http level so every server/location sees the resolved address. # Cloudflare IPv4 ranges (https://www.cloudflare.com/ips/). set_real_ip_from 173.245.48.0/20; set_real_ip_from 103.21.244.0/22; set_real_ip_from 103.22.200.0/22; set_real_ip_from 103.31.4.0/22; set_real_ip_from 141.101.64.0/18; set_real_ip_from 108.162.192.0/18; set_real_ip_from 190.93.240.0/20; set_real_ip_from 188.114.96.0/20; set_real_ip_from 197.234.240.0/22; set_real_ip_from 198.41.128.0/17; set_real_ip_from 162.158.0.0/15; set_real_ip_from 104.16.0.0/13; set_real_ip_from 104.24.0.0/14; set_real_ip_from 172.64.0.0/13; set_real_ip_from 131.0.72.0/22; # Cloudflare IPv6 ranges. set_real_ip_from 2a06:98c0::/29; set_real_ip_from 2606:4700::/32; set_real_ip_from 2803:f800::/32; set_real_ip_from 2405:b500::/32; set_real_ip_from 2405:8100::/32; set_real_ip_from 2c0f:f248::/32; real_ip_header CF-Connecting-IP; # Only redirect to HTTPS for non-local hosts, so local dev on :80 keeps working. # nginx map keys support regexes, which is how the RFC1918 ranges are matched. # Every regex is anchored with $ so a hostname like 127.0.0.1.evil.com cannot # match a private-IP prefix and bypass the HTTPS redirect. map $host $ssl_redirect { default 1; ~^localhost$ 0; ~^127\.\d+\.\d+\.\d+$ 0; ~^10\.\d+\.\d+\.\d+$ 0; ~^192\.168\.\d+\.\d+$ 0; ~^172\.(1[6-9]|2[0-9]|3[01])\.\d+\.\d+$ 0; ~^\[::1\]$ 0; } # Port 80: serve normally for localhost/private hosts, redirect everything else server { listen 80; server_name _; # Non-local hosts are forced to HTTPS; local/private hosts fall through # and are served normally below. if ($ssl_redirect) { return 301 https://$host$request_uri; } # Security headers add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header X-XSS-Protection "1; mode=block"; # Square Web Payments SDK: script from *.squarecdn.com, card-entry iframe # from js.squareup.com (frame-src; without it the payment form cannot # tokenize behind this proxy). connect-src allows the SDK's own network calls. # The SvelteKit SPA emits one inline bootstrap