Nginx Security Hardening Checklist: A Practical Configuration Guide
Harden your Nginx web server with this practical checklist. Covers version hiding, security headers, TLS configuration, rate limiting, and a complete example config.
Why Nginx hardening matters
Nginx is the most widely deployed web server on the internet. Its default configuration is designed for ease of setup, not for security. Out of the box, Nginx exposes its version number, serves default pages, allows directory listing, and lacks security headers.
Every piece of information your server reveals unnecessarily helps an attacker. Version numbers lead to targeted exploits. Default pages confirm the technology stack. Missing headers enable cross-site scripting and clickjacking.
This guide walks through each hardening step with the exact configuration directives you need.
Hide the server version
By default, Nginx includes its version number in HTTP response headers (Server: nginx/1.24.0) and on error pages. This tells attackers exactly which vulnerabilities to try.
In the http block of your nginx.conf:
http {
server_tokens off;
}
Verify it worked:
curl -sI https://yourcompany.com | grep -i server
# Should show "Server: nginx" without a version number
For even more privacy, you can install the ngx_headers_more module and remove the Server header entirely:
more_clear_headers Server;
Remove default pages
Nginx ships with a default welcome page at /usr/share/nginx/html/index.html. If this is still accessible, it confirms you are running Nginx and suggests the server may not be fully configured.
# Remove or replace the default page
sudo rm /usr/share/nginx/html/index.html
# Or replace with a blank page
echo "" | sudo tee /usr/share/nginx/html/index.html
Also remove or disable the default server block if you are using virtual hosts:
sudo rm /etc/nginx/sites-enabled/default
Disable directory listing
If a directory does not contain an index file, Nginx can display a listing of all files. This leaks directory structure, file names, and potentially sensitive files.
Ensure autoindex is off (it is off by default, but verify):
server {
autoindex off;
}
Also add a default index directive to serve something predictable:
server {
index index.html index.htm;
}
Add security headers
This is one of the highest-impact steps. See our dedicated guide on HTTP security headers for a full explanation of each header. Here is the summary configuration:
# HSTS -- force HTTPS for 2 years
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# CSP -- restrict resource loading
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
# Prevent clickjacking
add_header X-Frame-Options "DENY" always;
# Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;
# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Restrict browser features
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
The always parameter is critical -- without it, headers are not sent on error responses (404, 500, etc.).
Configure TLS properly
Weak TLS configuration is one of the most common findings in security scans. See our full guide on weak TLS cipher suites for details. Here is the recommended configuration:
# Protocols -- TLS 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites -- Mozilla Modern profile
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;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
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;
Rate limiting
Rate limiting protects against brute force attacks, credential stuffing, and application-layer DDoS. Nginx has built-in rate limiting using the limit_req module.
Define a rate limit zone
In the http block:
http {
# 10 requests per second per IP, with a 10MB shared memory zone
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# Stricter limit for login endpoints
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
}
Apply rate limits to locations
server {
# General rate limit with burst allowance
location / {
limit_req zone=general burst=20 nodelay;
}
# Strict rate limit for authentication endpoints
location /login {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
}
location /api/auth {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
}
}
Custom error page for rate-limited requests
error_page 429 /429.html;
location = /429.html {
internal;
return 429 '{"error": "Too many requests. Please try again later."}';
}
Restrict HTTP methods
Most web applications only need GET, POST, and HEAD. Allowing other methods (PUT, DELETE, TRACE, OPTIONS) can expose vulnerabilities.
server {
# Only allow safe methods
if ($request_method !~ ^(GET|POST|HEAD)$) {
return 405;
}
}
If you run a REST API that requires PUT, DELETE, and PATCH, restrict the allowed methods per location:
location /api/ {
# API endpoints may need more methods
if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$) {
return 405;
}
}
location / {
# Static content only needs GET and HEAD
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
}
}
Log configuration for security monitoring
Good logging is essential for detecting and investigating attacks.
Access log format with security-relevant fields
http {
log_format security '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$http_x_forwarded_for';
access_log /var/log/nginx/access.log security;
error_log /var/log/nginx/error.log warn;
}
Log rate-limited requests separately
# Log rate-limited requests for monitoring
limit_req_log_level warn;
Rotate logs
Ensure logrotate is configured for Nginx logs to prevent disk space issues:
cat /etc/logrotate.d/nginx
If it does not exist, create it:
/var/log/nginx/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
Additional hardening
Limit request body size
Prevent excessively large uploads that could exhaust disk space or memory:
client_max_body_size 10m;
Set timeouts to prevent slowloris attacks
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
keepalive_timeout 15s;
Block access to hidden files
Prevent access to .git, .env, .htaccess, and other dotfiles:
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
Block access to sensitive file extensions
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist|env)$ {
deny all;
access_log off;
log_not_found off;
}
Complete hardened nginx.conf example
Here is a complete server block incorporating all the above:
server {
listen 443 ssl http2;
server_name yourcompany.com www.yourcompany.com;
# TLS
ssl_certificate /etc/letsencrypt/live/yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourcompany.com/privkey.pem;
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
# Hide version
server_tokens off;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Rate limiting
limit_req zone=general burst=20 nodelay;
# Restrict methods
if ($request_method !~ ^(GET|POST|HEAD)$) {
return 405;
}
# Block dotfiles
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block sensitive extensions
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist|env)$ {
deny all;
}
# Timeouts
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
keepalive_timeout 15s;
client_max_body_size 10m;
# Disable autoindex
autoindex off;
# Root and index
root /var/www/yourcompany.com;
index index.html;
# Logging
access_log /var/log/nginx/yourcompany.access.log security;
error_log /var/log/nginx/yourcompany.error.log warn;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourcompany.com www.yourcompany.com;
return 301 https://$host$request_uri;
}
After applying, test and reload:
sudo nginx -t
sudo systemctl reload nginx
Then verify with Security Headers and SSL Labs to confirm everything is in place. For more on your broader network exposure, review our guide on open ports security.
How SurfaceScan helps
SurfaceScan tests your Nginx servers (and other web servers) from the outside on every scan. It detects exposed version numbers, missing security headers, weak TLS configuration, and other hardening gaps. Each finding includes the specific issue, the affected URL, and the configuration change needed to fix it -- so you can validate your hardening against what the internet actually sees.
Related articles
HTTP Security Headers: What They Are and How to Add Them
HTTP security headers like HSTS, CSP, and X-Frame-Options protect your site from clickjacking, XSS, and MIME sniffing. Learn how to add them on Nginx and Apache.
Open Ports: Which Ones Are Dangerous and How to Close Them
Not all open ports are a problem, but some should never be exposed to the internet. Learn which ports are dangerous, why, and how to close them safely.
Weak TLS Cipher Suites: How to Fix and Harden Your HTTPS
Weak TLS cipher suites like RC4 and 3DES leave your HTTPS connections vulnerable. Learn how to identify weak ciphers and configure strong ones on Nginx and Apache.