worker_processes auto; events { worker_connections 1024; } http { resolver 127.0.0.11 valid=10s; # Define a rate limit zone to prevent abuse limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; # Map Authorization header to a token variable for sticky sessions map $http_authorization $auth_token { "~*Bearer\s+(.*)" $1; default $remote_addr; # Fallback to IP hash if no token is provided } # Upstream with hash-based load balancing (Sticky Sessions) # The hash uses the user's API Key so the same API Key always hits the same node, # preserving the cascade_id context in memory. upstream windsurf_backend { hash $auth_token consistent; # In a real Swarm/Compose environment with scale=3, Docker DNS resolves 'windsurf-api' to all IPs. # But 'hash' works better with explicit servers or using a DNS resolver dynamically if needed. # Nginx free version requires manual IP listings for sticky sessions, # or we rely on ip_hash for generic load balancing if behind a single reverse proxy. zone windsurf_backend_zone 64k; server windsurf-api:3003 resolve; } # SEC-02: access logging without the query string. `?key=` is an accepted # auth carrier (Gemini-native clients; see extractToken in src/server.js) # and nginx's default `combined` format logs $request_uri verbatim, so API # keys would land in `docker logs`, log shippers and support bundles. # $uri is the normalized path only — every query parameter is dropped. If # you need query-level debugging, log a dedicated non-secret parameter # instead of restoring $request_uri. # SEC-02b: the Referer header is not logged either. A Referer is itself a # URL, so a link from a page opened with `?key=` copies the query into the # access log even though the request line is clean. A fixed marker (or # nothing) is the only safe shape. log_format main_no_query '$remote_addr - $remote_user [$time_local] ' '"$request_method $uri $server_protocol" $status $body_bytes_sent ' '"$http_user_agent"'; server { listen 80; # Apply the query-free format to every location in this server. access_log /var/log/nginx/access.log main_no_query; # Allow large payloads for multimodal images / base64 payloads client_max_body_size 32m; location / { limit_req zone=mylimit burst=20 nodelay; proxy_pass http://windsurf_backend; proxy_http_version 1.1; # Timeouts for long-running streaming completions and thinking models # 必须严格大于应用层 DEVIN_TIMEOUT_MS(默认 600s)。改任意一边都要重新核对。 proxy_connect_timeout 60s; proxy_send_timeout 900s; proxy_read_timeout 900s; # SSE and streaming requirements proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; chunked_transfer_encoding on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # D1 (audit2/Server.md S01): overwrite, never forward, a # client-supplied X-Forwarded-Host. The app honours a trusted # forwarded Host only if it passes DASHBOARD_ALLOWED_HOSTS, but the # bundled proxy should not hand it attacker text in the first place. proxy_set_header X-Forwarded-Host $host; } } }