worker_processes auto; error_log stderr warn; # Run worker processes as nobody user (master process remains root for port binding) user nobody nogroup; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Temp directories client_body_temp_path /var/cache/nginx/client_temp; proxy_temp_path /var/cache/nginx/proxy_temp; fastcgi_temp_path /var/cache/nginx/fastcgi_temp; uwsgi_temp_path /var/cache/nginx/uwsgi_temp; scgi_temp_path /var/cache/nginx/scgi_temp; # Docker DNS resolver resolver 127.0.0.11 valid=30s; lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;"; lua_shared_dict stream_keys 10m; lua_shared_dict rate_limit 10m; # Enable compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss image/svg+xml; # 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; # Map to handle CORS origin properly map $http_origin $cors_origin { default ""; "~^https?://localhost(:[0-9]+)?$" $http_origin; "~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin; "~^https?://\[::1\](:[0-9]+)?$" $http_origin; } upstream backend { server drogon-backend:8080; } upstream frontend { server sveltekit:3000; } upstream ome { server ovenmediaengine:8080; } # Rate limiting zones limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/m; limit_req_zone $binary_remote_addr zone=register_limit:10m rate=1r/m; # Increase client max body size for avatar uploads client_max_body_size 1m; server { listen 80; server_name localhost; # Security headers for the whole server add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Fixed: Serve uploaded files with correct configuration location /uploads/ { # Use root directive with absolute path to avoid alias+try_files bug root /app; # Security settings autoindex off; add_header X-Content-Type-Options "nosniff" always; # Only serve files, not directories try_files $uri =404; # Cache static images location ~* \.(jpg|jpeg|png|gif|webp|svg)$ { expires 30d; add_header Cache-Control "public, immutable" always; add_header X-Content-Type-Options "nosniff" always; } } # SvelteKit immutable assets (with content hashes) location ~ ^/_app/immutable/ { proxy_pass http://frontend; 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; # Long cache for immutable assets proxy_cache_valid 200 1y; expires 1y; add_header Cache-Control "public, immutable" always; access_log off; } # SvelteKit mutable assets location ~ ^/_app/ { proxy_pass http://frontend; 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; # Short cache for mutable assets expires 1h; add_header Cache-Control "public, max-age=3600" always; } # Authentication endpoints with strict rate limiting location = /api/auth/register { limit_req zone=register_limit burst=2 nodelay; # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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; } location = /api/auth/login { limit_req zone=auth_limit burst=5 nodelay; # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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; } location ~ ^/api/auth/(pgp-challenge|pgp-verify) { limit_req zone=auth_limit burst=5 nodelay; # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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; } # Public user profile endpoints - no authentication required location ~ ^/api/users/[^/]+/?$ { # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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; # Cache public profiles for a short time expires 1m; add_header Cache-Control "public, max-age=60" always; } # Public user PGP keys endpoints - no authentication required location ~ ^/api/users/[^/]+/pgp-keys$ { # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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; # Cache PGP keys for a bit longer as they change less frequently expires 5m; add_header Cache-Control "public, max-age=300" always; } # Public realm endpoints (with viewer token authentication for stream-key) location ~ ^/api/realms/(by-name/[^/]+|live|[0-9]+/stats|[0-9]+/viewer-token|[0-9]+/stream-key)$ { # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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 Cookie $http_cookie; # Short cache for live realm data expires 10s; add_header Cache-Control "public, max-age=10" always; } # Public stream endpoints (some require viewer tokens) location ~ ^/api/stream/(heartbeat/[^/]+)$ { # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type" always; add_header Access-Control-Allow-Credentials "true" always; if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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 Cookie $http_cookie; } # Other API endpoints (authenticated) location /api/ { limit_req zone=api_limit burst=20 nodelay; # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; add_header Access-Control-Allow-Credentials "true" always; # Handle preflight if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } proxy_pass http://backend; 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 Cookie $http_cookie; # Don't cache API responses expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate" always; } # WebSocket location /ws/ { proxy_pass http://backend; 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_set_header Cookie $http_cookie; # WebSocket timeouts proxy_read_timeout 3600s; proxy_send_timeout 3600s; } # Frontend (all other requests) location / { proxy_pass http://frontend; 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; # Enable HTTP/1.1 for keep-alive proxy_http_version 1.1; proxy_set_header Connection ""; } } # Separate server block for port 8088 (HLS/LLHLS) server { listen 8088; server_name localhost; # Security headers add_header X-Content-Type-Options "nosniff" always; # Token validation for HLS/LLHLS playlists and segments location ~ ^/app/([^/]+)/(.*\.(m3u8|ts|m4s))$ { set $stream_key $1; set $file_path $2; # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "Range" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always; # Handle preflight if ($request_method = 'OPTIONS') { add_header Content-Length 0; add_header Content-Type text/plain; return 204; } # Access control via Lua access_by_lua_block { local redis_helper = require "redis_helper" -- Get viewer token from cookie local cookie_header = ngx.var.http_cookie if not cookie_header then ngx.status = ngx.HTTP_FORBIDDEN ngx.say("No authentication token") return ngx.exit(ngx.HTTP_FORBIDDEN) end -- Extract viewer_token cookie local token = nil -- Handle URL-encoded cookies and spaces cookie_header = ngx.unescape_uri(cookie_header) for k, v in string.gmatch(cookie_header, "([^=]+)=([^;]+)") do k = k:match("^%s*(.-)%s*$") -- trim whitespace if k == "viewer_token" then token = v:match("^%s*(.-)%s*$") -- trim whitespace break end end if not token then ngx.status = ngx.HTTP_FORBIDDEN ngx.say("Missing viewer token") return ngx.exit(ngx.HTTP_FORBIDDEN) end -- Validate token local valid_stream = redis_helper.validate_viewer_token(token, ngx.var.stream_key) if not valid_stream then ngx.status = ngx.HTTP_FORBIDDEN ngx.say("Invalid viewer token") return ngx.exit(ngx.HTTP_FORBIDDEN) end -- Optionally refresh token TTL on segment access redis_helper.refresh_viewer_token(token) } # Cache settings for segments location ~ \.ts$ { expires 1h; add_header Cache-Control "public, max-age=3600" always; } # Don't cache playlists location ~ \.m3u8$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } # Proxy to OvenMediaEngine proxy_pass http://ome/app/$stream_key/$file_path; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Public access for stream info location / { # CORS headers add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Credentials "true" always; proxy_pass http://ome; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } }