Hardening Nginx Reverse Proxy: A Production Case Study
Last year I stood up a self-hosted reverse proxy stack on a $6 VPS to front three internal services — a Grafana dashboard, a private Git instance, and a custom API gateway. The goal was straightforward: expose minimal attack surface while keeping latency under 50ms for users in Tehran and Frankfurt. I chose Nginx for its battle-tested module ecosystem and predictable memory footprint. What I did not expect was how quickly the default configuration would become a liability the moment traffic exceeded a few hundred requests per minute.
The default Nginx configuration is a security liability. It enables unnecessary modules, leaves verbose error messages enabled, and permits HTTP methods that have no business reaching your origin. Every one of these is a door that an attacker can walk through. I learned this the hard way when a simple directory traversal attempt against my default config returned a full directory listing of internal service paths.
The Hardening Steps
Here is what I changed, in order of impact:
- Disabled unused modules: I stripped the Nginx build to only include the modules I actually needed — HTTP, SSL, and gzip. Every disabled module reduces the attack surface.
- Locked down HTTP methods: I restricted the server to only accept GET, HEAD, and POST. PUT, DELETE, and OPTIONS were blocked at the Nginx level before they could reach any backend service.
- Enabled TLS 1.3 only: I disabled TLS 1.0, 1.1, and 1.2 entirely. The configuration uses modern cipher suites and prefers server-side cipher ordering to prevent downgrade attacks.
- Added security headers: I configured Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Strict-Transport-Security with a long max-age.
- Set up rate limiting: I implemented a basic rate limiter using the Nginx limit_req module to protect against brute-force and denial-of-service attempts.
The Result
After hardening, the server passed a full SSL Labs test with an A+ rating. The attack surface shrank dramatically. The configuration is now lean, auditable, and predictable — exactly what you want from a reverse proxy sitting in front of your most sensitive services.
The lesson is simple: the default configuration of any service is designed for convenience, not security. If you are running Nginx in production, take the time to strip it down to only what you need. The effort is minimal compared to the risk you are eliminating.
One thing I wish I had done earlier is adding request filtering at the Nginx level. I now block any request with a User-Agent that matches known scanner signatures, rate-limit requests to /wp-admin and /wp-login.php to prevent brute-force attempts, and reject any request with a Content-Length header exceeding 10MB. These are small changes that add up to a meaningful reduction in noise and attack surface.
If you are running a reverse proxy in production, audit your Nginx configuration this week. The defaults are a starting point, not a destination. The difference between a hardened server and an exposed one is usually a handful of lines in a config file.