Nginx is the backbone of modern web infrastructure, serving as a high-performance web server, load balancer, mail proxy, and reverse proxy. When exposing internal container services or self-hosted applications to local or external networks, hardening your Nginx configuration is essential to prevent information disclosure, SSL stripping, and cross-site scripting.
1. Core Capabilities: What Nginx Can Do
Nginx is highly versatile. Beyond serving standard static web pages, it handles several critical roles across infrastructure setups:
- Reverse Proxy / SSL Terminator: Sits at the edge of your network, terminating incoming HTTPS traffic, managing TLS certificates, and securely forwarding requests to backend containers or services (e.g., Jellyfin, Nextcloud, Mealie).
- Static Asset Server: Delivers compiled static sites (such as Hugo or Astro) with ultra-low latency and minimal RAM overhead.
- Load Balancer: Distributes incoming HTTP/HTTPS traffic across multiple backend nodes using algorithms like round-robin, least-connections, or IP-hash.
- Content Cache (Microcaching): Caches dynamic backend responses (from Node.js, Python, or PHP-FPM) in memory or disk for short intervals, drastically mitigating load spikes.
- Rate Limiting & Traffic Shaping: Limits request rates per IP address to safeguard sensitive endpoints against brute-force attacks and abuse.
2. Installation and Directory Layout
Setting up Nginx with a clean directory hierarchy prevents configuration clutter as services scale.
Basic Installation
On Debian or Ubuntu-based distributions, install Nginx from the official package manager:
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
Configuration Architecture
Avoid placing all virtual hosts into the global /etc/nginx/nginx.conf file. Follow the modular structure:
/etc/nginx/nginx.conf: Global settings, worker process counts, global logging formats, and rate-limiting zones.
/etc/nginx/sites-available/: Individual virtual host (server block) configuration files.
/etc/nginx/sites-enabled/: Active site configurations, linked via symbolic links from sites-available.
Enabling a site configuration:
sudo ln -s /etc/nginx/sites-available/my-app.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
3. Hardened Production Nginx Template
Below is a production-ready template combining version masking, modern TLS protocols, strong cipher suites, HSTS, essential security headers, and rate limiting:
http {
# 1. Hide Nginx version details in server headers and error pages
server_tokens off;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Custom security log format
log_format secure_format '$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 secure_format;
# Rate Limiting Zone: 10 requests/second limit per IP
limit_req_zone $binary_remote_addr zone=app_limit:10m rate=10r/s;
# Redirect all HTTP traffic to HTTPS
server {
listen 80;
listen [::]:80;
server_name w4dxr.us www.w4dxr.us;
return 301 https://$host$request_uri;
}
# Main HTTPS Server Block
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name w4dxr.us www.w4dxr.us;
# SSL/TLS Certificate paths (Let's Encrypt / Certbot)
ssl_certificate /etc/letsencrypt/live/w4dxr.us/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/w4dxr.us/privkey.pem;
# 2. Enforce Modern TLS Protocols (Disable legacy SSLv3, TLS 1.0, TLS 1.1)
ssl_protocols TLSv1.2 TLSv1.3;
# 3. Strong Cipher Suites
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_prefer_server_ciphers off;
# SSL Session Caching
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# 4. HTTP Strict Transport Security (HSTS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 5. Essential 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;
# Static site root
root /var/www/w4dxr/public;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Reverse Proxy for internal services
location /app/ {
# Apply rate limiting
limit_req zone=app_limit burst=20 nodelay;
proxy_pass http://127.0.0.1:8080/;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
4. Production Application Proxy Examples
Different self-hosted apps require tailored Nginx directives. Below are ready-to-use virtual host blocks for common applications.
A. Nextcloud Configuration
Nextcloud requires high upload limits, WebDAV redirects for CalDAV/CardDAV syncing, and memory-buffering adjustments.
server {
listen 443 ssl;
http2 on;
server_name cloud.example.com;
ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# Nextcloud DAV service discovery redirects
location = /.well-known/carddav { return 301 $scheme://$host/remote.php/dav; }
location = /.well-known/caldav { return 301 $scheme://$host/remote.php/dav; }
# Increase body size for file uploads (e.g., 10 Gigabytes)
client_max_body_size 10G;
client_body_timeout 300s;
fastcgi_buffers 64 4K;
location / {
proxy_pass http://127.0.0.1:8080;
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;
# Disable response buffering for large file transfers
proxy_buffering off;
proxy_max_temp_file_size 0;
}
}
Jellyfin needs WebSockets enabled for live dashboard sync and video player control, long timeouts for video streaming, and disabled proxy buffering to prevent stream stalling.
server {
listen 443 ssl;
http2 on;
server_name media.example.com;
ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8096;
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;
# Disable buffering for live video streaming
proxy_buffering off;
}
# WebSocket location block for real-time player updates
location /socket {
proxy_pass http://127.0.0.1:8096;
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;
# Extend read/send timeouts for persistent WebSocket connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
C. Vaultwarden / Bitwarden Configuration
Vaultwarden relies on a separate WebSocket endpoint (/notifications/hub) for live password vault updates across browser extensions and mobile apps.
server {
listen 443 ssl;
http2 on;
server_name vault.example.com;
ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
client_max_body_size 525M; # Allows up to 500MB attachment uploads
# Standard Web Vault
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
# Vaultwarden Notifications WebSocket
location /notifications/hub {
proxy_pass http://127.0.0.1:3012;
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;
}
}
5. Key Hardening Checklist
- Disable Server Tokens: Set
server_tokens off; to hide version strings from potential vulnerability scans.
- Header Inheritance Rule: If you declare an
add_header directive inside a child location block, Nginx completely ignores all add_header directives set in the parent server block. Re-declare all security headers inside location blocks that specify custom headers.
- Isolate Backend Services: Avoid binding Docker container ports to
0.0.0.0 (e.g., -p 8080:8080). Bind them explicitly to 127.0.0.1 (-p 127.0.0.1:8080:8080) so they are accessible only through Nginx.
- Always Test Changes: Run
sudo nginx -t before issuing a reload command to ensure configuration syntax is valid.
6. Common Problems and Solutions
502 Bad Gateway
-
Cause: Nginx cannot establish a connection with the upstream backend application.
-
Fix: Verify the backend service is running (systemctl status <service> or docker ps). Confirm the IP address and port match your proxy_pass setting. On SELinux-enabled systems (RHEL/Fedora), enable network connections for HTTP servers:
sudo setsebool -P httpd_can_network_connect 1
403 Forbidden
-
Cause: Nginx lacks read or execute permissions for the requested files or directories.
-
Fix: Update permissions for the Nginx worker user (www-data or nginx):
sudo chown -R www-data:www-data /var/www/w4dxr/public
sudo chmod -R 755 /var/www/w4dxr/public
Ensure an index.html or index.php file exists in the directory if directory listing (autoindex) is disabled.
Error: Address already in use
-
Cause: Port 80 or 443 is already bound by another process (e.g., Apache, Docker, or an orphan Nginx process).
-
Fix: Identify the conflicting process and stop it:
sudo ss -tulpn | grep -E ':80|:443'
sudo systemctl stop apache2
- Cause: A nested
location block contains its own add_header directive, overriding the server block headers.
- Fix: Copy all global security headers directly into the specific
location block that introduces extra custom headers.
413 Request Entity Too Large
-
Cause: Client request or file upload exceeds the default Nginx limit (1MB).
-
Fix: Increase the client body size limit in the http, server, or location block:
client_max_body_size 50M;