# Work Package Suite — NGINX site config # This container sits behind an external reverse proxy that handles SSL. # It listens on port 80 (plain HTTP on the internal Docker network). # Cache-Control per file type, computed here rather than in a nested location. # WHY A MAP: nginx's add_header is not inherited into a block that declares its own # add_header — a `location ~* \.(html|css|js)$` that set only Cache-Control would have # silently dropped the CSP / HSTS / X-Frame-Options / nosniff headers below for exactly # those files. Computing the value here keeps every header in ONE scope. An empty value # means nginx omits the header entirely, so images and fonts stay freely cacheable. # # Code assets must revalidate on every load: with no Cache-Control at all the browser # applies HEURISTIC freshness (~10% of the file's age), so the least recently changed # file gets the LONGEST lifetime — which is how a page ends up running against a # stylesheet or script from a previous deploy. ETag/Last-Modified keep it a cheap 304. map $uri $wp_cache_control { default ""; ~*\.(?:html|css|js|webmanifest)$ "no-cache"; ~*/$ "no-cache"; # directory index → index.html } server { listen 80; server_name wp.controls.dev; root /usr/share/nginx/html; index index.html; # ── Security response headers (defense-in-depth) ───────────────────────── # CSP keeps 'unsafe-inline' for now because the app uses inline handlers/styles # heavily; even so, connect-src/img-src/object-src/base-uri/frame-ancestors # sharply limit what injected script could load or exfiltrate. Tighten toward # nonce-based scripts once inline handlers are refactored. add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "no-referrer" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'" always; # Empty for anything that isn't code, in which case nginx omits the header. add_header Cache-Control $wp_cache_control always; location / { try_files $uri $uri/ =404; } # Proxy /api/ to the FastAPI container (service name "api" on the internal network) location /api/ { proxy_pass http://api:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; # This container is only ever reached via the TLS-terminating external # proxy, so the real client scheme is HTTPS. Hard-set it (a local $scheme # here is always "http") so the API marks the session cookie Secure. proxy_set_header X-Forwarded-Proto https; client_max_body_size 5m; } }