Hiding the Nginx Version: Stop Fingerprint Leakage
Default Nginx exposes its version in response headers and error pages; attackers match CVEs from that. server_tokens off, X-Powered-By stripping, headers-more Server rewrite, custom error pages, curl verification, module incompatibility.
Applicable scenarios
- Site has no WAF; response headers expose nginx/1.x.x
- Pentest report flags "web server version disclosure" as medium
- Reduce automated scanners matching CVEs by version
Prerequisites
- Nginx installed (1.18+, 1.24 used here)
- root or sudo; can edit the main config
- Comfortable with nginx -t and reload
How it works
Nginx defaults to server_tokens on. Two leak points: response header Server: nginx/1.24.0 with the exact version; default 404/50x error-page footer nginx/1.24.0. Some distro builds also stamp an OS identifier. Exact version lets attackers filter unpatched CVEs and run PoCs cheaply. Version hiding shrinks attack surface: it does not patch vulns, it denies cheap targeting.
Reverse proxy / FastCGI also leaks app versions via X-Powered-By: PHP/8.2.1. Strip that too.
Procedure
1. Disable server_tokens
`http {
server_tokens off;
}`
After this, Server is just nginx; error-page footer is nginx without a version.
2. Hide the PHP version header
`http {
fastcgi_hide_header X-Powered-By;
}`
3. Fully rewrite the Server header (optional)
If the baseline forbids the word nginx in Server, install headers-more:
`# CentOS/RHEL
yum install -y nginx-module-headers-more
# Ubuntu/Debian
apt install -y libnginx-mod-http-headers-more-filter`
Load it in the http block and rewrite:
`load_module modules/ngx_http_headers_more_filter_module.so;
http {
more_set_headers "Server: WebServer";
}`
Dynamic modules must match the running Nginx version exactly. Prefer distro module packages. Hand-compiling often fails nginx -t with module incompatibility.
4. Custom error pages
`server {
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /404.html { root /usr/share/nginx/html; internal; }
location = /50x.html { root /usr/share/nginx/html; internal; }
}`
server_tokens off already strips versions from default error pages. Custom pages further avoid leaking internal paths and module info.
5. Reload
`nginx -t && nginx -s reload`
Verification
`curl -sI https://helixcyber.ai | grep -i server
# Expected: Server: nginx (no version number)
# After headers-more: Server: WebServer`
Hit a missing path to trigger 404; confirm the footer has no nginx/1.24.0. Check curl -sI https://helixcyber.ai | grep -i powered — X-Powered-By should be gone.
FAQ
FAQ 1: server_tokens off, 502 page still shows the version?
Typical reverse-proxy case: the 502 page is generated upstream and passed through. http-block server_tokens only affects responses Nginx itself generates. Add proxy_intercept_errors on; in the relevant server/location and define error_page so Nginx owns the error page, or more_clear_headers on the upstream response.
FAQ 2: After headers-more, nginx -t says module is not binary compatible?
Module was built against a different Nginx version or configure flags. Record current flags with nginx -V, rebuild with the same ./configure --add-dynamic-module=..., or install the matching distro module package. Do not copy an .so built for version A onto version B.
FAQ 3: Is hiding the version enough?
No. It only raises recon cost. Vulns still need patches. Treat version hiding as a baseline item and regularly upgrade Nginx to the latest stable.
Summary
Nginx version hiding is the cheapest, most certain baseline: one server_tokens off line strips header and error-page versions; fastcgi_hide_header clears the PHP fingerprint; headers-more rewrites Server if required. Recheck both headers and error pages with curl. On reverse proxies, watch upstream error-page passthrough.