# Dangerous Pi - Nginx reverse proxy configuration with HTTPS # Serves frontend on / and proxies /api/*, /ws/* to backend # SSL termination at nginx, backend remains HTTP upstream frontend { server 127.0.0.1:3000; } upstream backend { server 127.0.0.1:8000; } # HTTP server - captive portal detection + redirect to HTTPS server { listen 80 default_server; listen [::]:80 default_server; server_name dangerous-pi.local _; # =========================================== # CAPTIVE PORTAL DETECTION (must stay on HTTP) # =========================================== # OS connectivity checks don't follow HTTPS redirects. # These endpoints must respond on HTTP for captive portal to work. # Android / Chrome OS connectivity check location = /generate_204 { return 204; } # Additional Android check paths location = /gen_204 { return 204; } # Google connectivity check location = /connectivitycheck/gstatic/generate_204 { return 204; } # iOS / macOS captive portal detection location = /hotspot-detect.html { return 200 'SuccessSuccess'; add_header Content-Type text/html; } # iOS alternate path location = /library/test/success.html { return 200 'SuccessSuccess'; add_header Content-Type text/html; } # Windows 10/11 connectivity check location = /connecttest.txt { return 200 'Microsoft Connect Test'; add_header Content-Type text/plain; } # Windows NCSI check location = /ncsi.txt { return 200 'Microsoft NCSI'; add_header Content-Type text/plain; } # Firefox / Mozilla connectivity check location = /success.txt { return 200 'success'; add_header Content-Type text/plain; } # Kindle / Amazon devices location = /kindle-wifi/wifistub.html { return 200 'Kindle81ce4465-7167-4dcb-835b-dcc9e44c112a'; add_header Content-Type text/html; } # =========================================== # END CAPTIVE PORTAL DETECTION # =========================================== # Redirect all other HTTP traffic to the main HTTPS page # IMPORTANT: For captive portals, we must redirect to OUR domain (not $host) # because the browser may be requesting a random domain like detectportal.firefox.com # and our SSL cert only covers dangerous-pi.local and 192.168.4.1 location / { return 302 https://192.168.4.1/; } } # HTTPS server - main application server { listen 443 ssl default_server; listen [::]:443 ssl default_server; http2 on; server_name dangerous-pi.local _; # SSL certificate configuration ssl_certificate /opt/dangerous-pi/ssl/dangerous-pi.crt; ssl_certificate_key /opt/dangerous-pi/ssl/dangerous-pi.key; # SSL security settings (modern configuration) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # Proxy API requests to FastAPI backend location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; proxy_connect_timeout 75s; } # Proxy WebSocket connections to backend # nginx handles SSL termination, so wss:// becomes ws:// to backend location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # Everything else goes to Remix frontend location / { proxy_pass http://frontend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_cache_bypass $http_upgrade; } # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # HSTS: Tell browsers to always use HTTPS (1 year) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Gzip compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml; }