Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
Major Feature Additions: - Customer management: Full CRUD with data export and privacy compliance - Payment management: Centralized payment tracking and administration - Push notification subscriptions: Manage and track web push subscriptions Security Enhancements: - IP whitelist middleware for administrative endpoints - Data retention service with configurable policies - Enhanced push notification security documentation - Security fixes progress tracking (2025-11-14) UI/UX Improvements: - Enhanced navigation with improved mobile responsiveness - Updated admin dashboard with order status counts - Improved product CRUD forms - New customer and payment management interfaces Backend Improvements: - Extended customer service with data export capabilities - Enhanced order service with status count queries - Improved crypto payment service with better error handling - Updated validators and configuration Documentation: - DEPLOYMENT_NGINX_GUIDE.md: Nginx deployment instructions - IP_STORAGE_ANALYSIS.md: IP storage security analysis - PUSH_NOTIFICATION_SECURITY.md: Push notification security guide - UI_UX_IMPROVEMENT_PLAN.md: Planned UI/UX enhancements - UI_UX_IMPROVEMENTS_COMPLETED.md: Completed improvements Cleanup: - Removed temporary database WAL files - Removed stale commit message file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
449 lines
12 KiB
Markdown
449 lines
12 KiB
Markdown
# 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=<guid> \
|
|
-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 = ^<HOST> .* "GET /Admin/
|
|
^<HOST> .* "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
|
|
```
|