PenTesting Company
← Blog

Hide the Nginx Server Header

12 min read

By default, Nginx includes a Server response header that reports the software and version, for example Server: nginx/1.24.0. Version disclosure is a minor issue on its own, but it hands an attacker free reconnaissance, and hiding it is quick.

What the default looks like

The examples below were captured on Debian 13 with the packaged nginx/1.26.3 and libnginx-mod-http-headers-more-filter 0.38. With server_tokens left at its default of on, a plain request shows the full version:

$ curl -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: nginx/1.26.3
Date: Wed, 07 Oct 2026 14:09:39 GMT
Content-Type: text/html
Content-Length: 3
Connection: keep-alive
Accept-Ranges: bytes

Note that some distributions already ship with server_tokens off; in the default nginx.conf (the Debian package does), so check your actual output before assuming either way.

Turn off the version

Open your Nginx configuration file, usually at /etc/nginx/nginx.conf, and add server_tokens off; inside the http block:

http {
    # ...
    server_tokens off;
    # ...
}

The server_tokens directive is valid in the http, server, and location contexts. Setting it once in http covers every site; setting it in a server block covers only that site. After the change, the version is gone but the name stays:

$ curl -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
...

Check the configuration and reload

Always test the configuration before reloading:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

$ sudo systemctl reload nginx

If nginx -t reports an error, fix it before reloading. A reload keeps the running workers on the old configuration if the new one fails to load.

Removing or replacing the header with headers-more

Hiding the nginx name as well is not possible with a built-in directive. It requires the third-party headers-more module or a custom build. On Debian and Ubuntu the module is packaged:

sudo apt install libnginx-mod-http-headers-more-filter

The package enables the module with a load_module line under /etc/nginx/modules-enabled/, which belongs in the main (top-level) context. The more_clear_headers and more_set_headers directives go in http, server, or location. To remove the header completely:

server {
    listen 8080;
    server_tokens off;
    more_clear_headers Server;
}
$ curl -I http://127.0.0.1:8080/nope
HTTP/1.1 404 Not Found
Date: Wed, 07 Oct 2026 14:09:40 GMT
Content-Type: text/html
Content-Length: 146
Connection: keep-alive

To replace the value instead of removing it:

more_set_headers "Server: web";
$ curl -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: web
...

Error pages expose the version too

The built-in error pages print the server name in the body. With the default setting, a 404 page ends like this:

$ curl -s http://127.0.0.1:8080/nope
...
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.26.3</center>

In our test, more_clear_headers Server; on its own removed the header but left nginx/1.26.3 in the error page body. Adding server_tokens off; reduced the body to nginx. If you want the name gone from error pages as well, serve your own pages with the error_page directive.

server_tokens or headers-more?

  • server_tokens off; is built in, needs no extra module, and hides the version in both the Server header and the default error pages. The name nginx remains.
  • headers-more can remove or rewrite the Server header entirely, but it is a third-party module you have to install and keep updated, and it does not change the error page body.
  • For most teams, server_tokens off; is enough. If you also want the name gone, use both together, plus custom error pages.

Other headers that leak recon information

The Server header is not the only one. Several others describe your software stack, and visitors can read all of them. They are usually set by the application, framework, or an upstream proxy rather than by Nginx itself. That means the lasting fix is often in the application's own configuration. In Nginx you can reduce what reaches the public, but only for responses it passes through from an upstream. Headers Nginx adds directly are not affected by these directives.

  • X-Powered-By. This names the framework or runtime behind the application and sometimes its version. It tells a visitor which technology to look up. Application frameworks usually set it themselves, so turn it off in the framework where you can. In Nginx, hide it from proxied responses with proxy_hide_header X-Powered-By;, or with fastcgi_hide_header X-Powered-By; when you pass requests to FastCGI.
  • Via and similar proxy headers. These show that a proxy, cache, or gateway sits in the path, and can include the software name or an internal hostname. Nginx does not normally add Via by default, but an upstream or another proxy in the chain may. To stop an upstream value passing through, use proxy_hide_header Via;. Some proxies and CDNs add their own headers after Nginx, so check what leaves your outermost layer.
  • Version banners such as X-AspNet-Version and X-Generator. X-AspNet-Version reports the ASP.NET version in use, and X-Generator is often set by a content management system to name itself. Both are set by the application or its platform. Switch them off there first, then add proxy_hide_header X-AspNet-Version; and proxy_hide_header X-Generator; as a second layer.
  • Default error pages from an upstream server. A default error page from an Apache upstream can print a line such as Apache/2.4.x (Debian) Server at example.com Port 80. Reduce it at the source with Apache's ServerTokens Prod and ServerSignature Off, and in Nginx let your own pages answer upstream errors with proxy_intercept_errors on; and error_page. For Nginx's own pages, server_tokens off; is covered above.

Here is a small configuration that applies these directives to a proxied location. The before and after headers are illustrative, not captured from a real system. Adjust the upstream name and the header list to what your own responses show.

server_tokens off;

location / {
    proxy_pass http://app_backend;
    proxy_hide_header X-Powered-By;
    proxy_hide_header X-AspNet-Version;
    proxy_hide_header X-Generator;
    proxy_hide_header Via;
    proxy_intercept_errors on;
}

# For a FastCGI upstream, use this instead of proxy_hide_header:
# fastcgi_hide_header X-Powered-By;
# Illustrative: before
HTTP/1.1 200 OK
Server: nginx/1.26.3
X-Powered-By: ExampleFramework/4.2
X-AspNet-Version: 4.0.30319
Via: 1.1 internal-cache

# Illustrative: after
HTTP/1.1 200 OK
Server: nginx

Test with sudo nginx -t and reload as before, then request a page and an error page and read the headers again. Hiding these headers reduces what a casual visitor learns. It is one small layer, not a defence on its own, and it does not fix any vulnerability behind it. If you would like to see what your public surface actually reveals, we can run a scoped external test against a scope we agree together. Contact us to get started.

Verification checklist

  • Run sudo nginx -t and confirm it reports the test is successful.
  • Reload with sudo systemctl reload nginx.
  • Run curl -I https://your-site/ and check the Server line.
  • Request a page that does not exist and check both the header and the body: curl -si https://your-site/does-not-exist.
  • Repeat for every server block and any proxy or CDN in front of Nginx, since those can add their own Server header.

What this does and does not do

Hiding the version makes simple version fingerprinting harder. It does not fix any vulnerability, and other techniques can still identify the server. Keeping Nginx patched and up to date matters far more than what the header says.

What an external penetration test looks at beyond the Server header: an illustrative example

This is an illustrative scenario, not a real client or case study.

Hiding the version is one small fix. If you run internet-facing servers and are deciding whether to bring in a tester, it helps to know what else an external test reviews and what you get at the end.

The scenario

A small company runs an Nginx reverse proxy in front of its web application. On the same public IP range it also exposes a VPN gateway for remote staff and a mail server.

How scope is agreed

Before any testing starts, the client and the tester align on:

  • The public IP ranges and hostnames that are in scope.
  • Confirming with the client before testing any subdomain discovered during the engagement that was not on the agreed list.
  • A testing window that suits the client's operations.
  • A named client contact to reach right away if a critical-severity issue turns up.

What the tester reviews

  • Internet-facing inventory. Which hosts and services in the agreed ranges actually answer from the internet, compared with what the team expects to be exposed.
  • Service identification. What software and versions are running. Hiding the version header makes this a bit harder, but it does not prevent it, because behavior, error pages, and other responses still give clues. That is why patching matters more than the header.
  • TLS configuration. Which protocol versions and ciphers are accepted, and whether certificates are valid and cover the right names.
  • Admin and status pages. Dashboards, status pages, and management interfaces that should only be reachable internally.
  • Directory listing. Folders that show their contents to anyone who browses to them.
  • Forgotten default virtual hosts. What the server returns for its bare IP address or an unexpected hostname. This is often an old or test site that nobody maintains.
  • Source-control folders and backup files. Leftovers such as a .git folder or archived copies of configuration files sitting in a web root.
  • Proxy and alias path configuration. Location, alias, and proxy rules that map URLs to files or upstream services in ways the team did not intend.
  • The VPN and mail services. Whether they are patched, how they handle sign-in, whether multi-factor authentication is enforced where expected, and whether the mail server relays mail it should not.

Example findings

A real report covers every confirmed finding. These four hypothetical rows show the level of detail each one carries. Severity depends on context, so these ratings apply to this example setup only.

Four hypothetical example findings from an illustrative external penetration test
Example findingSeverityWhat was observedBusiness impactRemediation guidance
Status page reachable from the internetMediumThe nginx status page on the main web hostname answered requests from any address and showed live connection and request counts.Outsiders could see operational details about the server and its traffic that help them plan and time further activity.Restrict the page with allow and deny rules so only internal addresses can reach it, or move it to a listener that is only reachable internally.
A .git folder served by a forgotten virtual hostHighAn old staging hostname that still pointed at the server served the application's .git folder from its web root.Anyone could retrieve the application's source code and history, including any credentials that were ever committed.Remove the folder and the unused virtual host, deny hidden paths, rotate any secrets found in the history, and add a catch-all default server.
Alias path misconfiguration exposes files outside the intended folderHighA location for static files and its alias used mismatched trailing slashes, so the server would also serve files from the folder above the intended one, including application configuration.Configuration files and the credentials in them could be read from the internet.Make the trailing slashes on the location and the alias match, and keep sensitive files out of folders next to a web root.
TLS allows outdated protocol versionsLowThe reverse proxy still accepted TLS 1.0 and TLS 1.1 connections alongside TLS 1.2 and TLS 1.3.Older clients could connect using protocols with known weaknesses, and the setup falls short of common configuration baselines.Allow TLS 1.2 and 1.3 only, then confirm the change from an external host.

Remediation snippets for the example findings

These short Nginx examples match the fixes above. We checked each one with nginx -t on nginx 1.26.3. Adjust addresses, hostnames, and paths to your setup, and test before reloading.

Limit the status page to local and internal addresses:

location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
}

Deny hidden paths such as .git while keeping /.well-known/ available for certificate renewal:

location ~ /\.(?!well-known/) {
    deny all;
}

Add a catch-all default server so unknown hostnames and the bare IP address do not land on a forgotten site. The ssl_reject_handshake directive needs nginx 1.19.4 or later:

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
    return 444;
}

Keep the trailing slashes on the location and the alias matched:

location /static/ {
    alias /var/www/app/static/;
}

Allow TLS 1.2 and 1.3 only, in the http or server block:

ssl_protocols TLSv1.2 TLSv1.3;

The report and the readout

The report describes the agreed scope and testing window, gives a short summary for leadership, and lists each confirmed finding with its severity, what was observed, the business impact, and remediation guidance. After the report is delivered, a readout call walks your team through the findings and helps you decide what to fix first.

Retest terms set in the statement of work.

A penetration test is not a compliance certification. It gives you independent evidence of testing that can be one input to a broader security or compliance program.

If your public footprint looks like this scenario, read about our network penetration testing service or contact us to agree a scope.

Have a system you want tested?

Tell us what you are working with and we will follow up with next steps.