4 min
NGINX Hardening
Matthew's NGINX hardening guidelines (CIS + STIG)
π1 OS baseline (before NGINX)
Must
- Patch OS, minimal packages only; enable unattended security updates.
- Create dedicated user/group:
nginx:nginx(no shell, no home):useradd --system --no-create-home --shell /usr/sbin/nologin nginx - Set system-wide umask
027(CIS). - Time sync (chrony/ntpd), correct time zone.
- Enable a host firewall to allow only 80/443 (and 22 from admin IPs).
- Install AIDE or similar; include
/etc/nginx/**, cert paths, and unit overrides.
Should
- Separate partitions for
/var/log,/var/www,/var/cache/nginx. - SELinux/AppArmor enforcing (configure for httpd_t or nginx profile).
π2 Install and file permissions
Must
-
Install from vendor or trusted repo; pin versions.
-
Ownership/permissions:
/etc/nginx: root:root,750/etc/nginx/nginx.confand included confs: root:root,640/var/log/nginx: root:adm (or root:nginx),750; logs640- Web roots owned by deploy user, readable by
nginx(no write).
-
Remove default/example sites and
autoindexconfigs.
Should
- Move any secret files (JWT keys, upstream creds) to root-only dir and load via env or include with
roperms.
π3 Systemd hardening (STIG-leaning)
Create /etc/systemd/system/nginx.service.d/hardening.conf:
[Service]
nginx
nginx
CAP_NET_BIND_SERVICE
true
true
true
strict
true
true
true
true
true
true
true
true
true
AF_INET AF_INET6 AF_UNIX
@system-service @file-system @network-io
/var/log/nginx /var/cache/nginx
/etc/nginx /var/www
nginx
0750
0027
Then: systemctl daemon-reload && systemctl restart nginx
π4 Core NGINX security options
In /etc/nginx/nginx.conf:
nginx;
auto;
events {
1024;
}
http {
off;
Server;
main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'rt=$request_time ua="$upstream_addr" urt="$upstream_response_time" '
'rid="$request_id"';
/var/log/nginx/access.log main;
/var/log/nginx/error.log warn;
10s;
10s;
10s;
30s;
100;
10m;2 8k;
4096;
include mime.types;
application/octet-stream;
off;
1.1;
Connection "";
Host $host;
X-Real-IP $remote_addr;
X-Forwarded-For $proxy_add_x_forwarded_for;
X-Forwarded-Proto $scheme;
5s;
30s;
30s;
$binary_remote_addr zone=req_per_ip:10m rate=10r/s;
$binary_remote_addr zone=conn_per_ip:10m;
include /etc/nginx/conf.d/*.conf;
}
Per vhost (/etc/nginx/conf.d/site.conf):
server {
443 ssl http2;
[::]:443 ssl http2;
example.com;
TLSv1.2 TLSv1.3;
on;
'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:HIGH:!aNULL:!MD5:!DES:!3DES:!RC4:!RC2:!SEED:!IDEA';
10m;
shared:SSL:10m;
off;
Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
valid=300s;
on;
on;
X-Content-Type-Options "nosniff" always;
X-Frame-Options "DENY" always;Referrer-Policy "no-referrer" always;
Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;
zone=req_per_ip burst=20 nodelay;
conn_per_ip 20;
/var/www/example;
index index.html;
off;
/var/log/nginx/example_access.log main;
/var/log/nginx/example_error.log warn;
}
π5 Certificates and keys
Must
- Private keys owned by
root:rootwith600perms; store under/etc/ssl/private. - Use 2048-bit+ RSA (or ECDSA P-256/384). Prefer ECDSA + RSA dual certs if client mix.
- Rotate certs; enforce OCSP stapling; monitor expiry.
- Generate strong DH params (if using DH):
openssl dhparam -out /etc/ssl/dhparam.pem 4096andssl_dhparamin vhost (only if needed).
π6 Logging, rotation, and monitoring
Must
- Log format includes timing and request_id (shown above).
- Logrotate with strict perms:
/var/log/nginx/*.log {
weekly
rotate 8
missingok
compress
delaycompress
notifempty
create 0640 root adm
sharedscripts
postrotate
/bin/systemctl kill -s USR1 nginx || true
endscript
}
- Forward logs to a central SIEM (STIG).
Should
- Enable
journaldrate limits; ensure time sync.
π7 Access control and auth
Must
- Avoid HTTP Basic wherever possible; if used, protect with rate-limits and
fail2ban(nginx-http-auth, nginx-badbots jails). - Restrict admin paths by IP and/or mTLS.
- Remove default and sample configs/sites; 404 on unknown hosts.
π8 Content, uploads, and temp paths
Must
- Set
client_body_temp_pathandproxy_temp_pathto dirs owned bynginxwith750perms, on a partition withnodev,nosuid,noexecmount options if feasible. - Validate and limit upload sizes (
client_max_body_size) and types server-side.
π9 SELinux/AppArmor (pick one)
SELinux
- Enforcing mode.
- Typical booleans (as needed):
setsebool -P httpd_can_network_connect 1(reverse proxy)httpd_can_sendmail 0,httpd_enable_cgi 0unless needed. - Correct file contexts under
/var/wwwand/var/log/nginx.
AppArmor
- Enforce profile
usr.sbin.nginx; permit read of/etc/ssl/**,/var/www/**, write to/var/log/nginx/**,/var/cache/nginx/**.
π10 Optional but strong
- WAF (ModSecurity v3 with OWASP CRS) in detection β blocking mode.
- Bot and scan throttling with
map+limit_req. - mTLS for admin/API subpaths.
- Subresource Integrity (SRI) for third-party assets.
- Immutable deploys: serve static files from read-only dirs; use content-addressed paths.
π11 Verification and recurring checks
Commands
&&
| |
| |
Automated
- Weekly
oscap/OpenSCAP scan (CIS/STIG profiles if available). - Monthly TLS scan (testssl.sh or sslyze).
- Quarterly restore from backup of
/etc/nginx, certs, and a sample site.
πQuick βdiffableβ checklist (Must)
- OS minimal, patched, firewall on; SELinux/AppArmor enforcing.
-
nginxsystem user, no shell; umask 027. -
/etc/nginx750, confs 640; logs 640/750 dirs; webroot read-only to nginx. - systemd hardening override applied (ProtectSystem=strict, etc.).
-
server_tokens off; no default/autoindex. - TLS 1.2/1.3 only; modern ciphers; HSTS; OCSP stapling.
- Security headers (X-CTO, XFO, RP, PP, CSP).
- Rate limits and sane timeouts; request/body size limits.
- Log format with request_id; logrotate with 0640 and USR1 reopen.
- Secrets (keys) 600 root:root; DHparams if used.
-
Recurring verification:
nginx -t, TLS scan, OpenSCAP, AIDE checks.
πScript Usage
sudo bash harden-nginx.sh
βdomain example.com
βwebroot /var/www/example
βowner deploy
βemail admin@example.com