# Cipher Barcode Studio — hardened nginx configuration # Static-only site, no upstream, no PHP, no dynamic code. Read-only filesystem. worker_processes auto; pid /tmp/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Temp paths in /tmp so we can run as a non-root user with a read-only rootfs client_body_temp_path /tmp/client_body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; uwsgi_temp_path /tmp/uwsgi; scgi_temp_path /tmp/scgi; # Logging log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent"'; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; # Performance sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 30; server_tokens off; # hide nginx version # Limits — this is a static-only site, so cap request size strictly client_max_body_size 1k; client_body_buffer_size 1k; client_header_buffer_size 1k; large_client_header_buffers 2 4k; # Compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 256; gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml font/woff2; server { listen 8080 default_server; listen [::]:8080 default_server; server_name _; root /usr/share/nginx/html; index index.html; # ---------- Security headers ---------- # Static-only site: tight CSP. Inline styles/scripts are unavoidable # because the app is one self-contained HTML file, but external scripts # are restricted to 'self' and validated with SRI in the markup. add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always; # Block framing entirely (defense-in-depth alongside CSP frame-ancestors) add_header X-Frame-Options "DENY" always; # Stop MIME-type sniffing add_header X-Content-Type-Options "nosniff" always; # Disable unused browser features add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=(), usb=()" always; # Don't leak referrer to third-party fonts add_header Referrer-Policy "strict-origin-when-cross-origin" always; # HSTS — only meaningful behind HTTPS; harmless on HTTP add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Cross-origin isolation add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always; # ---------- Routes ---------- # Hash-able vendor assets get long-lived caches location /vendor/ { add_header Cache-Control "public, max-age=2592000, immutable" always; # Re-apply security headers (add_header doesn't inherit across location blocks) add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; try_files $uri =404; } # The main HTML — short cache so updates roll out location = / { add_header Cache-Control "public, max-age=300, must-revalidate" always; try_files /index.html =404; } # Healthcheck endpoint for docker / orchestrators location = /healthz { access_log off; add_header Content-Type text/plain; return 200 "ok\n"; } # Block hidden files and common sensitive paths location ~ /\. { deny all; return 404; } location / { try_files $uri $uri/ =404; } # Custom error pages error_page 404 /index.html; error_page 500 502 503 504 /index.html; } }