upstream backend_load_balancer { server 127.0.0.1:3004; server 127.0.0.1:3005; server 127.0.0.1:3006; } # Don't leak the nginx version in Server: headers or error pages. server_tokens off; # Shared proxy settings, reused by the HTTP and HTTPS servers below. # X-Real-IP must be $remote_addr (the actual TCP peer), not $host # (the attacker-controlled Host header) - downstream rate-limiting and # audit logging key off this value. map $http_x_forwarded_proto $proxied_scheme { default $scheme; https https; } server { listen 80; # If TLS is terminated upstream (Cloudflare, AWS ALB, etc.), keep this # block as the proxy. If you terminate TLS here, add a separate 443 # server (template below) and uncomment the redirect. # # return 301 https://$host$request_uri; # Reject body uploads larger than the avatar/PDF cap. Without this, # default nginx 1MB limit would silently 413 valid 5MB uploads, and # an attacker could DoS the backend with unbounded payloads. client_max_body_size 16m; location / { proxy_pass http://backend_load_balancer; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxied_scheme; # Defense-in-depth security headers. Most of these are also # appropriate to set in next.config.ts; setting them here means # they apply to backend responses (Swagger, raw API) too. add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # CSP for the API surface only - this server proxies the NestJS # backend, whose responses (JSON, Swagger HTML, error pages) don't # need to embed anything cross-origin. The frontend's CSP should be # configured separately in next.config.ts where it can be tuned to # the actual asset origins (Supabase storage, etc.). # # frame-ancestors 'none' duplicates X-Frame-Options for browsers # that don't respect the latter; the rest is a strict default. add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'" always; } } # Template for terminating TLS at nginx. Uncomment and fill in cert paths # once a certificate is provisioned for the hostname. Until then, leave # TLS to whatever's in front of nginx (Cloudflare, ALB, etc.). # # server { # listen 443 ssl http2; # server_name your.hostname.example; # # ssl_certificate /etc/letsencrypt/live/your.hostname.example/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/your.hostname.example/privkey.pem; # ssl_protocols TLSv1.2 TLSv1.3; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers off; # # # Tell browsers to never hit :80 again for this host. # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # # client_max_body_size 16m; # # location / { # proxy_pass http://backend_load_balancer; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-Proto https; # # add_header X-Content-Type-Options "nosniff" always; # add_header X-Frame-Options "DENY" always; # add_header Referrer-Policy "strict-origin-when-cross-origin" always; # add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # } # }