nginx-ops
Nginx configuration, reverse proxy, SSL/TLS, load balancing, and performance tuning. Use for: nginx, reverse proxy, load balancer, proxy_pass, ssl certificate, lets encrypt, web server, location block, upstream, server block, nginx config, certbot, hsts, gzip, rate limiting.
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/nginx-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Nginx Operations
Comprehensive Nginx configuration, reverse proxy patterns, SSL/TLS hardening, load balancing strategies, and performance optimization for production deployments.
Configuration Architecture Quick Reference
nginx.conf (main context)
├── worker_processes auto;
├── worker_rlimit_nofile 65535;
│
├── events { # Connection handling
│ ├── worker_connections 4096;
│ └── multi_accept on;
│ }
│
├── http { # HTTP server settings
│ ├── include mime.types;
│ ├── default_type application/octet-stream;
│ ├── sendfile on;
│ ├── gzip on;
│ │
│ ├── upstream backend { # Load balancing pool
│ │ └── server 127.0.0.1:3000;
│ │ }
│ │
│ ├── server { # Virtual host
│ │ ├── listen 443 ssl;
│ │ ├── server_name example.com;
│ │ │
│ │ ├── location / { # Request routing
│ │ │ └── proxy_pass http://backend;
│ │ │ }
│ │ │
│ │ └── location /static/ {
│ │ └── root /var/www;
│ │ }
│ │ }
│ │
│ └── include /etc/nginx/conf.d/*.conf;
│ }
│
└── stream { # TCP/UDP proxying (optional)
└── server { ... }
}
Directive Inheritance Rules
| Rule | Behavior | Example |
|---|---|---|
| Inherit down | Child blocks inherit parent directives | gzip on; in http applies to all server blocks |
| Override | Child directive overrides parent | gzip off; in location overrides http-level gzip on; |
| Array directives | NOT inherited - must be redeclared | proxy_set_header in location replaces ALL headers from server |
| No upward | Inner blocks never affect outer | location-level settings don't affect server |
Critical: Array-type directives (proxy_set_header, add_header, proxy_hide_header) are completely replaced when redefined in a child block, not merged. If you set one proxy_set_header in a location, you must redeclare ALL of them.
Reverse Proxy Decision Tree
Need to proxy requests?
│
├─ Single backend server?
│ └─ Use simple proxy_pass
│ proxy_pass http://127.0.0.1:3000;
│
├─ Multiple backend servers?
│ │
│ ├─ Need session persistence?
│ │ ├─ By client IP → ip_hash
│ │ └─ By cookie → sticky cookie (Nginx Plus)
│ │
│ ├─ Backends have unequal capacity?
│ │ └─ Use weight parameter
│ │ server backend1:3000 weight=3;
│ │ server backend2:3000 weight=1;
│ │
│ ├─ Want fewest active connections?
│ │ └─ least_conn
│ │
│ ├─ Want even random distribution?
│ │ └─ random two least_conn
│ │
│ └─ Default (no special needs)?
│ └─ round-robin (default, no directive needed)
│
├─ WebSocket connections?
│ └─ Add Upgrade + Connection headers
│ proxy_set_header Upgrade $http_upgrade;
│ proxy_set_header Connection "upgrade";
│
├─ gRPC backend?
│ └─ Use grpc_pass grpc://backend;
│
└─ Streaming / Server-Sent Events?
└─ Disable buffering
proxy_buffering off;
SSL/TLS Quick Start
Let's Encrypt with Certbot
# Install certbot
sudo apt install certbot python3-certbot-nginx # Debian/Ubuntu
sudo dnf install certbot python3-certbot-nginx # RHEL/Fedora
# Obtain certificate (nginx plugin - easiest)
sudo certbot --nginx -d example.com -d www.example.com
# Obtain certificate (webroot - no nginx restart)
sudo certbot certonly --webroot -w /var/www/html -d example.com
# Test auto-renewal
sudo certbot renew --dry-run
Minimal Production SSL Config
server {
listen 443 ssl http2;
server_name example.com;
# Certificates
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS (1.2 + 1.3)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (2 years)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Session caching
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
root /var/www/example.com;
index index.html;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
Location Matching Order
Nginx evaluates location blocks in a specific priority order, not in the order they appear in the config file.
| Priority | Modifier | Type | Example | Behavior |
|---|---|---|---|---|
| 1 | = |
Exact match | location = /favicon.ico |
Stops search immediately on match |
| 2 | ^~ |
Prefix (no regex) | location ^~ /static/ |
Stops search if this prefix matches (skips regex) |
| 3 | ~ |
Regex (case-sensitive) | location ~ \.php$ |
First matching regex wins |
| 3 | ~* |
Regex (case-insensitive) | location ~* \.(jpg\|png)$ |
First matching regex wins |
| 4 | (none) | Prefix | location /api/ |
Longest prefix wins (but only after regex check) |
Evaluation Algorithm
- Check all prefix locations, remember the longest match
- If longest match has
^~modifier → use it, stop - Check regex locations in config-file order → first match wins
- If no regex matches → use the longest prefix from step 1
= /pathis checked first and wins immediately if matched
Example
location = / { } # Only exact "/"
location / { } # Catch-all prefix
location /api/ { } # Prefix: /api/*
location ^~ /static/ { } # Prefix, skip regex: /static/*
location ~ \.php$ { } # Regex: any .php file
location ~* \.(gif|jpg)$ { } # Case-insensitive regex: images
| Request URI | Matched Location | Why |
|---|---|---|
/ |
= / |
Exact match (priority 1) |
/index.html |
/ |
Longest prefix, no regex match |
/api/users |
/api/ |
Longest prefix, no regex match |
/static/logo.png |
^~ /static/ |
^~ skips regex check |
/app/index.php |
~ \.php$ |
Regex beats prefix |
/photos/cat.jpg |
~* \.(gif\|jpg)$ |
Regex beats prefix |
Common Configurations
SPA Routing (React, Vue, Angular)
server {
listen 80;
server_name app.example.com;
root /var/www/app/dist;
index index.html;
# Serve static files directly, fall back to index.html for SPA routes
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
WebSocket Proxy
location /ws/ {
proxy_pass http://127.0.0.1:3000;
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_read_timeout 86400s; # Keep WebSocket alive for 24h
proxy_send_timeout 86400s;
}
Rate Limiting
# Define zone: 10MB shared memory, 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
# Allow burst of 20, process excess without delay up to burst
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
Gzip Compression
http {
gzip on;
gzip_comp_level 5; # Balance CPU vs compression (1-9)
gzip_min_length 256; # Don't compress tiny responses
gzip_vary on; # Vary: Accept-Encoding header
gzip_proxied any; # Compress proxied responses too
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
}
Static File Serving
location /static/ {
alias /var/www/static/; # Note: alias, not root (includes /static/ path)
expires 30d;
add_header Cache-Control "public, no-transform";
# Disable access log for static files
access_log off;
# Enable open file cache
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
}
CORS Headers
location /api/ {
# CORS headers
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
# Handle preflight requests
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
Docker Patterns
Nginx as Reverse Proxy in Docker Compose
# docker-compose.yml
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
networks:
- webnet
app:
build: .
expose:
- "3000" # Internal only, not published to host
networks:
- webnet
networks:
webnet:
# nginx.conf for docker-compose (use service name as hostname)
upstream app_backend {
server app:3000; # Docker DNS resolves service name
}
server {
listen 80;
location / {
proxy_pass http://app_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;
}
}
Multi-Stage Build with Static Assets
# Stage 1: Build frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# nginx.conf for containerized SPA
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Cache busted assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Common Gotchas
| Gotcha | Why | Fix |
|---|---|---|
Trailing slash in proxy_pass |
proxy_pass http://backend keeps /api/users as-is; proxy_pass http://backend/ strips the matched location prefix |
Be intentional: with / to strip prefix, without to preserve |
| Missing proxy headers | Backend sees nginx's IP, not the client's. Breaks auth, logging, and geo detection | Always set X-Real-IP, X-Forwarded-For, X-Forwarded-Proto, and Host |
| Buffer size errors (502) | Large headers (cookies, JWTs) exceed default buffer sizes | Increase proxy_buffer_size 8k; and proxy_buffers 4 16k; |
worker_connections too low |
Default is 512 or 1024; each client uses 2 connections (client + upstream) | Set worker_connections 4096; and raise worker_rlimit_nofile |
try_files with proxy_pass |
try_files and proxy_pass in the same location don't work as expected |
Use try_files $uri @backend; with a named location for proxy |
| "if is evil" | if inside location creates an implicit nested location, breaking directives |
Use map for variable-based logic; reserve if for return/rewrite only |
| Resolver for dynamic upstreams | Variables in proxy_pass (e.g., $upstream) bypass startup DNS resolution |
Add resolver 127.0.0.11 valid=30s; (Docker) or resolver 1.1.1.1; |
Missing index directive |
Returns 403 Forbidden when accessing a directory instead of index file | Add index index.html; in server or location block |
| Permission denied on socket | Nginx worker can't read the upstream Unix socket | Ensure nginx user is in the socket's group; chmod 660 the socket |
Duplicate Content-Encoding with gzip |
Upstream already compresses + nginx gzip double-compresses | Use gzip_proxied carefully or proxy_set_header Accept-Encoding ""; |
add_header not inherited |
Adding ANY add_header in a location discards ALL parent add_header directives |
Redeclare all headers in the child block, or use include for shared headers |
alias vs root confusion |
root appends the location path; alias replaces it. /img/ + root /data = /data/img/; alias /data/ = /data/ |
Use alias when location path shouldn't appear in filesystem path |
Reference Files
| File | Contents | Lines |
|---|---|---|
| reverse-proxy.md | Upstream blocks, load balancing, proxy caching, WebSocket/gRPC, timeouts, real-world configs | ~650 |
| ssl-security.md | TLS config, Let's Encrypt, HSTS, OCSP, security headers, rate limiting, mTLS | ~550 |
| performance.md | Worker tuning, compression, caching, HTTP/2+3, static files, monitoring | ~550 |
See Also
- docker-ops - Container orchestration, docker-compose patterns
- security-ops - Application security, authentication patterns
- ci-cd-ops - Deployment pipelines, zero-downtime deploys
- Nginx official docs
- Mozilla SSL Configuration Generator
- Nginx Config Generator
Files (claude-mods)
-
assets
-
.gitkeep 0 B · in bundle
-
-
references
-
performance.md 25.1 KB
# Performance Reference Comprehensive guide to Nginx performance optimization: worker tuning, connection handling, compression, caching, HTTP/2 and HTTP/3, static file serving, and monitoring. --- ## Table of Contents 1. [Worker Configuration](#worker-configuration) 2. [Event Model](#event-model) 3. [Connection Handling](#connection-handling) 4. [Sendfile and TCP Optimizations](#sendfile-and-tcp-optimizations) 5. [Compression](#compression) 6. [Open File Cache](#open-file-cache) 7. [Static File Serving](#static-file-serving) 8. [Proxy Caching](#proxy-caching) 9. [FastCGI Caching](#fastcgi-caching) 10. [Microcaching](#microcaching) 11. [HTTP/2](#http2) 12. [HTTP/3 (QUIC)](#http3-quic) 13. [Connection Draining](#connection-draining) 14. [Monitoring](#monitoring) --- ## Worker Configuration ### Worker Processes ```nginx # Auto-detect CPU cores (recommended) worker_processes auto; # Or set explicitly (match CPU core count) # worker_processes 4; # Pin workers to specific CPUs (optional, advanced) worker_cpu_affinity auto; # Or manually: worker_cpu_affinity 0001 0010 0100 1000; ``` ### Worker Connections ```nginx events { # Maximum simultaneous connections per worker # Total capacity = worker_processes * worker_connections worker_connections 4096; # Accept multiple connections at once multi_accept on; } ``` ### File Descriptor Limits Each connection uses at least one file descriptor (two when proxying). ```nginx # Maximum open files per worker process # Should be >= 2 * worker_connections worker_rlimit_nofile 65535; ``` Also set OS-level limits: ```bash # /etc/security/limits.conf nginx soft nofile 65535 nginx hard nofile 65535 # Or /etc/systemd/system/nginx.service.d/override.conf [Service] LimitNOFILE=65535 ``` ### Sizing Guidelines | Traffic Level | `worker_processes` | `worker_connections` | `worker_rlimit_nofile` | Total Capacity | |--------------|-------------------|---------------------|----------------------|----------------| | Low (< 1K rps) | auto (2-4) | 1024 | 4096 | 2K-4K connections | | Medium (1K-10K rps) | auto (4-8) | 4096 | 16384 | 16K-32K connections | | High (10K-100K rps) | auto (8-16) | 8192 | 65535 | 64K-128K connections | --- ## Event Model ### Linux (epoll) ```nginx events { use epoll; worker_connections 4096; multi_accept on; } ``` `epoll` is the most efficient event model on Linux, using O(1) event notification. ### BSD/macOS (kqueue) ```nginx events { use kqueue; worker_connections 4096; multi_accept on; } ``` ### Event Model Comparison | Model | OS | Scalability | Notes | |-------|-----|-------------|-------| | `epoll` | Linux 2.6+ | Excellent | Default and best for Linux | | `kqueue` | FreeBSD, macOS | Excellent | Default for BSD systems | | `select` | All | Poor | Legacy, limited to 1024 fds | | `poll` | All | Fair | Better than select, still O(n) | Nginx auto-selects the best available model. Explicit `use` is optional but recommended for clarity. --- ## Connection Handling ### Keepalive Configuration ```nginx http { # Client-facing keepalive keepalive_timeout 65s; # Close idle connections after 65s keepalive_requests 1000; # Max requests per keepalive connection # Reset timed-out connections (free resources faster) reset_timedout_connection on; # Client timeouts client_body_timeout 12s; # Time to receive request body client_header_timeout 12s; # Time to receive request headers send_timeout 10s; # Time between successive writes to client # Limit request/header sizes client_max_body_size 10m; # Max upload size client_body_buffer_size 16k; # Buffer for request body client_header_buffer_size 1k; # Buffer for request headers large_client_header_buffers 4 8k; # For large headers (cookies, etc.) } ``` ### Keepalive Tuning | Scenario | `keepalive_timeout` | `keepalive_requests` | Rationale | |----------|--------------------|--------------------|-----------| | API server | 30-60s | 1000-10000 | Frequent requests, reuse connections | | Static files | 15-30s | 100-500 | Quick downloads, then disconnect | | WebSocket | 3600s+ | N/A | Long-lived connections | | High-traffic | 15-30s | 100 | Free connections sooner | --- ## Sendfile and TCP Optimizations ### sendfile Transfers files directly in kernel space without copying to userspace. Significant performance improvement for static files. ```nginx http { # Enable kernel-level file transfer sendfile on; # Send headers and beginning of file in one packet tcp_nopush on; # Disable Nagle algorithm (send small packets immediately) tcp_nodelay on; } ``` ### How They Work Together | Directive | Purpose | When Active | |-----------|---------|-------------| | `sendfile on` | Zero-copy file transfer via kernel | Serving static files | | `tcp_nopush on` | Batch headers + file data into full packets | With sendfile, before last packet | | `tcp_nodelay on` | Send last packet immediately (no 200ms Nagle delay) | After tcp_nopush releases last packet | The combination `sendfile on; tcp_nopush on; tcp_nodelay on;` is optimal: 1. `sendfile` transfers the file efficiently 2. `tcp_nopush` fills packets completely for the bulk of the transfer 3. `tcp_nodelay` sends the final partial packet without waiting --- ## Compression ### Gzip Configuration ```nginx http { # Enable gzip compression gzip on; # Compression level (1-9, higher = smaller but more CPU) # 5-6 is a good balance gzip_comp_level 5; # Minimum response size to compress (skip tiny responses) gzip_min_length 256; # Add Vary: Accept-Encoding header gzip_vary on; # Compress proxied responses gzip_proxied any; # MIME types to compress (text/html is always compressed) gzip_types text/plain text/css text/javascript text/xml application/javascript application/json application/xml application/xml+rss application/atom+xml application/vnd.ms-fontobject font/opentype image/svg+xml image/x-icon; # Disable gzip for old browsers (IE6) gzip_disable "msie6"; # Buffer size for gzip gzip_buffers 16 8k; } ``` ### Gzip Level Comparison | Level | Compression Ratio | CPU Usage | Best For | |-------|------------------|-----------|----------| | 1 | Low (~60%) | Minimal | Very high traffic, CPU-bound | | 3-4 | Medium (~70%) | Low | Good default for most sites | | 5-6 | Good (~75%) | Moderate | Recommended balance | | 9 | Maximum (~78%) | High | Rarely worth it over level 6 | The diminishing returns above level 5-6 are significant: going from level 5 to 9 might save 3% more bytes but costs 3-4x more CPU. ### Brotli Compression Brotli achieves 15-20% better compression than gzip at similar CPU cost. Requires the `ngx_brotli` module. ```nginx # Requires: ngx_brotli module # Install: https://github.com/google/ngx_brotli http { # Brotli dynamic compression brotli on; brotli_comp_level 6; brotli_min_length 256; brotli_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml; # Serve pre-compressed .br files if available brotli_static on; # Keep gzip as fallback (not all clients support brotli) gzip on; gzip_comp_level 5; gzip_types text/plain text/css application/javascript application/json; } ``` ### Pre-Compressed Files Serve pre-compressed files to avoid runtime compression overhead. ```nginx http { # Serve .gz files if they exist gzip_static on; # Serve .br files if they exist (requires brotli module) brotli_static on; } ``` Build step to pre-compress: ```bash # Pre-compress static assets during build fd -e js -e css -e html -e svg -e json dist/ -x gzip -k -9 {} fd -e js -e css -e html -e svg -e json dist/ -x brotli -k {} ``` --- ## Open File Cache Cache file descriptors, metadata, and lookup results to reduce filesystem calls. ```nginx http { # Cache up to 1000 file descriptors, remove unused after 20s open_file_cache max=1000 inactive=20s; # How often to check if cached info is still valid open_file_cache_valid 30s; # Minimum number of accesses before caching open_file_cache_min_uses 2; # Cache file lookup errors (e.g., file not found) open_file_cache_errors on; } ``` ### When to Use | Scenario | Recommended | |----------|-------------| | Serving many static files | Yes | | Reverse proxy only | No (not needed) | | High-traffic static site | Yes, increase max | | Few large files | Marginal benefit | --- ## Static File Serving ### Optimized Static File Configuration ```nginx server { listen 443 ssl http2; server_name static.example.com; root /var/www/static; # Performance fundamentals sendfile on; tcp_nopush on; tcp_nodelay on; # File descriptor caching open_file_cache max=2000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on; # Immutable hashed assets (e.g., app.a3b4c5d6.js) location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires max; add_header Cache-Control "public, immutable"; access_log off; } # Regular static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, no-transform"; access_log off; } # HTML files (shorter cache, must revalidate) location ~* \.html$ { expires 1h; add_header Cache-Control "public, must-revalidate"; } # Enable ETag for cache validation etag on; } ``` ### Cache-Control Header Reference | Directive | Purpose | Use Case | |-----------|---------|----------| | `public` | Any cache can store | Static assets | | `private` | Only browser can store | User-specific content | | `no-cache` | Must revalidate before using | HTML pages | | `no-store` | Don't cache at all | Sensitive data | | `max-age=N` | Cache for N seconds | All cacheable content | | `immutable` | Never changes (skip revalidation) | Hashed filenames | | `must-revalidate` | Don't serve stale, even if disconnected | Critical content | | `stale-while-revalidate=N` | Serve stale while fetching fresh | UX optimization | ### Expires Directive Shortcuts ```nginx # Specific durations expires 30d; # 30 days expires 1h; # 1 hour expires 30m; # 30 minutes expires max; # Far future (practically forever) expires off; # Don't add Expires header expires -1; # Already expired (forces revalidation) expires epoch; # Set to Unix epoch (Jan 1, 1970) expires modified +24h; # 24h after file modification time ``` --- ## Proxy Caching ### Production Proxy Cache Configuration ```nginx http { # Define cache storage proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=proxy_cache:20m # 20MB metadata (~160K keys) max_size=20g # 20GB max disk usage inactive=7d # Remove unused items after 7 days use_temp_path=off # Write directly to cache dir manager_files=100 # Files to process per cache manager cycle manager_threshold=200ms; # Max time for cache manager cycle server { location / { proxy_pass http://backend; # Enable caching proxy_cache proxy_cache; # Cache key (determines what is considered a unique response) proxy_cache_key "$scheme$request_method$host$request_uri"; # Cache durations by status code proxy_cache_valid 200 301 302 1h; proxy_cache_valid 404 1m; # Serve stale content during backend errors proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; # Background refresh proxy_cache_background_update on; # Prevent thundering herd (only one request refreshes) proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_lock_age 5s; # Skip caching for logged-in users proxy_cache_bypass $cookie_session $http_authorization; proxy_no_cache $cookie_session $http_authorization; # Show cache status in response header add_header X-Cache-Status $upstream_cache_status always; # Minimum uses before caching (prevent caching one-time requests) proxy_cache_min_uses 2; } } } ``` ### Cache Status Values The `$upstream_cache_status` variable contains: | Value | Meaning | |-------|---------| | `HIT` | Served from cache | | `MISS` | Not in cache, fetched from backend | | `BYPASS` | Cache was bypassed (proxy_cache_bypass matched) | | `EXPIRED` | Cache entry expired, fetched fresh from backend | | `STALE` | Served stale (backend unavailable, using proxy_cache_use_stale) | | `UPDATING` | Stale entry served while background update in progress | | `REVALIDATED` | Cache entry was revalidated with If-Modified-Since | ### Cache Key Design ```nginx # Default: includes method, scheme, host, and URI proxy_cache_key "$scheme$request_method$host$request_uri"; # Include query parameters explicitly proxy_cache_key "$host$request_uri$is_args$args"; # Include a custom header (e.g., API version) proxy_cache_key "$host$request_uri$http_x_api_version"; # Include cookie for per-user caching (use carefully!) proxy_cache_key "$host$request_uri$cookie_lang"; # Separate cache for mobile vs desktop proxy_cache_key "$host$request_uri$http_user_agent_class"; ``` --- ## FastCGI Caching For PHP-FPM and other FastCGI applications. ```nginx http { # FastCGI cache zone fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=fcgi_cache:10m max_size=5g inactive=60m use_temp_path=off; server { # Skip cache for logged-in users and POST requests set $skip_cache 0; # Don't cache POST requests if ($request_method = POST) { set $skip_cache 1; } # Don't cache URLs with query strings if ($query_string != "") { set $skip_cache 1; } # Don't cache admin pages (WordPress example) if ($request_uri ~* "/wp-admin/|/wp-login.php") { set $skip_cache 1; } # Don't cache logged-in users (WordPress) if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; } location ~ \.php$ { fastcgi_pass php-fpm; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; # Enable FastCGI cache fastcgi_cache fcgi_cache; fastcgi_cache_key "$scheme$request_method$host$request_uri"; fastcgi_cache_valid 200 60m; fastcgi_cache_valid 301 302 10m; fastcgi_cache_valid 404 1m; # Skip cache conditions fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; # Serve stale during errors fastcgi_cache_use_stale error timeout updating http_500 http_502 http_503; fastcgi_cache_background_update on; fastcgi_cache_lock on; # Cache status header add_header X-FastCGI-Cache $upstream_cache_status; } } } ``` --- ## Microcaching Cache dynamic content for very short durations (1-5 seconds) to absorb traffic spikes. Even a 1-second cache dramatically reduces backend load under high traffic. ```nginx http { proxy_cache_path /var/cache/nginx/micro levels=1:2 keys_zone=micro_cache:5m max_size=1g inactive=1m use_temp_path=off; server { location / { proxy_pass http://backend; # Enable microcaching proxy_cache micro_cache; proxy_cache_valid 200 1s; # Cache for just 1 second # Serve stale while updating proxy_cache_use_stale updating error timeout; proxy_cache_background_update on; # Only one request triggers backend fetch proxy_cache_lock on; proxy_cache_lock_timeout 1s; # Don't cache if backend sets Cache-Control: no-cache proxy_cache_bypass $http_cache_control; # Don't cache for authenticated users proxy_cache_bypass $cookie_session; proxy_no_cache $cookie_session; add_header X-Cache-Status $upstream_cache_status; } } } ``` ### Microcaching Impact | Requests/sec | Without Cache | 1s Microcache | Reduction | |-------------|---------------|---------------|-----------| | 100 | 100 backend hits/s | 1 backend hit/s | 99% | | 1,000 | 1,000 backend hits/s | 1 backend hit/s | 99.9% | | 10,000 | 10,000 backend hits/s | 1 backend hit/s | 99.99% | --- ## HTTP/2 ### Basic HTTP/2 Configuration ```nginx server { # http2 directive (Nginx 1.25.1+) listen 443 ssl; http2 on; # For older Nginx versions: # listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # HTTP/2 specific settings http2_max_concurrent_streams 128; http2_recv_buffer_size 256k; } ``` ### HTTP/2 Benefits | Feature | HTTP/1.1 | HTTP/2 | |---------|---------|--------| | Multiplexing | 6 connections per domain | Unlimited streams on 1 connection | | Header compression | None | HPACK compression | | Server push | Not possible | Supported (but deprecated) | | Stream priority | N/A | Priority hints | | Binary protocol | Text-based | Binary framing | ### HTTP/2 Server Push (Deprecated) Server push was removed from Chrome and is generally considered deprecated. Use `<link rel="preload">` or `103 Early Hints` instead. ```nginx # 103 Early Hints (modern alternative to server push) location / { # Send early hints before the main response add_header Link "</style.css>; rel=preload; as=style" early; add_header Link "</app.js>; rel=preload; as=script" early; proxy_pass http://backend; } ``` --- ## HTTP/3 (QUIC) HTTP/3 uses QUIC (UDP-based transport) for faster connection establishment and better performance on lossy networks. ### Basic HTTP/3 Configuration Requires Nginx 1.25.0+ compiled with QUIC support, or nginx-quic branch. ```nginx server { # Standard HTTPS (HTTP/1.1 and HTTP/2) listen 443 ssl; http2 on; # HTTP/3 via QUIC (UDP) listen 443 quic reuseport; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # Required: TLS 1.3 only for QUIC ssl_protocols TLSv1.2 TLSv1.3; # Advertise HTTP/3 support via Alt-Svc header add_header Alt-Svc 'h3=":443"; ma=86400' always; # QUIC-specific settings quic_retry on; # Enable address validation ssl_early_data on; # Enable 0-RTT (with replay protection) # Required for QUIC ssl_session_tickets on; } ``` ### Firewall Configuration for QUIC ```bash # Allow UDP port 443 for QUIC sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT # Or with firewalld sudo firewall-cmd --permanent --add-port=443/udp sudo firewall-cmd --reload # Or with ufw sudo ufw allow 443/udp ``` ### HTTP/3 Benefits | Feature | HTTP/2 (TCP) | HTTP/3 (QUIC) | |---------|-------------|---------------| | Connection setup | 2-3 RTT (TCP + TLS) | 0-1 RTT | | Head-of-line blocking | Yes (TCP level) | No (per-stream) | | Connection migration | No (IP changes break) | Yes (connection ID) | | Packet loss handling | All streams blocked | Only affected stream | | 0-RTT resumption | TLS 1.3 only | Built-in | --- ## Connection Draining ### Graceful Reload ```bash # Graceful reload: new workers start, old workers finish existing requests sudo nginx -s reload # What happens: # 1. Master process reads new config # 2. Starts new worker processes with new config # 3. Old workers stop accepting new connections # 4. Old workers finish processing existing requests # 5. Old workers exit ``` ### Worker Shutdown Timeout ```nginx # Maximum time for old workers to finish requests during reload # After this timeout, old workers are forcefully terminated worker_shutdown_timeout 30s; ``` ### Zero-Downtime Deployment ```bash # 1. Deploy new application code # 2. Signal nginx to reload config sudo nginx -t && sudo nginx -s reload # Or with systemd sudo nginx -t && sudo systemctl reload nginx ``` ### Upstream Draining ```nginx upstream backend { server 127.0.0.1:3000; # Mark server as draining (finish existing, no new) server 127.0.0.1:3001 down; # Use 'down' to stop new traffic server 127.0.0.1:3002; } ``` --- ## Monitoring ### stub_status Module ```nginx server { listen 8080; # Restrict to internal access allow 127.0.0.1; allow 10.0.0.0/8; deny all; location /nginx_status { stub_status; } } ``` Output: ``` Active connections: 291 server accepts handled requests 16630948 16630948 31070465 Reading: 6 Writing: 179 Waiting: 106 ``` | Metric | Meaning | |--------|---------| | Active connections | Current active client connections (including waiting) | | accepts | Total accepted connections | | handled | Total handled connections (should equal accepts) | | requests | Total client requests | | Reading | Connections where nginx is reading the request header | | Writing | Connections where nginx is writing response to client | | Waiting | Idle keepalive connections | ### Request Timing Variables Use these in log formats for performance monitoring. ```nginx http { log_format performance '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'rt=$request_time ' 'urt=$upstream_response_time ' 'uct=$upstream_connect_time ' 'uht=$upstream_header_time ' 'cs=$upstream_cache_status'; access_log /var/log/nginx/performance.log performance; } ``` ### Timing Variable Reference | Variable | Meaning | |----------|---------| | `$request_time` | Total time from first byte read to last byte sent (seconds, ms resolution) | | `$upstream_response_time` | Time from establishing upstream connection to receiving last byte | | `$upstream_connect_time` | Time to establish connection to upstream server | | `$upstream_header_time` | Time from connection to receiving response headers from upstream | | `$upstream_cache_status` | HIT, MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED | ### Conditional Logging ```nginx # Only log slow requests (> 1 second) map $request_time $loggable_slow { ~^[0-9]*\.[0-9]$ 0; # < 1 second default 1; # >= 1 second } access_log /var/log/nginx/slow.log performance if=$loggable_slow; # Don't log health checks map $request_uri $loggable { /health 0; /ping 0; default 1; } access_log /var/log/nginx/access.log combined if=$loggable; ``` ### JSON Log Format Easier to parse with log aggregation tools (ELK, Loki, etc.). ```nginx log_format json_combined escape=json '{' '"time":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"request_method":"$request_method",' '"request_uri":"$request_uri",' '"status":$status,' '"body_bytes_sent":$body_bytes_sent,' '"request_time":$request_time,' '"upstream_response_time":"$upstream_response_time",' '"upstream_cache_status":"$upstream_cache_status",' '"http_referrer":"$http_referer",' '"http_user_agent":"$http_user_agent",' '"server_name":"$server_name"' '}'; access_log /var/log/nginx/access.json json_combined; ``` ### Integration with Prometheus Use the `nginx-prometheus-exporter` for Prometheus/Grafana monitoring. ```bash # Run nginx-prometheus-exporter ./nginx-prometheus-exporter -nginx.scrape-uri=http://127.0.0.1:8080/nginx_status ``` Or use the VTS (Virtual Host Traffic Status) module for more detailed metrics: ```nginx # Requires ngx_http_vhost_traffic_status_module http { vhost_traffic_status_zone; server { listen 8080; location /status { vhost_traffic_status_display; vhost_traffic_status_display_format prometheus; } } } ``` ### Quick Health Check Script ```bash #!/bin/bash # nginx-health.sh - Quick nginx health check NGINX_STATUS="http://127.0.0.1:8080/nginx_status" RESPONSE=$(curl -s "$NGINX_STATUS") ACTIVE=$(echo "$RESPONSE" | rg -o 'Active connections: (\d+)' -r '$1') WAITING=$(echo "$RESPONSE" | rg -o 'Waiting: (\d+)' -r '$1') READING=$(echo "$RESPONSE" | rg -o 'Reading: (\d+)' -r '$1') WRITING=$(echo "$RESPONSE" | rg -o 'Writing: (\d+)' -r '$1') echo "Active: $ACTIVE | Reading: $READING | Writing: $WRITING | Waiting: $WAITING" # Alert if active connections exceed threshold if [ "$ACTIVE" -gt 5000 ]; then echo "WARNING: High connection count: $ACTIVE" fi ``` -
reverse-proxy.md 26.2 KB
# Reverse Proxy Reference Comprehensive guide to Nginx reverse proxy configuration: upstream blocks, load balancing algorithms, proxy headers, WebSocket and gRPC proxying, caching, and production-ready configurations. --- ## Table of Contents 1. [Upstream Blocks](#upstream-blocks) 2. [Load Balancing Algorithms](#load-balancing-algorithms) 3. [Health Checks](#health-checks) 4. [Proxy Headers](#proxy-headers) 5. [WebSocket Proxy](#websocket-proxy) 6. [gRPC Proxy](#grpc-proxy) 7. [Proxy Caching](#proxy-caching) 8. [Proxy Buffering](#proxy-buffering) 9. [Keepalive Connections](#keepalive-connections) 10. [Timeout Configuration](#timeout-configuration) 11. [Real-World Configurations](#real-world-configurations) --- ## Upstream Blocks An `upstream` block defines a group of backend servers that Nginx can proxy requests to. ### Basic Upstream ```nginx upstream backend { server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } server { listen 80; location / { proxy_pass http://backend; } } ``` ### Server Directive Parameters ```nginx upstream backend { # Basic server server 127.0.0.1:3000; # Weighted server - receives 3x traffic server 127.0.0.1:3001 weight=3; # Backup server - only used when all primary servers are down server 127.0.0.1:3002 backup; # Mark server as permanently unavailable server 127.0.0.1:3003 down; # Failure detection: after 3 fails within 30s, mark unavailable for 30s server 127.0.0.1:3004 max_fails=3 fail_timeout=30s; # Limit concurrent connections to this server server 127.0.0.1:3005 max_conns=100; # Unix socket backend server unix:/var/run/app.sock; # Resolve hostname (requires resolver directive) server backend.service.consul resolve; } ``` ### Parameter Reference | Parameter | Default | Description | |-----------|---------|-------------| | `weight=N` | 1 | Relative weight for weighted load balancing | | `max_fails=N` | 1 | Number of failed attempts before marking unavailable | | `fail_timeout=T` | 10s | Time to consider fails AND duration to mark unavailable | | `backup` | - | Only used when all non-backup servers are unavailable | | `down` | - | Permanently marks server as unavailable | | `max_conns=N` | 0 (unlimited) | Maximum concurrent connections to this server | | `resolve` | - | Monitor DNS changes and update upstream automatically | | `slow_start=T` | 0 | Gradually increase traffic to recovered server (Nginx Plus) | --- ## Load Balancing Algorithms ### Round-Robin (Default) Distributes requests sequentially across servers. No directive needed. ```nginx upstream backend { # Round-robin is the default - no directive needed server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` **Use when:** Backends are homogeneous and requests have similar processing time. ### Weighted Round-Robin ```nginx upstream backend { server 127.0.0.1:3000 weight=5; # Gets 5/8 of requests server 127.0.0.1:3001 weight=2; # Gets 2/8 of requests server 127.0.0.1:3002 weight=1; # Gets 1/8 of requests } ``` **Use when:** Backends have different capacity (CPU, memory). ### Least Connections Routes to the server with the fewest active connections. ```nginx upstream backend { least_conn; server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` **Use when:** Requests have variable processing time. Prevents slow requests from piling up on one server. ### IP Hash Routes requests from the same client IP to the same backend server (session persistence). ```nginx upstream backend { ip_hash; server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` **Use when:** Application requires sticky sessions and you can't use external session storage. **Caveat:** If clients are behind a NAT/proxy, many IPs map to one, causing uneven distribution. ### Random Randomly selects a server for each request. ```nginx upstream backend { random; server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` ### Random with Two Choices Picks two servers at random, then selects the one with fewer connections (power of two choices). ```nginx upstream backend { random two least_conn; server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` **Use when:** Large number of backends where least_conn coordination overhead is high. ### Hash (Consistent Hashing) Map requests to servers based on a configurable key. ```nginx upstream backend { hash $request_uri consistent; server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; } ``` **Use when:** You want cache affinity (same URLs always go to the same backend for better cache hit rates). The `consistent` parameter uses ketama consistent hashing, which minimizes redistribution when servers are added/removed. ### Algorithm Comparison | Algorithm | Session Persistence | Even Distribution | Variable Request Time | Best For | |-----------|--------------------|--------------------|----------------------|----------| | Round-robin | No | Yes (uniform) | Poor | Homogeneous backends | | Weighted | No | Yes (proportional) | Poor | Mixed-capacity backends | | Least connections | No | Adapts to load | Good | Variable processing time | | IP hash | Yes (by IP) | Depends on IPs | Poor | Sticky sessions | | Random two | No | Good | Good | Large clusters | | Hash | Yes (by key) | Depends on keys | Poor | Cache affinity | --- ## Health Checks ### Passive Health Checks (Open Source) Nginx detects unhealthy backends based on failed request attempts. ```nginx upstream backend { server 127.0.0.1:3000 max_fails=3 fail_timeout=30s; server 127.0.0.1:3001 max_fails=3 fail_timeout=30s; server 127.0.0.1:3002 max_fails=3 fail_timeout=30s; } ``` **How it works:** 1. If a server fails `max_fails` times within `fail_timeout` seconds, it's marked unavailable 2. After `fail_timeout` seconds, Nginx sends one test request 3. If the test succeeds, the server is marked available again **What counts as a failure:** Controlled by `proxy_next_upstream`: ```nginx location / { proxy_pass http://backend; # What errors trigger failover to next upstream proxy_next_upstream error timeout http_502 http_503 http_504; # Limit retries across upstream servers proxy_next_upstream_tries 3; # Limit total time for all retries proxy_next_upstream_timeout 10s; } ``` ### Active Health Checks (Nginx Plus or Third-Party) For open-source Nginx, use the `nginx_upstream_check_module` (third-party) or external health check tools. ```nginx # Nginx Plus active health check upstream backend { zone backend_zone 64k; # Required for active checks server 127.0.0.1:3000; server 127.0.0.1:3001; } server { location / { proxy_pass http://backend; health_check interval=5s fails=3 passes=2 uri=/health; } } ``` ### Application Health Endpoint Pattern ```nginx # Backend should implement /health returning 200 location /health { proxy_pass http://backend; access_log off; # Don't clutter logs proxy_connect_timeout 2s; # Fail fast proxy_read_timeout 2s; } ``` --- ## Proxy Headers ### Essential Headers Every reverse proxy should forward these headers so the backend knows about the original request. ```nginx location / { proxy_pass http://backend; # Pass the original Host header proxy_set_header Host $host; # Client's real IP address proxy_set_header X-Real-IP $remote_addr; # Append to existing forwarded-for chain proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Original protocol (http or https) proxy_set_header X-Forwarded-Proto $scheme; # Original port proxy_set_header X-Forwarded-Port $server_port; } ``` ### Header Reference | Header | Variable | Purpose | |--------|----------|---------| | `Host` | `$host` | Original hostname from client request | | `X-Real-IP` | `$remote_addr` | Client's IP address | | `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | Chain of proxy IPs | | `X-Forwarded-Proto` | `$scheme` | Original protocol (http/https) | | `X-Forwarded-Port` | `$server_port` | Original port number | | `X-Request-ID` | `$request_id` | Unique request identifier for tracing | | `Connection` | `""` | Clear hop-by-hop header for keepalive | ### Reusable Headers Include Create a shared include file to avoid repetition: ```nginx # /etc/nginx/includes/proxy-headers.conf 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-Port $server_port; proxy_set_header X-Request-ID $request_id; ``` ```nginx # Usage in server/location blocks location / { proxy_pass http://backend; include /etc/nginx/includes/proxy-headers.conf; } ``` ### Hiding Backend Headers ```nginx location / { proxy_pass http://backend; # Remove headers that leak backend info proxy_hide_header X-Powered-By; proxy_hide_header Server; proxy_hide_header X-AspNet-Version; # Pass through headers that are hidden by default proxy_pass_header X-Custom-Header; } ``` --- ## WebSocket Proxy WebSocket requires HTTP/1.1 with the Upgrade mechanism. ### Basic WebSocket Proxy ```nginx # Map to handle Upgrade header map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name ws.example.com; location /ws/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; # WebSocket upgrade headers proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; # 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; # Long timeouts for persistent connections proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } ``` ### WebSocket with SSL ```nginx server { listen 443 ssl http2; server_name ws.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; location /ws/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $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_read_timeout 3600s; proxy_send_timeout 3600s; } } ``` ### Socket.IO Configuration Socket.IO uses both WebSocket and HTTP long-polling: ```nginx location /socket.io/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $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; # Important for Socket.IO long-polling fallback proxy_buffering off; proxy_cache off; } ``` --- ## gRPC Proxy ### Basic gRPC Proxy ```nginx upstream grpc_backend { server 127.0.0.1:50051; } server { listen 443 ssl http2; server_name grpc.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; location / { # Use grpc_pass for gRPC backends grpc_pass grpc://grpc_backend; # gRPC-specific headers grpc_set_header X-Real-IP $remote_addr; grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` ### gRPC with TLS to Backend ```nginx location / { # grpcs:// for TLS-encrypted gRPC backends grpc_pass grpcs://grpc_backend; grpc_ssl_certificate /etc/nginx/certs/client.pem; grpc_ssl_certificate_key /etc/nginx/certs/client.key; grpc_ssl_verify on; grpc_ssl_trusted_certificate /etc/nginx/certs/ca.pem; } ``` ### gRPC Error Handling ```nginx location / { grpc_pass grpc://grpc_backend; # Intercept gRPC errors and return custom responses grpc_intercept_errors on; error_page 502 = /error502grpc; } location = /error502grpc { internal; default_type application/grpc; add_header grpc-status 14; add_header grpc-message "Backend unavailable"; return 204; } ``` --- ## Proxy Caching ### Basic Cache Configuration ```nginx http { # Define cache storage # levels=1:2 - Two-level directory hierarchy # keys_zone=cache:10m - 10MB shared memory for cache keys (~80,000 keys) # max_size=10g - Maximum cache size on disk # inactive=60m - Remove items not accessed in 60 minutes # use_temp_path=off - Write directly to cache dir (better performance) proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=10g inactive=60m use_temp_path=off; server { listen 80; location / { proxy_pass http://backend; # Enable caching with the named zone proxy_cache app_cache; # Cache different status codes for different durations proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_valid any 5m; # Custom cache key proxy_cache_key "$scheme$request_method$host$request_uri"; # Add header to show cache status (HIT, MISS, BYPASS, etc.) add_header X-Cache-Status $upstream_cache_status; } } } ``` ### Cache Bypass ```nginx location / { proxy_pass http://backend; proxy_cache app_cache; proxy_cache_valid 200 10m; # Bypass cache when specific conditions are met proxy_cache_bypass $http_cache_control; # Client sends Cache-Control proxy_cache_bypass $cookie_nocache; # Cookie "nocache" is set proxy_cache_bypass $arg_nocache; # Query param ?nocache=1 # Don't store in cache under these conditions proxy_no_cache $http_pragma; # Client sends Pragma: no-cache proxy_no_cache $arg_nocache; } ``` ### Stale Cache (Serve Old Content During Errors) ```nginx location / { proxy_pass http://backend; proxy_cache app_cache; proxy_cache_valid 200 10m; # Serve stale content when backend is down or slow proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; # Update cache in background while serving stale proxy_cache_background_update on; # Only one request refreshes cache, others get stale proxy_cache_lock on; proxy_cache_lock_timeout 5s; } ``` ### Cache Purge ```nginx # Requires ngx_cache_purge module location ~ /purge(/.*) { allow 127.0.0.1; deny all; proxy_cache_purge app_cache "$scheme$request_method$host$1"; } ``` Usage: `curl -X PURGE https://example.com/purge/api/data` --- ## Proxy Buffering ### Buffering On (Default) Nginx reads the entire response from the backend, then sends it to the client. Good for fast backends with slow clients. ```nginx location / { proxy_pass http://backend; # Buffering on (default) proxy_buffering on; # Size of the buffer for the first part of the response (headers) proxy_buffer_size 8k; # Number and size of buffers for the response body proxy_buffers 8 16k; # Maximum size that can be busy sending to client proxy_busy_buffers_size 32k; # Temporary files if response exceeds buffers proxy_temp_file_write_size 64k; proxy_max_temp_file_size 1024m; } ``` ### Buffering Off Send data to client as soon as it arrives from backend. Required for streaming. ```nginx location /stream/ { proxy_pass http://backend; # Disable buffering for streaming responses proxy_buffering off; # Also disable request body buffering proxy_request_buffering off; } ``` **Disable buffering for:** - Server-Sent Events (SSE) - Long-polling - Streaming downloads - Real-time data feeds - Large file downloads where you want immediate start ### Server-Sent Events (SSE) Configuration ```nginx location /events/ { proxy_pass http://backend; proxy_http_version 1.1; # Critical for SSE proxy_buffering off; proxy_cache off; # Don't add compression (breaks streaming) proxy_set_header Accept-Encoding ""; # Keep connection alive proxy_set_header Connection ""; proxy_read_timeout 86400s; # Chunked transfer chunked_transfer_encoding on; } ``` --- ## Keepalive Connections Reuse connections to upstream servers instead of opening a new TCP connection per request. ### Upstream Keepalive ```nginx upstream backend { server 127.0.0.1:3000; server 127.0.0.1:3001; # Keep up to 32 idle connections alive per worker process keepalive 32; # Maximum requests per keepalive connection before closing keepalive_requests 1000; # Idle timeout for keepalive connections keepalive_timeout 60s; } server { location / { proxy_pass http://backend; # Required for keepalive to work with upstream proxy_http_version 1.1; proxy_set_header Connection ""; # Standard 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; } } ``` **Important:** `proxy_http_version 1.1` and `proxy_set_header Connection ""` are both required. HTTP/1.0 uses `Connection: close` by default, which prevents keepalive. ### Keepalive Sizing | Scenario | `keepalive` Value | Rationale | |----------|-------------------|-----------| | Low traffic | 8-16 | Minimal idle connections | | Medium traffic | 32-64 | Balance memory vs connection reuse | | High traffic | 128-256 | Maximize connection reuse | | Microservices | 16-32 per upstream | Per-service pools | --- ## Timeout Configuration ### Complete Timeout Reference ```nginx location / { proxy_pass http://backend; # Time to establish connection to backend proxy_connect_timeout 5s; # Default: 60s # Time to wait for backend to start sending response proxy_read_timeout 60s; # Default: 60s # Time allowed to send request body to backend proxy_send_timeout 60s; # Default: 60s } ``` ### Timeout Guidelines | Timeout | Typical Value | Use Case | |---------|---------------|----------| | `proxy_connect_timeout` | 3-5s | Fail fast if backend is unreachable | | `proxy_read_timeout` | 30-60s | API responses, page rendering | | `proxy_read_timeout` | 300s+ | File uploads, long reports | | `proxy_read_timeout` | 3600s | WebSocket, SSE | | `proxy_send_timeout` | 30-60s | Most applications | ### Per-Location Timeouts ```nginx # Fast API endpoint location /api/ { proxy_pass http://backend; proxy_connect_timeout 3s; proxy_read_timeout 10s; } # File upload endpoint location /upload/ { proxy_pass http://backend; proxy_connect_timeout 5s; proxy_read_timeout 300s; client_max_body_size 100m; } # Report generation (slow) location /reports/ { proxy_pass http://backend; proxy_connect_timeout 5s; proxy_read_timeout 600s; } ``` --- ## Real-World Configurations ### Node.js Application ```nginx upstream nodejs { server 127.0.0.1:3000; keepalive 32; } server { listen 443 ssl http2; server_name app.example.com; ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; include /etc/nginx/includes/ssl-params.conf; # Security headers add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; # Proxy to Node.js location / { proxy_pass http://nodejs; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; proxy_connect_timeout 5s; proxy_read_timeout 60s; # Handle large JWT tokens proxy_buffer_size 16k; proxy_buffers 4 32k; } # Serve static files directly (bypass Node.js) location /static/ { alias /var/www/app/public/; expires 30d; add_header Cache-Control "public, immutable"; access_log off; } # WebSocket endpoint location /ws { proxy_pass http://nodejs; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; } } ``` ### Python/Gunicorn Application ```nginx upstream gunicorn { server unix:/run/gunicorn/app.sock fail_timeout=10s; keepalive 16; } server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; include /etc/nginx/includes/ssl-params.conf; client_max_body_size 10m; location / { proxy_pass http://gunicorn; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_buffer_size 8k; proxy_buffers 4 16k; } # Django static files location /static/ { alias /var/www/app/staticfiles/; expires 30d; access_log off; } # Django media uploads location /media/ { alias /var/www/app/media/; expires 7d; } } ``` ### Go Binary Application ```nginx upstream goapp { server 127.0.0.1:8080; server 127.0.0.1:8081; keepalive 64; } server { listen 443 ssl http2; server_name service.example.com; ssl_certificate /etc/letsencrypt/live/service.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/service.example.com/privkey.pem; include /etc/nginx/includes/ssl-params.conf; # Go apps typically serve their own static files location / { proxy_pass http://goapp; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; proxy_connect_timeout 3s; proxy_read_timeout 30s; # Go apps handle large payloads efficiently proxy_request_buffering off; } # Health check location /healthz { proxy_pass http://goapp; access_log off; proxy_connect_timeout 2s; proxy_read_timeout 2s; } } ``` ### PHP-FPM Application ```nginx upstream php-fpm { server unix:/run/php/php8.3-fpm.sock; } server { listen 443 ssl http2; server_name site.example.com; ssl_certificate /etc/letsencrypt/live/site.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/site.example.com/privkey.pem; include /etc/nginx/includes/ssl-params.conf; root /var/www/site/public; index index.php index.html; client_max_body_size 50m; # Try static file first, then directory, then PHP location / { try_files $uri $uri/ /index.php?$query_string; } # PHP processing location ~ \.php$ { fastcgi_pass php-fpm; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_connect_timeout 5s; fastcgi_send_timeout 30s; fastcgi_read_timeout 30s; fastcgi_buffer_size 16k; fastcgi_buffers 4 16k; } # Deny access to hidden files (except .well-known) location ~ /\.(?!well-known) { deny all; } # Static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } } ``` ### Multiple Services on One Domain (Path-Based Routing) ```nginx # Upstream definitions for each service upstream api_service { server 127.0.0.1:3000; keepalive 32; } upstream admin_service { server 127.0.0.1:4000; keepalive 16; } upstream docs_service { server 127.0.0.1:5000; keepalive 8; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; include /etc/nginx/includes/ssl-params.conf; # API service at /api/ location /api/ { proxy_pass http://api_service/; # Trailing / strips /api/ prefix proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; # API-specific settings proxy_read_timeout 30s; client_max_body_size 10m; # Rate limiting for API limit_req zone=api burst=20 nodelay; } # Admin panel at /admin/ location /admin/ { proxy_pass http://admin_service/; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; # Restrict admin access by IP allow 10.0.0.0/8; allow 192.168.0.0/16; deny all; } # Documentation at /docs/ location /docs/ { proxy_pass http://docs_service/; proxy_http_version 1.1; proxy_set_header Connection ""; include /etc/nginx/includes/proxy-headers.conf; # Cache documentation pages proxy_cache app_cache; proxy_cache_valid 200 1h; } # Frontend SPA (catch-all) location / { root /var/www/frontend/dist; index index.html; try_files $uri $uri/ /index.html; } } ``` ### Proxy to Multiple Ports with Subdomain Routing ```nginx # Alternative: subdomain-based routing server { listen 443 ssl http2; server_name api.example.com; include /etc/nginx/includes/ssl-params.conf; location / { proxy_pass http://api_service; include /etc/nginx/includes/proxy-headers.conf; } } server { listen 443 ssl http2; server_name admin.example.com; include /etc/nginx/includes/ssl-params.conf; location / { proxy_pass http://admin_service; include /etc/nginx/includes/proxy-headers.conf; } } ``` -
ssl-security.md 24 KB
# SSL/TLS & Security Reference Comprehensive guide to Nginx SSL/TLS configuration, Let's Encrypt automation, security headers, rate limiting, access control, and mutual TLS. --- ## Table of Contents 1. [TLS Configuration](#tls-configuration) 2. [Let's Encrypt & Certbot](#lets-encrypt--certbot) 3. [Certificate Management](#certificate-management) 4. [HSTS](#hsts) 5. [OCSP Stapling](#ocsp-stapling) 6. [Security Headers](#security-headers) 7. [Rate Limiting](#rate-limiting) 8. [IP Restrictions](#ip-restrictions) 9. [Basic Authentication](#basic-authentication) 10. [Mutual TLS (mTLS)](#mutual-tls-mtls) 11. [HTTP to HTTPS Redirect](#http-to-https-redirect) --- ## TLS Configuration ### Modern Configuration (TLS 1.3 Only) For services where all clients support TLS 1.3 (modern browsers, API clients you control). ```nginx server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # TLS 1.3 only ssl_protocols TLSv1.3; # TLS 1.3 ciphers are not configurable via ssl_ciphers # They are negotiated automatically: # TLS_AES_256_GCM_SHA384 # TLS_CHACHA20_POLY1305_SHA256 # TLS_AES_128_GCM_SHA256 ssl_prefer_server_ciphers off; } ``` ### Intermediate Configuration (TLS 1.2 + 1.3) Recommended for most production sites. Compatible with all modern browsers. ```nginx server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # TLS 1.2 and 1.3 ssl_protocols TLSv1.2 TLSv1.3; # Cipher suite for TLS 1.2 (TLS 1.3 ciphers are automatic) 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; # DH parameters for DHE ciphers ssl_dhparam /etc/nginx/dhparam.pem; # Session caching ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; } ``` ### Generate DH Parameters ```bash # Generate 4096-bit DH parameters (takes several minutes) openssl dhparam -out /etc/nginx/dhparam.pem 4096 # Or use pre-generated params from Mozilla (faster, still secure) curl -sL https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/nginx/dhparam.pem ``` ### SSL Session Configuration ```nginx # Shared session cache across all worker processes # 10m = 10MB, enough for ~40,000 sessions ssl_session_cache shared:SSL:10m; # Session lifetime ssl_session_timeout 1d; # Disable session tickets (better forward secrecy) # Enable only if you rotate ticket keys regularly ssl_session_tickets off; ``` ### TLS Version Comparison | Version | Status | Performance | Security | Support | |---------|--------|-------------|----------|---------| | TLS 1.0 | Deprecated | Slow | Weak | Drop immediately | | TLS 1.1 | Deprecated | Slow | Weak | Drop immediately | | TLS 1.2 | Active | Good | Strong | All modern browsers | | TLS 1.3 | Preferred | Best (0-RTT) | Strongest | 95%+ browsers | --- ## Let's Encrypt & Certbot ### Installation ```bash # Debian/Ubuntu sudo apt update sudo apt install certbot python3-certbot-nginx # RHEL/Fedora sudo dnf install certbot python3-certbot-nginx # Alpine sudo apk add certbot certbot-nginx # Snap (universal) sudo snap install --classic certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot ``` ### Obtaining Certificates #### Nginx Plugin (Easiest) Certbot automatically modifies your nginx config. ```bash # Single domain sudo certbot --nginx -d example.com # Multiple domains sudo certbot --nginx -d example.com -d www.example.com -d api.example.com # Non-interactive (for automation) sudo certbot --nginx --non-interactive --agree-tos \ --email admin@example.com -d example.com ``` #### Webroot Method (No Restart) Use when you don't want certbot to modify your nginx config. ```nginx # Add this to your nginx server block first location /.well-known/acme-challenge/ { root /var/www/certbot; } ``` ```bash sudo certbot certonly --webroot -w /var/www/certbot -d example.com ``` #### Standalone Method Certbot runs its own temporary web server (requires port 80 to be free). ```bash # Stop nginx first sudo systemctl stop nginx sudo certbot certonly --standalone -d example.com # Restart nginx sudo systemctl start nginx ``` #### DNS Challenge (Wildcard Certificates) Required for wildcard certificates (`*.example.com`). ```bash # Manual DNS challenge sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" # With DNS plugin (Cloudflare example) sudo certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d example.com -d "*.example.com" ``` Cloudflare credentials file: ```ini # /etc/letsencrypt/cloudflare.ini dns_cloudflare_api_token = your-api-token-here ``` ```bash # Secure the credentials file sudo chmod 600 /etc/letsencrypt/cloudflare.ini ``` ### Auto-Renewal #### Systemd Timer (Recommended) Certbot usually installs this automatically. ```ini # /etc/systemd/system/certbot.timer [Unit] Description=Run certbot twice daily [Timer] OnCalendar=*-*-* 00,12:00:00 RandomizedDelaySec=3600 Persistent=true [Install] WantedBy=timers.target ``` ```ini # /etc/systemd/system/certbot.service [Unit] Description=Certbot renewal [Service] Type=oneshot ExecStart=/usr/bin/certbot renew --quiet ``` ```bash # Enable and start sudo systemctl enable certbot.timer sudo systemctl start certbot.timer # Check status sudo systemctl list-timers certbot.timer ``` #### Cron Alternative ```bash # /etc/cron.d/certbot 0 0,12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx" ``` ### Renewal Hooks ```bash # Test renewal with hooks sudo certbot renew --dry-run \ --pre-hook "echo 'Before renewal'" \ --post-hook "systemctl reload nginx" \ --deploy-hook "echo 'Certificate renewed'" # Hook scripts (placed in /etc/letsencrypt/renewal-hooks/) # /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh #!/bin/bash systemctl reload nginx ``` ```bash chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh ``` ### Certificate File Locations ``` /etc/letsencrypt/live/example.com/ ├── cert.pem # Domain certificate only ├── chain.pem # Intermediate CA certificate(s) ├── fullchain.pem # cert.pem + chain.pem (use this for ssl_certificate) ├── privkey.pem # Private key (use this for ssl_certificate_key) └── README ``` --- ## Certificate Management ### Certificate Chain Configuration ```nginx # fullchain.pem includes: domain cert + intermediate CA cert(s) ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Trusted certificate for OCSP stapling verification ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; ``` ### Verify Certificate Chain ```bash # Check certificate details openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout # Verify chain openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \ /etc/letsencrypt/live/example.com/cert.pem # Check expiration openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate # Test SSL from outside openssl s_client -connect example.com:443 -servername example.com ``` ### Multiple Certificates (RSA + ECDSA) Serve different certificate types for maximum compatibility and performance. ```nginx server { listen 443 ssl http2; server_name example.com; # RSA certificate (compatibility) ssl_certificate /etc/nginx/certs/example.com-rsa.pem; ssl_certificate_key /etc/nginx/certs/example.com-rsa.key; # ECDSA certificate (performance) - Nginx picks the best one ssl_certificate /etc/nginx/certs/example.com-ecdsa.pem; ssl_certificate_key /etc/nginx/certs/example.com-ecdsa.key; } ``` --- ## HSTS HTTP Strict Transport Security tells browsers to always use HTTPS for this domain. ### Basic HSTS ```nginx # 2-year max-age (recommended for production) add_header Strict-Transport-Security "max-age=63072000" always; ``` ### HSTS with Subdomains ```nginx # Apply to all subdomains as well add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; ``` ### HSTS Preload Submit to browser preload list (permanently enforced, difficult to undo). ```nginx add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; ``` **Before enabling preload:** 1. Ensure ALL subdomains support HTTPS 2. Start with a short `max-age` (e.g., 300) and test 3. Submit at https://hstspreload.org/ ### Gradual HSTS Rollout ```nginx # Step 1: Short max-age, monitor for issues (1 week) add_header Strict-Transport-Security "max-age=604800" always; # Step 2: Increase to 1 month add_header Strict-Transport-Security "max-age=2592000" always; # Step 3: Include subdomains add_header Strict-Transport-Security "max-age=2592000; includeSubDomains" always; # Step 4: Full production (2 years + preload) add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; ``` --- ## OCSP Stapling OCSP stapling embeds the certificate's revocation status in the TLS handshake, improving connection speed and privacy. ```nginx server { listen 443 ssl http2; server_name example.com; # Enable OCSP stapling ssl_stapling on; # Verify OCSP response using trusted CA cert ssl_stapling_verify on; # CA cert chain for verification (intermediate + root) ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; # DNS resolver for OCSP responder lookup resolver 1.1.1.1 8.8.8.8 valid=300s; resolver_timeout 5s; } ``` ### Verify OCSP Stapling ```bash # Test OCSP stapling openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null | \ grep -A 17 "OCSP Response Status" # Should show: "OCSP Response Status: successful (0x0)" ``` --- ## Security Headers ### Complete Security Headers Configuration ```nginx # /etc/nginx/includes/security-headers.conf # Prevent MIME type sniffing add_header X-Content-Type-Options nosniff always; # Clickjacking protection add_header X-Frame-Options DENY always; # Or allow same-origin framing: # add_header X-Frame-Options SAMEORIGIN always; # XSS Protection (legacy browsers) add_header X-XSS-Protection "1; mode=block" always; # Referrer Policy add_header Referrer-Policy strict-origin-when-cross-origin always; # Permissions Policy (formerly Feature-Policy) add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; # Content Security Policy add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always; # Cross-Origin policies add_header Cross-Origin-Opener-Policy same-origin always; add_header Cross-Origin-Resource-Policy same-origin always; add_header Cross-Origin-Embedder-Policy require-corp always; ``` ### Usage ```nginx server { listen 443 ssl http2; server_name example.com; include /etc/nginx/includes/security-headers.conf; # ... rest of config } ``` ### Header Reference | Header | Value | Purpose | |--------|-------|---------| | `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing | | `X-Frame-Options` | `DENY` or `SAMEORIGIN` | Prevent clickjacking | | `X-XSS-Protection` | `1; mode=block` | Legacy XSS filter | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer leakage | | `Permissions-Policy` | `camera=(), ...` | Disable browser features | | `Content-Security-Policy` | `default-src 'self'; ...` | Control resource loading | | `Strict-Transport-Security` | `max-age=63072000; ...` | Force HTTPS | | `Cross-Origin-Opener-Policy` | `same-origin` | Isolate browsing context | | `Cross-Origin-Resource-Policy` | `same-origin` | Prevent cross-origin reads | ### Content-Security-Policy Examples ```nginx # Minimal CSP (strict) add_header Content-Security-Policy "default-src 'self';" always; # With Google Fonts and Analytics add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://www.google-analytics.com;" always; # API-only (no HTML rendering) add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none';" always; # Report-only mode (for testing) add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always; ``` --- ## Rate Limiting ### Basic Rate Limiting ```nginx http { # Define rate limit zone # $binary_remote_addr = client IP (compact binary, 4 or 16 bytes) # zone=name:size = shared memory zone name and size # rate=10r/s = 10 requests per second limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; server { location / { # Apply rate limit # burst=20 = allow 20 excess requests to queue # nodelay = process burst immediately (don't throttle) limit_req zone=general burst=20 nodelay; # Custom status code (default is 503) limit_req_status 429; proxy_pass http://backend; } } } ``` ### Multiple Rate Limit Zones ```nginx http { # Global rate limit: 30 req/s per IP limit_req_zone $binary_remote_addr zone=global:10m rate=30r/s; # Login rate limit: 5 req/min per IP limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # API rate limit: by API key limit_req_zone $http_x_api_key zone=api:10m rate=100r/s; server { # Global limit applies everywhere limit_req zone=global burst=50 nodelay; location /api/login { # Stricter limit for login endpoint limit_req zone=login burst=3 nodelay; proxy_pass http://backend; } location /api/ { # API key-based limiting limit_req zone=api burst=200 nodelay; proxy_pass http://backend; } } } ``` ### Connection Limiting Limit the number of simultaneous connections per IP. ```nginx http { # Define connection limit zone limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { # Max 20 simultaneous connections per IP limit_conn conn_limit 20; # Limit bandwidth per connection (useful for downloads) limit_rate 1m; # 1MB/s per connection limit_rate_after 10m; # Full speed for first 10MB location /downloads/ { # Tighter limits for download section limit_conn conn_limit 5; limit_rate 500k; } } } ``` ### Rate Limiting with Whitelisting ```nginx http { # Map to identify whitelisted IPs geo $rate_limit { default 1; 10.0.0.0/8 0; # Internal network 192.168.0.0/16 0; # Private network 203.0.113.50 0; # Monitoring server } # Only apply rate limiting to non-whitelisted IPs map $rate_limit $rate_limit_key { 0 ""; 1 $binary_remote_addr; } limit_req_zone $rate_limit_key zone=api:10m rate=10r/s; server { location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://backend; } } } ``` ### Logging Rate-Limited Requests ```nginx http { limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; # Log rate-limited requests at warn level limit_req_log_level warn; # Custom log format for rate-limited requests log_format ratelimit '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"limit_req_status=$limit_req_status"'; } ``` --- ## IP Restrictions ### Allow/Deny Directives ```nginx location /admin/ { # Allow specific IPs and ranges allow 10.0.0.0/8; allow 192.168.1.0/24; allow 203.0.113.50; # Deny everything else deny all; proxy_pass http://admin_backend; } ``` **Order matters:** Nginx evaluates `allow`/`deny` rules in order and uses the first match. ### Geo Module Map client IP to a variable for conditional logic. ```nginx http { geo $allowed_country { default no; 10.0.0.0/8 yes; # Internal 203.0.0.0/8 yes; # Example allowed range } server { location / { if ($allowed_country = no) { return 403; } proxy_pass http://backend; } } } ``` ### GeoIP2 Module For geo-blocking or geo-routing by country. Requires `ngx_http_geoip2_module` and MaxMind GeoLite2 database. ```nginx # Load GeoIP2 module load_module modules/ngx_http_geoip2_module.so; http { geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb { auto_reload 60m; $geoip2_metadata_country_build metadata build_epoch; $geoip2_data_country_code country iso_code; $geoip2_data_country_name country names en; } # Block specific countries map $geoip2_data_country_code $blocked_country { default no; XX yes; # Replace XX with country code YY yes; } server { if ($blocked_country = yes) { return 403; } } } ``` ### Combining IP and Authentication ```nginx location /admin/ { # Require BOTH IP match AND authentication satisfy all; allow 10.0.0.0/8; deny all; auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://admin_backend; } location /internal/ { # Require EITHER IP match OR authentication satisfy any; allow 10.0.0.0/8; deny all; auth_basic "Internal Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://internal_backend; } ``` --- ## Basic Authentication ### Setup ```bash # Install htpasswd utility sudo apt install apache2-utils # Debian/Ubuntu sudo dnf install httpd-tools # RHEL/Fedora # Create password file with first user sudo htpasswd -c /etc/nginx/.htpasswd admin # Add additional users (no -c flag!) sudo htpasswd /etc/nginx/.htpasswd user2 # Use bcrypt hashing (more secure, requires htpasswd 2.4+) sudo htpasswd -B /etc/nginx/.htpasswd user3 # Secure the file sudo chown root:www-data /etc/nginx/.htpasswd sudo chmod 640 /etc/nginx/.htpasswd ``` ### Nginx Configuration ```nginx location /admin/ { auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://admin_backend; } # Disable auth for specific sub-paths location /admin/health { auth_basic off; proxy_pass http://admin_backend; } ``` ### Auth for Entire Site with Exceptions ```nginx server { listen 443 ssl http2; server_name staging.example.com; # Global auth for staging environment auth_basic "Staging Environment"; auth_basic_user_file /etc/nginx/.htpasswd; location / { proxy_pass http://backend; } # Exempt health checks and webhooks location /health { auth_basic off; proxy_pass http://backend; } location /webhooks/ { auth_basic off; proxy_pass http://backend; } } ``` --- ## Mutual TLS (mTLS) Mutual TLS requires both server and client to present certificates, providing strong authentication. ### Server Configuration ```nginx server { listen 443 ssl http2; server_name api.example.com; # Server certificate (standard) ssl_certificate /etc/nginx/certs/server.pem; ssl_certificate_key /etc/nginx/certs/server.key; # CA certificate that signed client certificates ssl_client_certificate /etc/nginx/certs/client-ca.pem; # Require client certificate ssl_verify_client on; # Or make it optional: # ssl_verify_client optional; # Verification depth (how many intermediate CAs to check) ssl_verify_depth 2; # CRL for revoked client certificates ssl_crl /etc/nginx/certs/client-revoked.crl; location / { # Pass client certificate info to backend proxy_set_header X-SSL-Client-DN $ssl_client_s_dn; proxy_set_header X-SSL-Client-Serial $ssl_client_serial; proxy_set_header X-SSL-Client-Verify $ssl_client_verify; proxy_set_header X-SSL-Client-Fingerprint $ssl_client_fingerprint; proxy_pass http://backend; } } ``` ### Optional Client Certificate ```nginx server { listen 443 ssl http2; server_name example.com; ssl_client_certificate /etc/nginx/certs/client-ca.pem; ssl_verify_client optional; location /public/ { # No client cert required proxy_pass http://backend; } location /secure/ { # Require valid client cert for this path if ($ssl_client_verify != SUCCESS) { return 403; } proxy_pass http://secure_backend; } } ``` ### Generate Client Certificates ```bash # 1. Create CA (one-time) openssl genrsa -out client-ca.key 4096 openssl req -new -x509 -days 3650 -key client-ca.key -out client-ca.pem \ -subj "/CN=Client CA" # 2. Generate client key and CSR openssl genrsa -out client.key 2048 openssl req -new -key client.key -out client.csr \ -subj "/CN=client-name/O=organization" # 3. Sign with CA openssl x509 -req -days 365 -in client.csr -CA client-ca.pem \ -CAkey client-ca.key -CAcreateserial -out client.pem # 4. Create PKCS12 bundle for browser import openssl pkcs12 -export -out client.p12 \ -inkey client.key -in client.pem -certfile client-ca.pem # 5. Test with curl curl --cert client.pem --key client.key https://api.example.com/ ``` ### Client Certificate Variables | Variable | Description | |----------|-------------| | `$ssl_client_verify` | `SUCCESS`, `FAILED:reason`, or `NONE` | | `$ssl_client_s_dn` | Subject DN of client certificate | | `$ssl_client_i_dn` | Issuer DN of client certificate | | `$ssl_client_serial` | Serial number of client certificate | | `$ssl_client_fingerprint` | SHA1 fingerprint of client certificate | | `$ssl_client_cert` | PEM-encoded client certificate | | `$ssl_client_raw_cert` | PEM-encoded client certificate (unescaped) | | `$ssl_client_escaped_cert` | URL-encoded client certificate | --- ## HTTP to HTTPS Redirect ### Standard Redirect ```nginx # Redirect all HTTP to HTTPS server { listen 80; server_name example.com www.example.com; return 301 https://example.com$request_uri; } ``` ### Catch-All Redirect ```nginx # Redirect ANY domain on HTTP to HTTPS server { listen 80 default_server; server_name _; return 301 https://$host$request_uri; } ``` ### Redirect with Let's Encrypt Exception ```nginx server { listen 80; server_name example.com www.example.com; # Allow ACME challenge for certificate renewal location /.well-known/acme-challenge/ { root /var/www/certbot; } # Redirect everything else to HTTPS location / { return 301 https://example.com$request_uri; } } ``` ### WWW to Non-WWW (with HTTPS) ```nginx # Redirect www to non-www server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; return 301 https://example.com$request_uri; } # Redirect HTTP www to HTTPS non-www server { listen 80; server_name www.example.com; return 301 https://example.com$request_uri; } ``` ### Redirect with Preserved POST Body Note: `301` and `302` redirects convert POST to GET. Use `307`/`308` to preserve the method. ```nginx # 308 Permanent Redirect (preserves HTTP method) server { listen 80; server_name api.example.com; return 308 https://api.example.com$request_uri; } ``` | Status | Permanent | Preserves Method | |--------|-----------|-----------------| | 301 | Yes | No (POST → GET) | | 302 | No | No (POST → GET) | | 307 | No | Yes | | 308 | Yes | Yes |
-
-
scripts
-
.gitkeep 0 B · in bundle
-
-
SKILL.md 14.8 KB
--- name: nginx-ops description: "Nginx configuration, reverse proxy, SSL/TLS, load balancing, and performance tuning. Use for: nginx, reverse proxy, load balancer, proxy_pass, ssl certificate, lets encrypt, web server, location block, upstream, server block, nginx config, certbot, hsts, gzip, rate limiting." license: MIT allowed-tools: "Read Write Bash" metadata: author: claude-mods related-skills: docker-ops, security-ops, ci-cd-ops --- # Nginx Operations Comprehensive Nginx configuration, reverse proxy patterns, SSL/TLS hardening, load balancing strategies, and performance optimization for production deployments. --- ## Configuration Architecture Quick Reference ``` nginx.conf (main context) ├── worker_processes auto; ├── worker_rlimit_nofile 65535; │ ├── events { # Connection handling │ ├── worker_connections 4096; │ └── multi_accept on; │ } │ ├── http { # HTTP server settings │ ├── include mime.types; │ ├── default_type application/octet-stream; │ ├── sendfile on; │ ├── gzip on; │ │ │ ├── upstream backend { # Load balancing pool │ │ └── server 127.0.0.1:3000; │ │ } │ │ │ ├── server { # Virtual host │ │ ├── listen 443 ssl; │ │ ├── server_name example.com; │ │ │ │ │ ├── location / { # Request routing │ │ │ └── proxy_pass http://backend; │ │ │ } │ │ │ │ │ └── location /static/ { │ │ └── root /var/www; │ │ } │ │ } │ │ │ └── include /etc/nginx/conf.d/*.conf; │ } │ └── stream { # TCP/UDP proxying (optional) └── server { ... } } ``` ### Directive Inheritance Rules | Rule | Behavior | Example | |------|----------|---------| | **Inherit down** | Child blocks inherit parent directives | `gzip on;` in `http` applies to all `server` blocks | | **Override** | Child directive overrides parent | `gzip off;` in `location` overrides `http`-level `gzip on;` | | **Array directives** | NOT inherited - must be redeclared | `proxy_set_header` in `location` replaces ALL headers from `server` | | **No upward** | Inner blocks never affect outer | `location`-level settings don't affect `server` | **Critical:** Array-type directives (`proxy_set_header`, `add_header`, `proxy_hide_header`) are **completely replaced** when redefined in a child block, not merged. If you set one `proxy_set_header` in a `location`, you must redeclare ALL of them. --- ## Reverse Proxy Decision Tree ``` Need to proxy requests? │ ├─ Single backend server? │ └─ Use simple proxy_pass │ proxy_pass http://127.0.0.1:3000; │ ├─ Multiple backend servers? │ │ │ ├─ Need session persistence? │ │ ├─ By client IP → ip_hash │ │ └─ By cookie → sticky cookie (Nginx Plus) │ │ │ ├─ Backends have unequal capacity? │ │ └─ Use weight parameter │ │ server backend1:3000 weight=3; │ │ server backend2:3000 weight=1; │ │ │ ├─ Want fewest active connections? │ │ └─ least_conn │ │ │ ├─ Want even random distribution? │ │ └─ random two least_conn │ │ │ └─ Default (no special needs)? │ └─ round-robin (default, no directive needed) │ ├─ WebSocket connections? │ └─ Add Upgrade + Connection headers │ proxy_set_header Upgrade $http_upgrade; │ proxy_set_header Connection "upgrade"; │ ├─ gRPC backend? │ └─ Use grpc_pass grpc://backend; │ └─ Streaming / Server-Sent Events? └─ Disable buffering proxy_buffering off; ``` --- ## SSL/TLS Quick Start ### Let's Encrypt with Certbot ```bash # Install certbot sudo apt install certbot python3-certbot-nginx # Debian/Ubuntu sudo dnf install certbot python3-certbot-nginx # RHEL/Fedora # Obtain certificate (nginx plugin - easiest) sudo certbot --nginx -d example.com -d www.example.com # Obtain certificate (webroot - no nginx restart) sudo certbot certonly --webroot -w /var/www/html -d example.com # Test auto-renewal sudo certbot renew --dry-run ``` ### Minimal Production SSL Config ```nginx server { listen 443 ssl http2; server_name example.com; # Certificates ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Modern TLS (1.2 + 1.3) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # HSTS (2 years) add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; resolver 1.1.1.1 8.8.8.8 valid=300s; resolver_timeout 5s; # Session caching ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; root /var/www/example.com; index index.html; } # HTTP → HTTPS redirect server { listen 80; server_name example.com www.example.com; return 301 https://example.com$request_uri; } ``` --- ## Location Matching Order Nginx evaluates `location` blocks in a specific priority order, **not** in the order they appear in the config file. | Priority | Modifier | Type | Example | Behavior | |----------|----------|------|---------|----------| | 1 | `=` | Exact match | `location = /favicon.ico` | Stops search immediately on match | | 2 | `^~` | Prefix (no regex) | `location ^~ /static/` | Stops search if this prefix matches (skips regex) | | 3 | `~` | Regex (case-sensitive) | `location ~ \.php$` | First matching regex wins | | 3 | `~*` | Regex (case-insensitive) | `location ~* \.(jpg\|png)$` | First matching regex wins | | 4 | _(none)_ | Prefix | `location /api/` | Longest prefix wins (but only after regex check) | ### Evaluation Algorithm 1. Check all **prefix** locations, remember the **longest** match 2. If longest match has `^~` modifier → use it, stop 3. Check **regex** locations in config-file order → first match wins 4. If no regex matches → use the longest prefix from step 1 5. `= /path` is checked first and wins immediately if matched ### Example ```nginx location = / { } # Only exact "/" location / { } # Catch-all prefix location /api/ { } # Prefix: /api/* location ^~ /static/ { } # Prefix, skip regex: /static/* location ~ \.php$ { } # Regex: any .php file location ~* \.(gif|jpg)$ { } # Case-insensitive regex: images ``` | Request URI | Matched Location | Why | |-------------|-----------------|-----| | `/` | `= /` | Exact match (priority 1) | | `/index.html` | `/` | Longest prefix, no regex match | | `/api/users` | `/api/` | Longest prefix, no regex match | | `/static/logo.png` | `^~ /static/` | `^~` skips regex check | | `/app/index.php` | `~ \.php$` | Regex beats prefix | | `/photos/cat.jpg` | `~* \.(gif\|jpg)$` | Regex beats prefix | --- ## Common Configurations ### SPA Routing (React, Vue, Angular) ```nginx server { listen 80; server_name app.example.com; root /var/www/app/dist; index index.html; # Serve static files directly, fall back to index.html for SPA routes location / { try_files $uri $uri/ /index.html; } # Cache static assets aggressively location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; } } ``` ### WebSocket Proxy ```nginx location /ws/ { proxy_pass http://127.0.0.1:3000; 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_read_timeout 86400s; # Keep WebSocket alive for 24h proxy_send_timeout 86400s; } ``` ### Rate Limiting ```nginx # Define zone: 10MB shared memory, 10 requests/second per IP limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; server { location /api/ { # Allow burst of 20, process excess without delay up to burst limit_req zone=api burst=20 nodelay; limit_req_status 429; proxy_pass http://backend; } } ``` ### Gzip Compression ```nginx http { gzip on; gzip_comp_level 5; # Balance CPU vs compression (1-9) gzip_min_length 256; # Don't compress tiny responses gzip_vary on; # Vary: Accept-Encoding header gzip_proxied any; # Compress proxied responses too gzip_types text/plain text/css text/javascript application/javascript application/json application/xml application/xml+rss image/svg+xml; } ``` ### Static File Serving ```nginx location /static/ { alias /var/www/static/; # Note: alias, not root (includes /static/ path) expires 30d; add_header Cache-Control "public, no-transform"; # Disable access log for static files access_log off; # Enable open file cache open_file_cache max=1000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; } ``` ### CORS Headers ```nginx location /api/ { # CORS headers add_header Access-Control-Allow-Origin "https://app.example.com" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; add_header Access-Control-Max-Age 86400 always; # Handle preflight requests if ($request_method = OPTIONS) { return 204; } proxy_pass http://backend; } ``` --- ## Docker Patterns ### Nginx as Reverse Proxy in Docker Compose ```yaml # docker-compose.yml services: nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./certs:/etc/nginx/certs:ro depends_on: - app networks: - webnet app: build: . expose: - "3000" # Internal only, not published to host networks: - webnet networks: webnet: ``` ```nginx # nginx.conf for docker-compose (use service name as hostname) upstream app_backend { server app:3000; # Docker DNS resolves service name } server { listen 80; location / { proxy_pass http://app_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; } } ``` ### Multi-Stage Build with Static Assets ```dockerfile # Stage 1: Build frontend FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Serve with nginx FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 ``` ```nginx # nginx.conf for containerized SPA server { listen 80; root /usr/share/nginx/html; index index.html; # SPA routing location / { try_files $uri $uri/ /index.html; } # Health check endpoint location /health { access_log off; return 200 "OK\n"; add_header Content-Type text/plain; } # Cache busted assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; } } ``` --- ## Common Gotchas | Gotcha | Why | Fix | |--------|-----|-----| | **Trailing slash in `proxy_pass`** | `proxy_pass http://backend` keeps `/api/users` as-is; `proxy_pass http://backend/` strips the matched `location` prefix | Be intentional: with `/` to strip prefix, without to preserve | | **Missing proxy headers** | Backend sees nginx's IP, not the client's. Breaks auth, logging, and geo detection | Always set `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, and `Host` | | **Buffer size errors (502)** | Large headers (cookies, JWTs) exceed default buffer sizes | Increase `proxy_buffer_size 8k;` and `proxy_buffers 4 16k;` | | **`worker_connections` too low** | Default is 512 or 1024; each client uses 2 connections (client + upstream) | Set `worker_connections 4096;` and raise `worker_rlimit_nofile` | | **`try_files` with `proxy_pass`** | `try_files` and `proxy_pass` in the same `location` don't work as expected | Use `try_files $uri @backend;` with a named location for proxy | | **"if is evil"** | `if` inside `location` creates an implicit nested location, breaking directives | Use `map` for variable-based logic; reserve `if` for `return`/`rewrite` only | | **Resolver for dynamic upstreams** | Variables in `proxy_pass` (e.g., `$upstream`) bypass startup DNS resolution | Add `resolver 127.0.0.11 valid=30s;` (Docker) or `resolver 1.1.1.1;` | | **Missing `index` directive** | Returns 403 Forbidden when accessing a directory instead of index file | Add `index index.html;` in `server` or `location` block | | **Permission denied on socket** | Nginx worker can't read the upstream Unix socket | Ensure nginx user is in the socket's group; `chmod 660` the socket | | **Duplicate `Content-Encoding` with gzip** | Upstream already compresses + nginx gzip double-compresses | Use `gzip_proxied` carefully or `proxy_set_header Accept-Encoding "";` | | **`add_header` not inherited** | Adding ANY `add_header` in a `location` discards ALL parent `add_header` directives | Redeclare all headers in the child block, or use `include` for shared headers | | **`alias` vs `root` confusion** | `root` appends the location path; `alias` replaces it. `/img/` + `root /data` = `/data/img/`; `alias /data/` = `/data/` | Use `alias` when location path shouldn't appear in filesystem path | --- ## Reference Files | File | Contents | Lines | |------|----------|-------| | [reverse-proxy.md](references/reverse-proxy.md) | Upstream blocks, load balancing, proxy caching, WebSocket/gRPC, timeouts, real-world configs | ~650 | | [ssl-security.md](references/ssl-security.md) | TLS config, Let's Encrypt, HSTS, OCSP, security headers, rate limiting, mTLS | ~550 | | [performance.md](references/performance.md) | Worker tuning, compression, caching, HTTP/2+3, static files, monitoring | ~550 | --- ## See Also - **docker-ops** - Container orchestration, docker-compose patterns - **security-ops** - Application security, authentication patterns - **ci-cd-ops** - Deployment pipelines, zero-downtime deploys - [Nginx official docs](https://nginx.org/en/docs/) - [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/) - [Nginx Config Generator](https://www.digitalocean.com/community/tools/nginx)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.