Final Configuration
Here is the detailed configuration of nginx with best practices , optimization and security concerns.
We have divided the configuration in multiple files, that are -
nginx.conf - main configuration file for nginx
app.conf - App specific file that need to be served.
ssl_params.conf - Common file for SSL headers
security_headers.conf - Reusable file for security related headers
proxy_params.conf - Reusable file for proxy params.
common-denied.conf - Reusable file for common file like 404 or 505 or access denied.
We need to create config folder manually . other wise change the path while including.
ssl_params.conf
To use this file we must need to configure ssl certificate first for the domain name.
# /config/ssl_params.conf
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
proxy_params.conf
Common params and headers used in reverse proxy
# /config/proxy_params.conf
proxy_pass_request_headers on;
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_hide_header Server;
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
send_timeout 10s;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k
common-denied.conf
File that contains 404 , or redirect or access denied that we can reuse in multiple blocks
#/config/common-denied.conf
if ($request_uri ~* "\.\.") {
return 403;
}
# Block access to sensitive directories and version control folders
location ~* ^/(\.well-known/|\.git|\.svn|\.hg|\.bzr|\._darcs|BitKeeper) {
deny all;
return 404;
}
# Custom error page configuration.
error_page 404 /404.html;
location = /404.html {
internal; # Ensure direct access is not allowed.
}
# Deny requests with suspicious User-Agents
if ($bad_user_agent) { #defined in nginx.conf
return 403;
}
# Custom error pages for server errors
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
security_headers.conf
common security headers that can be used in multiple blocks-
# /config/security_headers.conf
# HSTS: Enforce HTTPS (1 year) and include subdomains; add preload if desired
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
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 Cache-Control "no-cache, no-store, must-revalidate" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
# to disable permissions of camera, microphone as they are not needed in most of th apps
add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()";
nginx.conf
The main configuration file -
# /etc/nginx/nginx.conf
# user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
client_max_body_size 20M;
# Disable version disclosure for security
server_tokens off;
# Performance Optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
#buffers
client_body_buffer_size 16K;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Enable Gzip compression for text-based content
gzip on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
# Generate a CSP nonce based on the request ID (for inline scripts)
map $request_id $csp_nonce {
"~*" $request_id;
}
# Map to identify suspicious or malicious User Agents
map $http_user_agent $bad_user_agent {
default 0;
~*^$ 1; # Empty User-Agent
~*bot 1; # Bots (note: some legitimate bots may be affected)
~*spider 1;
~*crawl 1;
~*[<>]script 1;
~*(nmap|nikto|wikto|sf|sqlmap|bsqlbf|w3af|acunetix|havij|appscan) 1;
}
# Define a shared memory zone for rate limiting (DDoS protection)
limit_req_zone $binary_remote_addr zone=ddos_zone:10m rate=10r/s;
# Include additional server configurations from conf.d folder
include /etc/nginx/conf.d/*.conf;
}
app.conf
Configuration file for app specific that we are deploying. This file is having two server blocks one is for normal backend url and another is when we are using dns address like AWS load balancer.
# /proj/ideal-config.conf
###############################################################################
# Upstream definitions
###############################################################################
# backend servers, evenly distibutes loads
upstream backend_servers {
server example.com max_fails=3 fail_timeout=30s;
server example.com; # localhost:8080
}
upstream websocket_server{
server example.com; # loclhost:90
}
###############################################################################
# 1. HTTP Server Block - Redirect all HTTP traffic to HTTPS
###############################################################################
server {
listen 8000;
server_name example.com www.example.com app.example.com secure.example.com;
# Permanent redirection to HTTPS improves security and SEO
return 301 https://$host$request_uri;
}
###############################################################################
# 2. HTTPS Server Block - Main configuration for example.com and www.example.com
###############################################################################
server {
listen 443 ssl;
server_name example.com www.example.com;
# Use a public DNS resolver for upstream lookups
resolver 8.8.8.8 valid=10s;
resolver_timeout 5s;
# Include common SSL configuration
include /etc/nginx/config/ssl_params.conf;
# Include common security headers
include /etc/nginx/config/security_headers.conf;
root D:/work/company/myproj/dist/myapp/browser/;
index index.html index.htm index.nginx-debian.html;
# Apply standard rate limiting
limit_req zone=ddos_zone burst=20 nodelay;
# adjust csp headers as per need
# Content Security Policy (CSP) tailored for your site and trusted third-party services
add_header Content-Security-Policy "default-src 'self'; \
script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://cdnjs.cloudflare.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
font-src 'self' data: https://fonts.gstatic.com; \
frame-src 'self' https://www.google.com https://example.com; \
connect-src 'self' https://*.googleapis.com; \
img-src 'self' data:; \
frame-ancestors 'self'; \
worker-src 'self' data: blob:; \
base-uri 'self'; \
form-action 'self'; \
upgrade-insecure-requests;" always;
# Replace placeholder for nonce in files (if sub_filter is needed)
sub_filter_once off;
sub_filter_types *;
sub_filter "**CSP_NONCE**" "$csp_nonce";
# including common things that need to be restricted
include /etc/nginx/config/common-denied.conf;
set $allowed_origin http://backend_servers;
# Rate Limiting for DDoS protection applied globally via http block
location / {
try_files $uri $uri/ /index.html; # Check file existence, return index.html if not found.
if ($request_uri ~* ".(ico|json|css|js|gif|jpe?g|png)$") {
expires 30d;
access_log off;
add_header Pragma public;
add_header Cache-Control "public";
add_header X-Content-Type-Options nosniff always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://www.google.com https://www.gstatic.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com;
frame-src 'self' https://www.google.com $allowed_origin; connect-src 'self' data: https://*.googleapis.com wss://websocket_server; object-src 'none'; img-src 'self' data: ; frame-ancestors 'self' $allowed_origin; worker-src 'self' data: blob: ; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
break;
}
}
# Reverse Proxy for dynamic content under /app/ (or any designated URI)
location /app/ {
# Block suspicious SQL injection patterns in the query string
if ($query_string ~* "union.*select.*\(") {
return 403;
}
proxy_pass http://backend_servers;
include /etc/nginx/config/proxy_params.conf;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://www.google.com https://www.gstatic.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com ; frame-src 'self' https://www.google.com $allowed_origin ; connect-src 'self' data: https://*.googleapis.com wss://websocket_server; object-src 'none'; img-src 'self' data: ; frame-ancestors 'self' $allowed_origin; worker-src 'self' data: blob: ; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
# must be handle from backend, if not then uncomment
#add_header 'Access-Control-Allow-Origin' '*';
#add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
#if ($request_method = 'OPTIONS') {
# return 204;
#}
}
location /ws/ {
proxy_pass http://websocket_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
# allowing specific ip for senstive content
location /sensitive/ {
allow 192.168.1.0/24; # whitelisted ips
deny all;
}
# debug error log for troubleshooting
error_log /var/log/nginx/app_error.log debug;
}
###############################################################################
# 3. HTTPS Server Block - Subdomain Example (app.example.com) with alb dns address
###############################################################################
server {
listen 443 ssl;
server_name example2.com www.example2.com;
resolver 10.0.0.2 valid=30s ipv6=off;
resolver_timeout 5s;
# Include common SSL configuration
include /etc/nginx/config/ssl_params.conf;
# Include common security headers
include /etc/nginx/config/security_headers.conf;
root D:/work/company/myproj/dist/myapp/browser/;
index index.html index.htm index.nginx-debian.html;
# Apply standard rate limiting
limit_req zone=ddos_zone burst=20 nodelay;
# adjust csp headers as per need
add_header Content-Security-Policy "default-src 'self'; \
script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://cdnjs.cloudflare.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
font-src 'self' data: https://fonts.gstatic.com; \
frame-src 'self' https://www.google.com https://example.com; \
connect-src 'self' https://*.googleapis.com; \
img-src 'self' data:; \
frame-ancestors 'self'; \
worker-src 'self' data: blob:; \
base-uri 'self'; \
form-action 'self'; \
upgrade-insecure-requests;" always;
# Replace placeholder for nonce in files (if sub_filter is needed)
sub_filter_once off;
sub_filter_types *;
sub_filter "**CSP_NONCE**" "$csp_nonce";
# including common things that need to be restricted
include /etc/nginx/config/common-denied.conf;
# creating the variable with alb dns address and port as my BE need it
set $elb_dns "http://internal-prod-elb-12596.ap-south-1.elb.amazonaws.com:8000";
# Rate Limiting for DDoS protection applied globally via http block
location / {
try_files $uri $uri/ /index.html; # Check file existence, return index.html if not found.
if ($request_uri ~* ".(ico|json|css|js|gif|jpe?g|png)$") {
expires 30d;
access_log off;
add_header Pragma public;
add_header Cache-Control "public";
add_header X-Content-Type-Options nosniff always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://www.google.com https://www.gstatic.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com;
frame-src 'self' https://www.google.com $elb_dns; connect-src 'self' data: https://*.googleapis.com wss://websocket_server; object-src 'none'; img-src 'self' data: ; frame-ancestors 'self' $elb_dns; worker-src 'self' data: blob: ; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
break;
}
}
# Proxy all requests to the backend for this subdomain
location /api/ {
# When you use a literal upstream URL with a trailing slash in proxy_pass, nginx automatically strips the matching location prefix.
# However, when you use a variable (like $elb_dns), this automatic URI rewriting isn’t performed.
# Rewrite the URI: Remove the location prefix (/api) from the incoming request.
# For example, /api/api/auth/verify becomes /api/auth/verify. [ because BE need /api]
rewrite ^/api(/.*)$ $1 break;
proxy_pass $elb_dns; # adding variable in proy pass
include /etc/nginx/config/proxy_params.conf;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-$csp_nonce' https://maps.googleapis.com https://www.google.com https://www.gstatic.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com ; frame-src 'self' https://www.google.com $elb_dns ; connect-src 'self' data: https://*.googleapis.com wss://websocket_server; object-src 'none'; img-src 'self' data: ; frame-ancestors 'self' $elb_dns; worker-src 'self' data: blob: ; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
}
location /ws/ {
proxy_pass http://websocket_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
# allowing specific ip for senstive content
location /senstive/ {
allow 192.168.1.0/24; #whitelisted ips
deny all;
}
# debug error log for troubleshooting
error_log /var/logs/nginx/api_error.log debug;
}
###############################################################################
# 4. Optional: Server Block with ModSecurity (WAF) for extra protection
###############################################################################
# corrently commenting as we do not have mobsecurity installed
# server {
# listen 443 ssl http2;
# server_name secure.example.com;
# include /etc/nginx/config/ssl_params.conf
# include /etc/nginx/config/security_headers.conf;
# # Enable ModSecurity (ensure it is installed and configured)
# modsecurity on;
# modsecurity_rules_file /etc/nginx/modsec/main.conf;
# location / {
# proxy_pass http://backend.com:8080;
# include /etc/nginx/config/proxy_params.conf
# }
# }
Always check configuration nginx -t before running .
Last updated