Out of the box, Nginx is incredibly fast and efficient but it isn't inherently secure against modern automated attacks like scanners, scraping bots and most sophisticated brute force attacks. Over time, I've set up a modular approach to hardening my Nginx setups. By splitting the security configurations into multiple logical files, it becomes much easier later to maintain, audit, and apply them across multiple virtual hosts.
In this guide, I'll walk you through the essential configurations that will significantly improve your server's security. Please note before proceeding to the main article. You will need to install nginx-module-headers-more module for the more_set_headers directive to work.
1. Global Security Settings
This configurations will cover server-wide settings, masking the server identity and filtering out malicious traffic before it even reaches your application.
By leveraging Nginx's map module, we can identify path traversal attempts, unauthorized bot scanners, and cloud metadata SSRF attempts efficiently without cluttering our server blocks with heavy conditional arguments.
# Hide Nginx version
server_tokens off;
# Mask the server name (Note: requires the headers-more-nginx-module)
more_set_headers "Server: AnyNameHere";
# Identify sesitive paths URIs
map $request_uri $bad_request {
default 0;
"~*wp-admin" 1;
"~*(?:phpunit|phpinfo|phpmyadmin|php-?my-?admin|administrator|wp|wordpress)" 1;
"~*(?:/\.env|\.git|\.svn|\.hg|\.DS_Store|\.idea|\.htaccess|\.htpasswd)" 1;
"~*(?:etc/passwd|proc|\/\/)+" 1;
# Path traversal attempts (e.g., ../../etc/passwd)
"~*(?:\.\./|\.\.\\x5c)" 1;
# Cloud Metadata SSRF attempts
"~*(?:169\.254\.169\.254)" 1;
# Block direct access to common script/backup extensions remove it if you actually want to host this extension files on your server
"~*(?:\.sh|\.bash|\.bak|\.sql|\.tar|\.gz|\.zip)$" 1;
}
# Identify malicious automated scanners and bots
map $http_user_agent $bad_user_agent {
default 0;
"~*havij|nikto|scan|loader|fetch|acunetix|sonic|nessus|sqlmap|zgrab|masscan|nmap|dirb|gobuster|shodan|censys|wpscan" 1;
}
# Combine the conditions
map "$bad_request$bad_user_agent" $is_bad_request {
default 0;
"~*1" 1;
}
# Global rate limiting settings for DDos Protection. Change the limits according to your needs
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
How to apply it: Save the above configuration as security-global.conf file inside /etc/nginx/snippets directory. Include this file inside your main http {} block in nginx.conf. Like as below,
include /etc/nginx/snippets/security-global.conf;
Then, to actually drop the malicious traffic, add this simple check inside your server {} blocks of your main nginx configuration or on any blocks where you want to apply them.
if ($is_bad_request) {
return 444; # Closes the connection immediately without a response
}
To apply the rate limiting setting add the following line on any of your server {} block of the main nginx configuration,
limit_req zone=ratelimit burst=20 nodelay;
2. Location-Specific Rules
Follow this section only if you want to restrict all the hidden files from being served through nginx cause they can be accidentally end up in your web root. Also, Text editors often leave backup files ending in ~ and version control systems leave hidden directories like .git. This file ensures they are never served to the public.
# Deny access to all hidden files and directories (starting with a dot)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Deny access to temporary editor files
location ~ ~$ {
deny all;
}
How to apply it:
Save the above code block as security-location.conf file inside your /etc/nginx/snippets directory.
Then include the file inside your server {} block like below,
include /etc/nginx/snippets/security-location.conf;
Setting access_log off and log_not_found off can prevent your access log from being spammed. but still if you want them on your access log for security purposes then you can set it as access_log on.
3. HTTP Security Headers
This is the most crucial part. Injecting the right HTTP headers protects your users from Cross-Site Scripting (XSS), clickjacking, and MIME-type sniffing or various attacks. It reduces attack surfaces greatly. I have covered all the most important security headers except the content security policy header. Since that header need to be configured according to your application.
# Prevent clickjacking by restricting who can embed your site in an iframe
add_header X-Frame-Options "SAMEORIGIN" always;
# Force the browser's native XSS filter
add_header X-XSS-Protection "1; mode=block" always;
# Prevent MIME-sniffing (forces the browser to respect the declared content type)
add_header X-Content-Type-Options "nosniff" always;
# Control how much referrer information is passed to external sites
add_header Referrer-Policy "same-origin" always;
# Restrict access to device hardware (camera, microphone, location)
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Enforce HTTPS connections for a full year, including subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
How to apply it:
Save the file as /etc/nginx/snippets/security-headers.conf file. You can then include this file in your http {}, server {}, or even specific location {} blocks depending on which application you will need to apply those headers.
Wrapping Up
By splitting your security configurations into security-global.conf, security-location.conf and security-headers.conf, you can keep your main site configurations incredibly clean. Your typical virtual host file now only needs three simple include statements to achieve a robust baseline of security.
Note: Always remember to test your configuration before reloading the web server
nginx -t
systemctl reload nginx
Security is layered. While Nginx hardeing won't fix vulnerable application code but dropping malicious traffic at the edge saves your backend resources and mitigates entire classes of common automated attacks.
Top comments (0)