Nginx configuration has a deceptively simple syntax but a long tail of security and performance pitfalls: misconfigured SSL ciphers, missing security headers, rate limiting that blocks legitimate traffic, and proxy settings that leak internal server details. AI tools that generate nginx configs without understanding the security implications can introduce vulnerabilities.
Table of Contents
- The Security Baseline Test
- Task 2: Caching Layer Configuration
- Task 3: Load Balancing Configuration
- Task 4: Security-Hardened API Gateway
- Task 5: Geo-Blocking and Bot Mitigation
- Why Security Context Matters More Than Syntax
- Tool Comparison
- Practical Advice for Using AI with Nginx
- Related Reading
The Security Baseline Test
Every AI tool was given this prompt:
“Write a nginx configuration for a Node.js app running on port 3000, with HTTPS, Let’s Encrypt SSL, and production security settings.”
Copilot’s output (selected security issues):
# Copilot — missing critical security headers
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Missing: ssl_protocols, ssl_ciphers, HSTS, security headers
location / {
proxy_pass http://localhost:3000;
# Missing: proxy_hide_header, X-Frame-Options, X-Content-Type-Options
}
}
Claude’s complete configuration:
# /etc/nginx/sites-available/myapp
# Rate limiting zone — 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# Upstream definition for health checks
upstream nodejs_app {
server 127.0.0.1:3000;
keepalive 32;
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Allow Let's Encrypt ACME challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Modern SSL settings (TLS 1.2/1.3 only)
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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Hide nginx version
server_tokens off;
# Hide upstream server header
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
# Logging
access_log /var/log/nginx/myapp-access.log combined;
error_log /var/log/nginx/myapp-error.log warn;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Client body size limit
client_max_body_size 10M;
# API routes with rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://nodejs_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# WebSocket support
location /ws/ {
proxy_pass http://nodejs_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s; # Keep WebSocket connections alive
}
# Static assets with aggressive caching
location /static/ {
alias /var/www/myapp/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Health check endpoint (no rate limiting)
location /health {
proxy_pass http://nodejs_app;
access_log off;
}
}
Task 2: Caching Layer Configuration
Prompt: “Add nginx caching for API responses with cache invalidation support.”
# Claude's caching configuration
http {
# Cache storage location and size
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
# Cache API GET/HEAD responses
location /api/v1/ {
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept_language";
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 0;
# Cache bypass conditions
proxy_cache_bypass
$http_cache_control # Bypass if Cache-Control: no-cache
$http_x_bypass_cache # Bypass via custom header
$cookie_session; # Don't cache authenticated requests
proxy_no_cache
$http_cache_control
$cookie_session;
# Only cache GET and HEAD
proxy_cache_methods GET HEAD;
# Add cache status header for debugging
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://nodejs_app;
}
# Cache purge endpoint (restrict to internal IPs)
location ~ /purge(/.*) {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
proxy_cache_purge api_cache "$scheme$request_method$host$1";
}
}
}
Task 3: Load Balancing Configuration
# Claude's load balancer with health checks
upstream backend_pool {
# Least connections algorithm for uneven request distribution
least_conn;
server 10.0.1.10:3000 weight=2 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 weight=2 max_fails=3 fail_timeout=30s;
server 10.0.1.12:3000 weight=1 max_fails=3 fail_timeout=30s;
# Backup server — only used if all primary servers are down
server 10.0.1.20:3000 backup;
keepalive 32;
keepalive_requests 100;
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://backend_pool;
proxy_http_version 1.1;
proxy_set_header Connection ""; # Required for keepalive to upstream
# Retry on upstream errors
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
}
}
Task 4: Security-Hardened API Gateway
# Claude's API gateway with auth and injection protection
server {
# Block common injection patterns in query strings
set $block_query 0;
if ($query_string ~* "(union|select|insert|delete|drop|truncate|eval|javascript|vbscript|onload)") {
set $block_query 1;
}
if ($block_query = 1) {
return 400 "Bad Request";
}
# Block user agents that are clearly scanners
if ($http_user_agent ~* "(sqlmap|nikto|masscan|nessus|dirbuster|wfuzz)") {
return 403;
}
# JWT validation via auth_request to your auth service
location /api/protected/ {
auth_request /auth/validate;
auth_request_set $auth_user $upstream_http_x_auth_user;
# Pass auth user to upstream
proxy_set_header X-Auth-User $auth_user;
proxy_pass http://nodejs_app;
# Cache auth validation results (5 minutes)
proxy_cache_valid 200 5m;
}
# Internal auth validation endpoint
location = /auth/validate {
internal;
proxy_pass http://auth_service/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Authorization $http_authorization;
}
}
Task 5: Geo-Blocking and Bot Mitigation
One scenario where Copilot and Cursor both produced incomplete output was geo-blocking combined with bot mitigation. Claude generated a complete working block using the MaxMind GeoIP2 module:
# Requires: ngx_http_geoip2_module and MaxMind GeoLite2-Country database
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
$geoip2_data_country_code country iso_code;
}
map $geoip2_data_country_code $allowed_country {
default 0;
US 1;
CA 1;
GB 1;
AU 1;
DE 1;
}
server {
listen 443 ssl http2;
# Drop non-allowed countries at the request level
if ($allowed_country = 0) {
return 403 "Service not available in your region";
}
# Additional bot fingerprinting — real browsers send Accept-Language
# Known scrapers often omit it
set $bot_score 0;
if ($http_user_agent = "") { set $bot_score 1; }
if ($http_accept_language = "") { set $bot_score "${bot_score}1"; }
if ($bot_score = "11") {
return 429 "Too Many Requests";
}
location / {
proxy_pass http://nodejs_app;
}
}
This configuration demonstrates an important difference between tools: Claude explained that the if directive is safe here because it is used only to set variables and return responses — the two contexts where nginx documentation permits if at the server level. Copilot generated a syntactically similar block but omitted the explanation, leaving engineers unaware of the known footgun with if inside location blocks.
Why Security Context Matters More Than Syntax
All three tools can write syntactically valid nginx config. The differentiator is whether the tool understands the security implications of each directive. A quick evaluation checklist:
| Security Check | Claude | Copilot | Cursor |
|---|---|---|---|
| TLS 1.0/1.1 disabled by default | Yes | Sometimes | Rarely |
| HSTS with preload flag | Yes | No | No |
| OCSP stapling configured | Yes | No | No |
server_tokens off included |
Yes | Inconsistent | Yes |
| Proxy hides upstream headers | Yes | No | No |
| Rate limiting with burst queue | Yes | Basic | No |
Explains if directive risks |
Yes | No | No |
The pattern is consistent: Claude tends to generate configs that would pass a Mozilla Observatory scan on the first attempt. The others require one or two review cycles to reach the same standard.
Tool Comparison
| Feature | Claude | Copilot | Cursor |
|---|---|---|---|
| SSL hardening | TLS 1.2/1.3, OCSP | TLS 1.2+, basic | Basic SSL |
| Security headers | 6 headers including Permissions-Policy | 2-3 headers | 3-4 headers |
| Rate limiting | Zone + burst config | Missing or basic | Basic |
| Proxy timeouts | All three (connect/send/read) | Often missing | Partial |
| WebSocket config | Correct Upgrade headers | Sometimes correct | Correct |
| Caching | Full with bypass/purge | Basic | Basic |
| Load balancing | Health checks, retries | Round-robin only | Partial |
| Geo-blocking | Full GeoIP2 example | Incomplete | Not attempted |
Practical Advice for Using AI with Nginx
Regardless of which tool you use, apply these steps before any AI-generated nginx config reaches production:
- Run
nginx -tto catch syntax errors before reloading - Test SSL with
testssl.shor the Qualys SSL Labs scanner - Verify security headers with Mozilla Observatory
- Load test rate limiting with
wrkorvegetato confirm burst behaviour - Check that
proxy_next_upstreamsettings match your upstream’s idempotency guarantees — retrying a POST on a payment endpoint is dangerous
Related Reading
- Best AI Tools for Writing Nginx Configs
- AI Tools for Generating Nginx and Caddy Reverse Proxy Configs
- Best AI Tools for Writing GitHub Actions
- AI Tools for Writing Nginx Configurations 2026
Related Articles
- AI Tools for Generating Nginx Configuration Files 2026
- Best AI Tools for Writing Nginx Configs in 2026
- AI Tools for Writing Nginx Configurations 2026
- AI Tools for API Security Testing
- Writing Custom Instructions for AI Tools That Enforce Your
Built by theluckystrike — More at zovo.one