Knowledge Stream

Engineering Log

Nginx Web Server Hardening for Production Environments

Operational hardening of a publicly exposed Nginx reverse proxy running on Ubuntu 24.04 LTS.

~ Sai Terukula
In This Log

Operational hardening of a publicly exposed Nginx reverse proxy running on Ubuntu 24.04 LTS.

Executive Summary#

This hardening exercise focused on reducing unnecessary exposure at the edge layer. The goal was simple: move from a default-compatible Nginx setup to a security-enforced production baseline.

The changes addressed:

  • Version disclosure
  • Weak TLS fallback
  • Missing browser security headers
  • Abuse from automated traffic
  • Accidental exposure of sensitive files

The result was a cleaner transport posture, stronger client-side policy enforcement, and measurable reduction in edge-layer risk.

Environment#

  • Ubuntu 24.04 LTS
  • Nginx 1.24+
  • Reverse proxy setup
  • Public HTTPS endpoint on port 443
  • TLS certificates installed via Let’s Encrypt

Relevant configuration files:

  • /etc/nginx/nginx.conf
  • /etc/nginx/sites-available/yourdomain
  • /etc/nginx/sites-enabled/yourdomain
  • /etc/letsencrypt/live/yourdomain.com/

Baseline Hardening#

1. Disable Version Disclosure#

By default, Nginx exposes its version in the Server header.

Edited:

sudo vim /etc/nginx/nginx.conf

Inside the http {} block:

server_tokens off;

Validation:

sudo nginx -t
sudo systemctl reload nginx
curl -I https://yourdomain.com

The response now returns:

Server: nginx

without revealing the exact version.

2. Enforce Modern TLS Only#

In the HTTPS server block:

sudo vim /etc/nginx/sites-available/yourdomain

Configured:

ssl_protocols TLSv1.2 TLSv1.3;

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

ssl_prefer_server_ciphers off;

TLS 1.0 and 1.1 were explicitly removed. No manual cipher pinning was applied - modern OpenSSL defaults are sufficient unless compliance requires otherwise.

Validation:

sudo nginx -t

Browser-Side Security Controls#

3. Enable HSTS#

Inside the HTTPS server block:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

This forces clients to use HTTPS for one year. preload was intentionally not added until subdomain readiness is confirmed.

4. Add Security Headers#

To reduce browser-level risks:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

add_header Content-Security-Policy "
default-src 'self';
img-src 'self' https: data:;
script-src 'self';
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
" always;

X-XSS-Protection was intentionally excluded since it is deprecated. CSP is the correct modern control.

Abuse Controls#

5. Limit Request Body Size#

Inside /etc/nginx/nginx.conf under http {}:

client_max_body_size 10m;

This prevents oversized payload abuse. The value was set based on actual upload requirements.

6. Apply Rate Limiting#

Inside http {}:

limit_req_zone $binary_remote_addr zone=global_limit:10m rate=10r/s;

Inside the server block:

location / {
    limit_req zone=global_limit burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
}

If deployed behind a load balancer or CDN, configure real client IP handling first:

real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;

Otherwise, rate limiting applies to the proxy address instead of the actual client.

7. Restrict HTTP Methods#

Inside the server block:

if ($request_method !~ ^(GET|POST|HEAD)$) {
    return 405;
}

A 405 response keeps logs clear and observable. Silent drops were avoided to maintain traceability.

8. Protect Sensitive Files#

To prevent accidental leakage:

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

location ~* \.(env|git|htaccess|htpasswd|ini|log|bak)$ {
    deny all;
}

This blocks common sensitive artifacts while still allowing .well-known/acme-challenge for certificate renewal.

Validation Workflow#

Local validation:

sudo nginx -t
sudo systemctl status nginx
sudo ss -tulpn | grep nginx
curl -I https://yourdomain.com

Confirmed:

  • TLS 1.2 / 1.3 only
  • HSTS header present
  • No version disclosure
  • CSP and security headers returned

External validation:

  • SSL Labs Server Test
  • Mozilla Observatory

Results showed improved protocol posture and strong header compliance.

Operational Results#

After rollout:

  • Transport posture moved to modern-only TLS.
  • Browser-side policy enforcement became consistent.
  • Automated abuse was throttled before upstream impact.
  • Hardening baseline documented for reproducibility.

Lessons Learned#

  • Default Nginx configuration is compatibility-focused, not security-focused.
  • TLS enforcement is foundational.
  • Rate limiting must account for real client IP.
  • Security headers meaningfully reduce browser-layer exposure.
  • Hardening should be part of infrastructure provisioning, not an afterthought.