# Nginx Deployment Guide - Push Notification Security This guide explains how to properly configure nginx to isolate the admin panel while keeping push notifications functional. ## Architecture Overview ``` Internet │ ├─── api.littleshop.com (Public API) │ └─── Only: /api/push/vapid-key, /api/push/subscribe/customer, /api/push/unsubscribe │ └─── admin.dark.side (LAN-Only Admin Panel) └─── Full access: /Admin/*, /api/*, /blazor/* LittleShop Backend (littleshop:5000) ``` ## Complete Nginx Configuration ### Step 1: Create upstream backend ```nginx # /etc/nginx/conf.d/littleshop-upstream.conf upstream littleshop_backend { server littleshop:5000; keepalive 32; } ``` ### Step 2: Public API Server (Internet-Accessible) ```nginx # /etc/nginx/sites-available/littleshop-public-api.conf # Rate limiting zones limit_req_zone $binary_remote_addr zone=push_vapid:10m rate=60r/m; limit_req_zone $binary_remote_addr zone=push_subscribe:10m rate=10r/m; limit_req_zone $binary_remote_addr zone=push_unsubscribe:10m rate=20r/m; server { listen 80; listen [::]:80; server_name api.littleshop.com; # Redirect to HTTPS return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.littleshop.com; # SSL configuration ssl_certificate /etc/letsencrypt/live/api.littleshop.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.littleshop.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Security headers add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # VAPID public key endpoint (required for push notifications) location = /api/push/vapid-key { limit_req zone=push_vapid burst=10; proxy_pass http://littleshop_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; # CORS headers for push notifications add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; # Preflight requests if ($request_method = OPTIONS) { return 204; } } # Customer subscription endpoint location = /api/push/subscribe/customer { limit_req zone=push_subscribe burst=5; proxy_pass http://littleshop_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; # CORS headers add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; if ($request_method = OPTIONS) { return 204; } } # Unsubscribe endpoint location = /api/push/unsubscribe { limit_req zone=push_unsubscribe burst=10; proxy_pass http://littleshop_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; # CORS headers add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; if ($request_method = OPTIONS) { return 204; } } # Block all other endpoints location / { return 403 '{"error": "Access denied", "message": "Admin access is restricted to authorized networks"}'; add_header Content-Type application/json; } # Custom error pages error_page 403 /403.json; location = /403.json { internal; return 403 '{"error": "Access denied", "message": "This endpoint is not publicly accessible"}'; add_header Content-Type application/json; } } ``` ### Step 3: Admin Panel Server (LAN-Only) ```nginx # /etc/nginx/sites-available/littleshop-admin.conf server { listen 80; listen [::]:80; server_name admin.dark.side admin.littleshop.local; # Redirect to HTTPS return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name admin.dark.side admin.littleshop.local; # SSL configuration ssl_certificate /etc/letsencrypt/live/admin.dark.side/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/admin.dark.side/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # LAN-only IP whitelist # Private IPv4 ranges allow 10.0.0.0/8; # Class A private network allow 172.16.0.0/12; # Class B private network allow 192.168.0.0/16; # Class C private network allow 127.0.0.1; # Localhost # Private IPv6 ranges allow fc00::/7; # Unique local addresses allow ::1; # IPv6 localhost # Add your specific office/VPN IPs here # allow 203.0.113.50; # Example: Office static IP # allow 198.51.100.0/24; # Example: VPN subnet # Deny all other IPs deny all; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always; # Proxy settings proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 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_cache_bypass $http_upgrade; proxy_buffering off; # Admin panel location /Admin/ { proxy_pass http://littleshop_backend; } # API endpoints location /api/ { proxy_pass http://littleshop_backend; } # Blazor location /blazor/ { proxy_pass http://littleshop_backend; } # SignalR hubs location /activityHub { proxy_pass http://littleshop_backend; } location /notificationHub { proxy_pass http://littleshop_backend; } # Static files location /css/ { proxy_pass http://littleshop_backend; } location /js/ { proxy_pass http://littleshop_backend; } location /lib/ { proxy_pass http://littleshop_backend; } location /uploads/ { proxy_pass http://littleshop_backend; } # Favicon and manifest location ~ ^/(favicon\.ico|site\.webmanifest|manifest\.json)$ { proxy_pass http://littleshop_backend; } # Health check endpoint location /health { proxy_pass http://littleshop_backend; access_log off; } # Root location / { proxy_pass http://littleshop_backend; } # Logging access_log /var/log/nginx/littleshop-admin-access.log; error_log /var/log/nginx/littleshop-admin-error.log warn; } ``` ### Step 4: Enable sites ```bash # Create symbolic links sudo ln -s /etc/nginx/sites-available/littleshop-public-api.conf /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/littleshop-admin.conf /etc/nginx/sites-enabled/ # Test configuration sudo nginx -t # Reload nginx sudo systemctl reload nginx ``` ## Testing the Configuration ### Test Public Endpoints (Should Work) ```bash # VAPID key endpoint curl https://api.littleshop.com/api/push/vapid-key # Customer subscription (with valid customerId) curl -X POST https://api.littleshop.com/api/push/subscribe/customer?customerId= \ -H "Content-Type: application/json" \ -d '{"endpoint":"...","keys":{"p256dh":"...","auth":"..."}}' # Unsubscribe curl -X POST https://api.littleshop.com/api/push/unsubscribe \ -H "Content-Type: application/json" \ -d '{"endpoint":"..."}' ``` ### Test Blocked Endpoints (Should Return 403) ```bash # Admin API (should be blocked from internet) curl https://api.littleshop.com/api/push/subscribe # Admin panel (should be blocked from internet) curl https://api.littleshop.com/Admin/Dashboard ``` ### Test Admin Panel from LAN (Should Work) ```bash # From inside LAN curl https://admin.dark.side/Admin/Dashboard curl https://admin.dark.side/api/push/subscriptions ``` ## Docker Compose Integration If using Docker Compose with Nginx Proxy Manager: ```yaml # docker-compose.yml version: '3.8' services: littleshop: image: littleshop:latest container_name: littleshop networks: - littleshop-network - nginx-proxy-network environment: - ASPNETCORE_URLS=http://+:5000 ports: - "5000:5000" # Only accessible from docker network nginx-proxy-manager: image: 'jc21/nginx-proxy-manager:latest' container_name: nginx-proxy-manager networks: - nginx-proxy-network ports: - '80:80' - '443:443' - '81:81' # Admin UI volumes: - ./nginx-data:/data - ./nginx-letsencrypt:/etc/letsencrypt networks: littleshop-network: driver: bridge nginx-proxy-network: driver: bridge ``` ## Monitoring and Alerting ### Monitor Failed Access Attempts ```nginx # Add to admin server block location /Admin/ { access_log /var/log/nginx/admin-access.log combined; error_log /var/log/nginx/admin-error.log warn; # Log denied IPs if ($remote_addr !~* "^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)") { access_log /var/log/nginx/admin-denied.log combined; } proxy_pass http://littleshop_backend; } ``` ### Set up fail2ban for repeated access attempts ```ini # /etc/fail2ban/filter.d/nginx-littleshop-admin.conf [Definition] failregex = ^ .* "GET /Admin/ ^ .* "POST /api/push/(subscribe|test|broadcast) ignoreregex = ``` ```ini # /etc/fail2ban/jail.local [nginx-littleshop-admin] enabled = true port = http,https filter = nginx-littleshop-admin logpath = /var/log/nginx/admin-denied.log maxretry = 5 bantime = 3600 findtime = 600 ``` ## Production Checklist - [ ] SSL certificates installed and configured - [ ] Firewall rules updated to allow 80/443 - [ ] Rate limiting configured for public endpoints - [ ] IP whitelist configured for admin panel - [ ] Monitoring and logging enabled - [ ] fail2ban configured for intrusion detection - [ ] Health checks working - [ ] DNS records pointing to correct servers - [ ] Backup procedures in place - [ ] Team has VPN access for admin panel ## Troubleshooting ### Issue: "502 Bad Gateway" **Solution**: Check that LittleShop backend is running on port 5000 ```bash docker ps | grep littleshop curl http://localhost:5000/health ``` ### Issue: "403 Forbidden" from LAN **Solution**: Check IP whitelist includes your LAN subnet ```bash # Check your IP ip addr show # Or on Windows ipconfig ``` ### Issue: Push notifications not working **Solution**: Verify public endpoints are accessible ```bash curl -v https://api.littleshop.com/api/push/vapid-key # Should return 200 OK with public key ``` ### Issue: CORS errors in browser **Solution**: Check CORS headers are present in nginx config ```bash curl -H "Origin: https://example.com" https://api.littleshop.com/api/push/vapid-key -v # Look for Access-Control-Allow-Origin header ```