This commit is contained in:
Iliyan Angelov
2025-11-24 08:42:03 +02:00
parent 136f75a859
commit d7ff5c71e6
15 changed files with 697 additions and 43 deletions

View File

@@ -0,0 +1,145 @@
"""
API Security Middleware
Validates that API requests come from the frontend through nginx reverse proxy
This ensures the API is only accessible from the frontend, not directly from the internet
"""
from django.http import HttpResponseForbidden, JsonResponse
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
import logging
import re
logger = logging.getLogger('django.security')
class FrontendAPIProxyMiddleware(MiddlewareMixin):
"""
Enterprise Security: Only allow API requests from frontend through nginx
This middleware validates:
1. Custom header (X-Internal-API-Key) that nginx adds to prove request came through proxy
2. Origin/Referer header matches allowed frontend domains
3. Request comes from internal network (nginx proxy)
In production, this ensures the API cannot be accessed directly from the internet.
All requests must come through the frontend (nginx reverse proxy).
"""
def __init__(self, get_response):
self.get_response = get_response
super().__init__(get_response)
# Get API key from settings (nginx will add this header)
self.required_api_key = getattr(settings, 'INTERNAL_API_KEY', None)
# Get allowed frontend origins
self.allowed_origins = getattr(settings, 'CORS_ALLOWED_ORIGINS', [])
# Get allowed referer patterns
self.allowed_referer_patterns = getattr(settings, 'ALLOWED_REFERER_PATTERNS', [])
# API paths that require validation (default: all /api/ paths)
self.api_path_pattern = getattr(settings, 'API_SECURITY_PATH_PATTERN', r'^/api/')
# Skip validation in DEBUG mode for development
self.enforce_in_debug = getattr(settings, 'ENFORCE_API_SECURITY_IN_DEBUG', False)
def process_request(self, request):
"""
Validate API requests come from frontend through nginx
"""
# Skip validation for non-API paths
if not re.match(self.api_path_pattern, request.path):
return None
# Skip validation in DEBUG mode unless explicitly enabled
if settings.DEBUG and not self.enforce_in_debug:
return None
# Skip validation for OPTIONS requests (CORS preflight)
if request.method == 'OPTIONS':
return None
# Get the internal API key from header (added by nginx)
internal_api_key = request.META.get('HTTP_X_INTERNAL_API_KEY', '')
# Validate API key if configured
if self.required_api_key:
if internal_api_key != self.required_api_key:
logger.warning(
f"Blocked API request: Missing or invalid X-Internal-API-Key header. "
f"Path: {request.path}, IP: {self._get_client_ip(request)}"
)
return JsonResponse(
{
'error': 'Access Denied',
'message': 'This API is only accessible through the frontend application.',
'code': 'API_ACCESS_DENIED'
},
status=403
)
# Validate Origin header (for CORS requests)
origin = request.META.get('HTTP_ORIGIN', '')
if origin:
if origin not in self.allowed_origins:
logger.warning(
f"Blocked API request: Invalid Origin header. "
f"Origin: {origin}, Path: {request.path}, IP: {self._get_client_ip(request)}"
)
return JsonResponse(
{
'error': 'Access Denied',
'message': 'Invalid origin. This API is only accessible from authorized domains.',
'code': 'INVALID_ORIGIN'
},
status=403
)
# Validate Referer header (for same-origin requests)
referer = request.META.get('HTTP_REFERER', '')
if referer and not origin: # Only check referer if no origin header
is_valid_referer = False
# Check against allowed origins
for allowed_origin in self.allowed_origins:
if referer.startswith(allowed_origin):
is_valid_referer = True
break
# Check against referer patterns
if not is_valid_referer and self.allowed_referer_patterns:
for pattern in self.allowed_referer_patterns:
if re.match(pattern, referer):
is_valid_referer = True
break
if not is_valid_referer:
logger.warning(
f"Blocked API request: Invalid Referer header. "
f"Referer: {referer}, Path: {request.path}, IP: {self._get_client_ip(request)}"
)
return JsonResponse(
{
'error': 'Access Denied',
'message': 'Invalid referer. This API is only accessible from authorized domains.',
'code': 'INVALID_REFERER'
},
status=403
)
# Additional validation: Check if request comes from nginx (internal network)
# This is a secondary check - the IP whitelist middleware should handle this,
# but we can add additional validation here if needed
# Request is valid, continue processing
return None
def _get_client_ip(self, request):
"""Get client IP address from request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', 'unknown')

View File

@@ -62,6 +62,7 @@ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'gnx.middleware.ip_whitelist.IPWhitelistMiddleware', # Production: Block external access
'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -151,7 +152,7 @@ CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_TRUSTED_ORIGINS = config(
'CSRF_TRUSTED_ORIGINS',
default='https://gnxsoft.com',
default='https://gnxsoft.com,https://www.gnxsoft.com',
cast=lambda v: [s.strip() for s in v.split(',')]
)
@@ -229,7 +230,31 @@ REST_FRAMEWORK = {
},
}
# CORS Configuration
# ============================================================================
# API SECURITY CONFIGURATION
# API is only accessible from frontend through nginx reverse proxy
# ============================================================================
# Internal API Key - nginx will add this header to prove request came through proxy
# Generate a strong random key: python -c "import secrets; print(secrets.token_urlsafe(32))"
INTERNAL_API_KEY = config('INTERNAL_API_KEY', default='' if not DEBUG else 'dev-key-change-in-production')
# API security path pattern (default: all /api/ paths)
API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/')
# Enforce API security even in DEBUG mode (for testing)
ENFORCE_API_SECURITY_IN_DEBUG = config('ENFORCE_API_SECURITY_IN_DEBUG', default=False, cast=bool)
# Allowed referer patterns (regex patterns for referer validation)
ALLOWED_REFERER_PATTERNS = config(
'ALLOWED_REFERER_PATTERNS',
default='',
cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
)
# ============================================================================
# CORS Configuration - Strict origin validation
# ============================================================================
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # React development server
"http://127.0.0.1:3000",
@@ -237,14 +262,43 @@ CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:3001",
]
# Add production origins if configured
PRODUCTION_ORIGINS = config('PRODUCTION_ORIGINS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
# Add production origins if configured (defaults to gnxsoft.com domains)
PRODUCTION_ORIGINS = config(
'PRODUCTION_ORIGINS',
default='https://gnxsoft.com,https://www.gnxsoft.com',
cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
)
if PRODUCTION_ORIGINS:
CORS_ALLOWED_ORIGINS.extend(PRODUCTION_ORIGINS)
# Strict CORS configuration - only allow configured origins
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False # Never allow all origins, even in development
CORS_ALLOWED_ORIGIN_REGEXES = [] # No regex patterns by default
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only allow all origins in development
# CORS allowed methods
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
# CORS allowed headers
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'x-internal-api-key', # Custom header for nginx validation
]
# Django URL configuration
APPEND_SLASH = True

View File

@@ -32835,3 +32835,45 @@ INFO 2025-11-24 06:12:16,803 basehttp 108393 126550263457472 "GET /api/services/
INFO 2025-11-24 06:12:16,840 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:13:09,003 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:13:09,043 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:47,411 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
INFO 2025-11-24 06:36:47,412 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:47,426 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
INFO 2025-11-24 06:36:47,430 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:47,488 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:47,493 basehttp 108393 126550271850176 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:57,635 basehttp 108393 126550271850176 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
INFO 2025-11-24 06:36:57,637 basehttp 108393 126550271850176 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
INFO 2025-11-24 06:36:57,658 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:57,668 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:36:57,693 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
INFO 2025-11-24 06:37:09,801 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:37:09,806 basehttp 108393 126550632552128 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0
INFO 2025-11-24 06:37:09,807 basehttp 108393 126550623110848 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0
INFO 2025-11-24 06:37:09,826 basehttp 108393 126550649337536 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0
INFO 2025-11-24 06:37:09,842 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:37:09,843 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
INFO 2025-11-24 06:37:09,847 basehttp 108393 126550640944832 "GET /api/home/banner/ HTTP/1.1" 200 3438
INFO 2025-11-24 06:37:09,863 basehttp 108393 126550632552128 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
INFO 2025-11-24 06:37:09,868 basehttp 108393 126550640944832 "GET /api/home/banner/ HTTP/1.1" 200 3438
INFO 2025-11-24 06:37:09,878 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
INFO 2025-11-24 06:37:09,898 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:37:09,909 basehttp 108393 126550623110848 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
INFO 2025-11-24 06:37:09,915 basehttp 108393 126550632552128 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
INFO 2025-11-24 06:37:09,955 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:37:09,976 basehttp 108393 126550623110848 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
INFO 2025-11-24 06:37:10,010 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:37:10,060 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
INFO 2025-11-24 06:39:11,537 autoreload 108393 126550715764864 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:39:12,172 autoreload 139522 135175989801088 Watching for file changes with StatReloader
INFO 2025-11-24 06:39:24,941 autoreload 139522 135175989801088 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:39:25,492 autoreload 139604 123745838387328 Watching for file changes with StatReloader
INFO 2025-11-24 06:39:59,747 autoreload 139604 123745838387328 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:40:00,322 autoreload 139793 134765910855808 Watching for file changes with StatReloader
INFO 2025-11-24 06:40:19,231 autoreload 139793 134765910855808 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:40:19,802 autoreload 139883 138604311842944 Watching for file changes with StatReloader
INFO 2025-11-24 06:40:25,359 autoreload 139883 138604311842944 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:40:25,924 autoreload 139932 123776047718528 Watching for file changes with StatReloader
INFO 2025-11-24 06:40:57,132 autoreload 139932 123776047718528 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:40:57,712 autoreload 140119 136966227431552 Watching for file changes with StatReloader
INFO 2025-11-24 06:41:38,174 autoreload 140119 136966227431552 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:41:38,716 autoreload 140239 133780171337856 Watching for file changes with StatReloader

173
backEnd/nginx.conf.example Normal file
View File

@@ -0,0 +1,173 @@
# Nginx Configuration Example for GNX Web Application
# This configuration shows how to set up nginx as a reverse proxy
# to secure the Django API backend
# Generate a secure API key for INTERNAL_API_KEY:
# python -c "import secrets; print(secrets.token_urlsafe(32))"
# Add this key to your Django .env file as INTERNAL_API_KEY
upstream django_backend {
# Django backend running on internal network only
server 127.0.0.1:8000;
keepalive 32;
}
upstream nextjs_frontend {
# Next.js frontend
server 127.0.0.1:3000;
keepalive 32;
}
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
server {
listen 80;
server_name gnxsoft.com www.gnxsoft.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name gnxsoft.com www.gnxsoft.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/gnxsoft.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gnxsoft.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
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;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
# Logging
access_log /var/log/nginx/gnxsoft_access.log;
error_log /var/log/nginx/gnxsoft_error.log;
# Client body size limit
client_max_body_size 10M;
# API Endpoints - Proxy to Django backend
# These requests will have the X-Internal-API-Key header added
location /api/ {
# Rate limiting
limit_req zone=api_limit burst=20 nodelay;
# Add custom header to prove request came through nginx
# This value must match INTERNAL_API_KEY in Django settings
set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key;
# Standard proxy headers
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 X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Preserve original request headers
proxy_set_header Origin $http_origin;
proxy_set_header Referer $http_referer;
# Proxy settings
proxy_pass http://django_backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# Media files - Serve from Django
location /media/ {
alias /path/to/gnx/backEnd/media/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# Static files - Serve from Django
location /static/ {
alias /path/to/gnx/backEnd/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# Admin panel - Only allow from specific IPs (optional)
location /admin/ {
# Uncomment to restrict admin access
# allow 192.168.1.0/24;
# allow 10.0.0.0/8;
# deny all;
# Same proxy settings as /api/
set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key;
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_pass http://django_backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Frontend - Proxy to Next.js
location / {
# Rate limiting
limit_req zone=general_limit burst=50 nodelay;
# Proxy to Next.js frontend
proxy_pass http://nextjs_frontend;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint (optional)
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
# Block direct access to backend port 8000 from external IPs
# This should be done at the firewall level:
# ufw deny 8000
# iptables -A INPUT -p tcp --dport 8000 ! -s 127.0.0.1 -j DROP

View File

@@ -4,7 +4,7 @@
# Django Settings
SECRET_KEY=your-super-secret-production-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com,your-server-ip
ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,your-server-ip
# Database - Using SQLite (default)
# SQLite is configured in settings.py - no DATABASE_URL needed
@@ -37,9 +37,16 @@ SECURE_BROWSER_XSS_FILTER=True
X_FRAME_OPTIONS=DENY
# CORS Settings (Production)
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
CORS_ALLOW_CREDENTIALS=True
# CSRF Trusted Origins
CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
# API Security - Internal API Key (nginx will add this header)
# Generate a secure key: python -c "import secrets; print(secrets.token_urlsafe(32))"
INTERNAL_API_KEY=your-secure-api-key-here-change-this-in-production
# Static Files
STATIC_ROOT=/var/www/gnx/staticfiles/
MEDIA_ROOT=/var/www/gnx/media/