server { listen 8080; root /usr/share/nginx/html; index index.html; # ── Security headers ───────────────────────────────────────────────────── 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 "camera=(), microphone=(), geolocation=()" always; # CSP for a Vue 3 / Vite SPA: # - script-src 'self' + hash : Vite bundles (self) + one Vite-generated inline # script (modulepreload fallback in index.html, stable hash across builds) # - style-src 'self' 'unsafe-inline' : inline style="" attributes used by Vue # - img-src 'self' data: : SVG/PNG assets + possible data URIs # - connect-src 'self' : API calls to /api/ (same origin via proxy) # - object-src 'none' : no plugins # - frame-ancestors 'none' : prevents embedding in iframes (replaces X-Frame-Options for CSP-aware browsers) add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-ZswfTY7H35rbv8WC7NXBoiC7WNu86vSzCDChNWwZZDM='; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self';" always; location = /api/auth/login { limit_req zone=login burst=5 nodelay; limit_req_status 429; proxy_pass http://backend:8000/api/auth/login; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } location /api/ { proxy_pass http://backend:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } location / { try_files $uri $uri/ /index.html; } }