A fresh Nginx install is safe, but chatty. It announces its exact version in headers and error pages, answers any hostname pointed at your IP, and will happily serve a stray .git folder if one ends up in a web root. A few small changes fix all of that. This guide applies a short hardening checklist you can roll out to every site on the server.
Before you start
- A Linux VPS with Nginx installed and at least one working site.
- SSH access with sudo.
- A backup of the config:
sudo cp -a /etc/nginx /etc/nginx.bak-$(date +%F).
Hide the version number
By default the Server header and error pages show something like nginx/1.24.0. That tells scanners exactly which known bugs to try. Turn it off in the http context. On Ubuntu and Debian, nginx.conf usually contains a commented line you can enable:
grep -n "server_tokens" /etc/nginx/nginx.confIf it shows # server_tokens off;, remove the #. If there is no line at all, add it to a new file instead:
echo "server_tokens off;" | sudo tee /etc/nginx/conf.d/hardening.confOnly set it once, or nginx -t reports a duplicate directive. The header becomes plain Server: nginx. Removing the header entirely needs a third-party module and gains little.
Note: Hiding the version is a small step, not protection on its own. Keeping Nginx updated matters far more. See Automatic security updates on Linux.
Drop requests for unknown hosts
Bots scan IP ranges and send requests with your bare IP or random hostnames. Without a catch-all, Nginx serves them the first site it finds. Add a default server that answers nothing:
sudo nano /etc/nginx/conf.d/00-default.confserver {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}return 444closes the connection with no response.ssl_reject_handshake onrefuses the TLS handshake for unknown names, so no certificate is needed in this block. It needs Nginx 1.19.4 or newer, which all the distributions covered here ship.
On Ubuntu and Debian, the packaged default site also claims default_server on port 80. Remove its link first, or nginx -t fails with "a duplicate default server":
sudo rm /etc/nginx/sites-enabled/defaultOn AlmaLinux and Rocky, the main nginx.conf may contain a server block with default_server. Comment out that whole block or remove the default_server flag from its listen lines.
Block hidden files
Files starting with a dot, such as .git, .env and .htpasswd, often contain secrets. Block them in every site's server block while still allowing .well-known, which Let's Encrypt and other services use:
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
}Put it near the top of the server block so it matches before other regex locations.
Limit request methods
Most websites only need GET, HEAD and POST. Reject the rest inside the server block:
if ($request_method !~ ^(GET|HEAD|POST)$) {
return 405;
}Warning: REST APIs, WebDAV and some apps use PUT, PATCH, DELETE or OPTIONS (for CORS preflight). Do not add this to those sites, or add the methods they need to the list.
Limit body size and timeouts
Add these to /etc/nginx/conf.d/hardening.conf:
client_max_body_size 10m;
client_body_timeout 15s;
client_header_timeout 15s;
send_timeout 30s;
keepalive_timeout 30s;client_max_body_sizecaps uploads. Raise it for sites that need bigger uploads in their own server block.- The timeouts stop slow clients from holding connections open forever.
Check none of these are already set in nginx.conf. On Ubuntu and Debian, keepalive_timeout usually is; edit it there rather than adding a second copy.
Disable directory listings
autoindex is off by default. Make sure no site switched it on by accident:
sudo grep -rn "autoindex on" /etc/nginx/Apply the change
sudo nginx -t && sudo systemctl reload nginxIf the test fails, it names the file and line. Fix it, or restore your backup with sudo cp -a /etc/nginx.bak-YYYY-MM-DD/. /etc/nginx/.
Check it works
Version hidden:
curl -sI https://example.com/ | grep -i "^server"You should see server: nginx with no number.
Unknown hosts dropped:
curl -I http://203.0.113.10/curl should report Empty reply from server.
Dotfiles blocked:
curl -I https://example.com/.envYou want HTTP/2 403, not a 200 or a download.
Your real sites should still load normally in a browser.
Troubleshooting
nginx -tsays "duplicate default server": another file also usesdefault_serveron the same port. Remove the flag from one of them.nginx -tsays "server_tokens" directive is duplicate: it is set innginx.confand in your new file. Keep one.- Your site now shows an empty reply: its
server_namedoes not match the domain, so the catch-all answers. Fix theserver_name. - Let's Encrypt renewals fail: your dotfile rule blocks
.well-known. Use the pattern with(?!well-known)exactly as shown. - Uploads fail with 413:
client_max_body_sizeis too low for that site. See Fix Nginx 413 Request Entity Too Large.
Next steps
- Add browser-side protection: Add security headers in Nginx.
- Stop abusive clients: Rate limit requests in Nginx.
