1# ============================================================================= 2# AI Software Factory — hardened nginx reverse proxy 3# 4# Purpose: single entry point in front of the docker-compose app stack 5# (web :3000, api :4000, realtime :4001). See README.md in this directory. 6# 7# Layout: 8# :80 → redirect everything to https 9# :443 → TLS termination, then: 10# / → web (Next.js) 11# /api/ → api (NestJS, rate-limited, 512m upload cap) 12# /saml/ → api (SAML SP: metadata + start + ACS — TASK-0131) 13# /sso/ → api (OIDC RP: authorize + callback + start dispatcher) 14# /ws → realtime (WebSocket upgrade, 1h read timeout) 15# /health → 200 (LB health probe, does not touch the apps) 16# ============================================================================= 17 18user nginx; 19worker_processes auto; 20 21# Failures logged with a pid so they can be correlated. 22error_log /var/log/nginx/error.log warn; 23pid /var/run/nginx.pid; 24 25events { 26 worker_connections 4096; 27 multi_accept on; 28} 29 30http { 31 include /etc/nginx/mime.types; 32 default_type application/octet-stream; 33 34 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 35 '$status $body_bytes_sent "$http_referer" ' 36 '"$http_user_agent" "$http_x_forwarded_for" ' 37 'rt=$request_time uct=$upstream_connect_time ' 38 'uht=$upstream_header_time urt=$upstream_response_time'; 39 40 access_log /var/log/nginx/access.log main buffer=32k flush=5s; 41 42 # --- Performance --------------------------------------------------------- 43 sendfile on; 44 tcp_nopush on; 45 tcp_nodelay on; 46 keepalive_timeout 65; 47 keepalive_requests 10000; 48 types_hash_max_size 2048; 49 server_tokens off; # do not advertise nginx version 50 51 client_body_timeout 120s; 52 send_timeout 120s; 53 client_max_body_size 512m; # file uploads via /api/ (see location) 54 client_body_buffer_size 128k; 55 56 # --- Gzip ------------------------------------------------------------------ 57 # Note: for the web location we proxy gzip on to Next.js (which already 58 # compresses); the rules below cover static responses and /api JSON. 59 gzip on; 60 gzip_vary on; 61 gzip_min_length 1024; 62 gzip_proxied any; 63 gzip_comp_level 5; 64 gzip_types 65 text/plain 66 text/css 67 text/xml 68 text/javascript 69 application/javascript 70 application/x-javascript 71 application/json 72 application/xml 73 application/wasm 74 image/svg+xml 75 font/ttf 76 font/otf 77 font/woff 78 font/woff2; 79 80 # --- Proxy defaults (inherited by all locations) --------------------------- 81 proxy_http_version 1.1; 82 proxy_set_header Host $host; 83 proxy_set_header X-Real-IP $remote_addr; 84 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 85 proxy_set_header X-Forwarded-Proto $scheme; 86 proxy_set_header X-Forwarded-Host $host; 87 proxy_set_header Connection ""; 88 proxy_connect_timeout 5s; 89 proxy_send_timeout 60s; 90 proxy_read_timeout 60s; 91 proxy_buffering on; 92 proxy_buffers 16 32k; 93 proxy_busy_buffers_size 64k; 94 95 # --- Rate limit zones -------------------------------------------------------- 96 # api: 30 req/s per client IP (burst 60, nodelay) — normal browsing + 97 # editor autosave stays well under this; abuse does not. 98 # auth: 10 req/min per client IP for /api/v1/auth/* — registration, 99 # login, password reset, refresh. Complements (does not replace) 100 # the API's own rate limiting; this is the edge tripwire. 101 limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; 102 limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; 103 limit_req_status 429; 104 105 # Concurrent connection cap per IP (hammer protection for /ws). 106 limit_conn_zone $binary_remote_addr zone=connperip:10m; 107 limit_conn_status 429; 108 109 # --- Upstreams ---------------------------------------------------------------- 110 # docker-compose service names resolve on the compose network when nginx 111 # runs as a compose service; for a host-level nginx in front of 112 # localhost-published ports, swap the names for 127.0.0.1 (the README 113 # shows the exact diff). 114 upstream web_upstream { 115 server web:3000; 116 keepalive 32; 117 } 118 119 upstream api_upstream { 120 server api:4000; 121 keepalive 32; 122 } 123 124 upstream realtime_upstream { 125 # Long-lived WS connections: generous timeouts. 126 server realtime:4001; 127 keepalive 16; 128 keepalive_requests 0; 129 } 130 131 # --- Security headers (applied to every response) ----------------------------- 132 # 133 # Why these (and nothing more exotic): 134 # CSP default-src 'self' — no third-party scripts/styles/fonts by 135 # default; every factory surface is same-origin. 136 # script-src 'self' — Next.js bundles are same-origin. (Inline 137 # scripts are NOT allowed: Next.js inlines 138 # nothing by default in production.) 139 # style-src 'self' 'unsafe-inline' — Next.js inline <style> tags (CSS-in-JS 140 # and the initial theme style) require it. 141 # img-src 'self' data: blob: https: — same-origin assets + data: URIs for 142 # in-canvas thumbnails; https: allows 143 # avatar/file previews from other origins. 144 # font-src 'self' data: — bundled + data: fonts. 145 # connect-src 'self' ws: wss: https: — XHR/fetch same-origin, realtime 146 # WebSocket (ws/wss), and https APIs 147 # (AI provider is server-side, never the 148 # browser). 149 # frame-src 'self' https: — embedded preview panes (PDF/video if 150 # served from another origin). 151 # base-uri 'self' — prevent <base> injection hijacking. 152 # frame-ancestors 'none' — clickjacking protection (we are never 153 # an iframe; the web app is the frame). 154 # object-src 'none' — no <object>/<embed> plugins. 155 # 156 # NOTE: if you run a third-party widget (analytics, chat) in the future, 157 # extend the relevant -src directives BEFORE releasing, or the browser 158 # will block it (and log CSP violations). 159 160 # --- Servers --------------------------------------------------------------------- 161 162 # HTTP → HTTPS redirect (HSTS is set on the 443 server only, as required). 163 server { 164 listen 80; 165 listen [::]:80; 166 server_name _; 167 168 # Let ACME/Let's Encrypt finish HTTP-01 challenges if used (certbot 169 # note in README.md); everything else redirects. 170 location ^~ /.well-known/acme-challenge/ { 171 root /var/www/certbot; 172 } 173 174 location / { 175 return 301 https://$host$request_uri; 176 } 177 } 178 179 # HTTPS entry point. 180 server { 181 listen 443 ssl; 182 listen [::]:443 ssl; 183 http2 on; 184 server_name _; 185 186 # --- TLS (placeholder paths — point at your cert, e.g. certbot's) --- 187 ssl_certificate /etc/nginx/ssl/factory.example.com.fullchain.pem; 188 ssl_certificate_key /etc/nginx/ssl/factory.example.com.privkey.pem; 189 190 ssl_protocols TLSv1.2 TLSv1.3; 191 ssl_ciphers HIGH:!aNULL:!MD5; 192 ssl_prefer_server_ciphers on; 193 ssl_session_cache shared:SSL:10m; 194 ssl_session_timeout 1d; 195 ssl_session_tickets off; 196 197 # --- Headers ----------------------------------------------------------- 198 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; 199 add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' ws: wss: https:; frame-src 'self' https:; base-uri 'self'; frame-ancestors 'none'; object-src 'none'" always; 200 add_header X-Content-Type-Options "nosniff" always; 201 add_header X-Frame-Options "DENY" always; 202 add_header Referrer-Policy "strict-origin-when-cross-origin" always; 203 add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), fullscreen=(self)" always; 204 # X-Frame-Options DENY + CSP frame-ancestors 'none' are redundant on 205 # modern browsers; keep both for older clients. 206 207 # --- LB health check (does not hit the apps) --------------------------- 208 location = /health { 209 access_log off; 210 default_type text/plain; 211 return 200 "ok"; 212 } 213 214 # --- Web app (Next.js) --------------------------------------------------- 215 location / { 216 proxy_pass http://web_upstream; 217 218 # Next.js compresses its own responses; disable nginx gzip here 219 # to avoid double-compression. 220 gzip off; 221 222 # Long poll / HMR-free but keep reasonable timeouts for the 223 # occasional slow SSR page. 224 proxy_read_timeout 120s; 225 } 226 227 # --- API (NestJS) --------------------------------------------------------- 228 location /api/ { 229 limit_req zone=api burst=60 nodelay; 230 231 # Auth endpoints: tighter edge limit (10/min/IP). 232 location /api/v1/auth/ { 233 limit_req zone=auth burst=3 nodelay; 234 proxy_pass http://api_upstream; 235 } 236 237 proxy_pass http://api_upstream; 238 239 # File uploads (multipart + presigned passthrough). 240 client_max_body_size 512m; 241 proxy_request_buffering off; # stream large uploads to the API 242 243 # Long-running batch operations (import, export, bulk). 244 proxy_read_timeout 300s; 245 } 246 247 # --- SSO protocol surface (SAML SP + OIDC RP + start dispatcher) ------------- 248 # TASK-0131: the SSO browser bindings are served by the API but are 249 # mounted OUTSIDE the `api/v1` prefix (they are 302/form-POST 250 # browser bindings, not the JSON v1 API), so their paths are 251 # `/saml/*` + `/sso/*` — NOT under `/api/`. Without these locations 252 # they would fall through to `location /` (web/Next.js) and 404. 253 # The IdP's POST to the SAML ACS and the browser's start/authorize 254 # GETs all land here. 255 location /saml/ { 256 limit_req zone=api burst=60 nodelay; 257 proxy_pass http://api_upstream; 258 } 259 260 location /sso/ { 261 limit_req zone=api burst=60 nodelay; 262 proxy_pass http://api_upstream; 263 } 264 265 # --- Realtime (WebSocket) --------------------------------------------------- 266 location /ws { 267 limit_conn connperip 50; 268 269 proxy_pass http://realtime_upstream; 270 271 # WebSocket upgrade handshake. 272 proxy_http_version 1.1; 273 proxy_set_header Upgrade $http_upgrade; 274 proxy_set_header Connection "upgrade"; 275 276 # Long-lived connections: 1 hour idle timeout, no buffering. 277 proxy_read_timeout 3600s; 278 proxy_send_timeout 3600s; 279 proxy_buffering off; 280 } 281 282 # --- Deny anything that looks like a secret or dotfile ---------------------- 283 location ~ /\.(?!well-known) { 284 deny all; 285 } 286 } 287} 288