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.confInside the http {} block:
server_tokens off;Validation:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://yourdomain.comThe response now returns:
Server: nginxwithout revealing the exact version.
2. Enforce Modern TLS Only#
In the HTTPS server block:
sudo vim /etc/nginx/sites-available/yourdomainConfigured:
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 -tBrowser-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.comConfirmed:
- 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.