# 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;
}