ITYS I Told You So

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.conf and included confs: root:root, 640
    • /var/log/nginx: root:adm (or root:nginx), 750; logs 640
    • Web roots owned by deploy user, readable by nginx (no write).
  • Remove default/example sites and autoindex configs.

Should

  • Move any secret files (JWT keys, upstream creds) to root-only dir and load via env or include with ro perms.

πŸ”—3 Systemd hardening (STIG-leaning)

Create /etc/systemd/system/nginx.service.d/hardening.conf:

[Service]
User=nginx
Group=nginx
AmbientCapabilities=
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
MemoryDenyWriteExecute=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service @file-system @network-io
ReadWritePaths=/var/log/nginx /var/cache/nginx
ReadOnlyPaths=/etc/nginx /var/www
RuntimeDirectory=nginx
RuntimeDirectoryMode=0750
UMask=0027

Then: systemctl daemon-reload && systemctl restart nginx


πŸ”—4 Core NGINX security options

In /etc/nginx/nginx.conf:

user  nginx;
worker_processes  auto;

events {
    worker_connections  1024;
}

http {
    # CIS: minimize info leakage
    server_tokens off;
    more_clear_headers Server;  # if using headers-more-nginx-module; otherwise rely on server_tokens off

    # Logging (structured, includes request ID)
    log_format 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"';
    access_log /var/log/nginx/access.log main;
    error_log  /var/log/nginx/error.log warn;

    # Timeouts (reduce resource abuse)
    send_timeout 10s;
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 30s;
    keepalive_requests 100;

    # Request size and buffering
    client_max_body_size 10m;      # tighten per app
    large_client_header_buffers 2 8k;

    # MIME sniffing and compression safety
    types_hash_max_size 4096;
    include       mime.types;
    default_type  application/octet-stream;
    gzip off;  # prefer upstream or safe compression settings if needed

    # Proxy sane defaults (if reverse proxying)
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_connect_timeout 5s;
    proxy_read_timeout 30s;
    proxy_send_timeout 30s;

    # Rate limiting / DoS control (tune)
    limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    # Include sites
    include /etc/nginx/conf.d/*.conf;
}

Per vhost (/etc/nginx/conf.d/site.conf):

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    # TLS (STIG/CIS modern)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    # If OpenSSL 1.1.1+/3.x, use modern suites:
    ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:HIGH:!aNULL:!MD5:!DES:!3DES:!RC4:!RC2:!SEED:!IDEA';
    ssl_session_timeout 10m;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # HSTS (enable after validating HTTPS is permanent)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # OCSP stapling (provide resolvers)
    resolver 1.1.1.1 9.9.9.9 valid=300s;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Security headers (tune CSP for your app)
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;       # or SAMEORIGIN if needed
    add_header Referrer-Policy "no-referrer" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    # Content-Security-Policy example (tighten for your assets)
    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;

    # Request limits
    limit_req zone=req_per_ip burst=20 nodelay;
    limit_conn conn_per_ip 20;

    # App root
    root /var/www/example;
    index index.html;

    # No directory listings
    autoindex off;

    access_log /var/log/nginx/example_access.log main;
    error_log  /var/log/nginx/example_error.log warn;
}

πŸ”—5 Certificates and keys

Must

  • Private keys owned by root:root with 600 perms; 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 4096 and ssl_dhparam in 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 journald rate 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_path and proxy_temp_path to dirs owned by nginx with 750 perms, on a partition with nodev,nosuid,noexec mount 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 0 unless needed.
  • Correct file contexts under /var/www and /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

nginx -t && systemctl reload nginx
nginx -T | sed -n '1,120p'              # quick render of active config
ss -ltn | grep -E '(:80|:443)'
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null 2>/dev/null | openssl x509 -noout -text | sed -n '1,20p'
curl -I https://example.com

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.
  • nginx system user, no shell; umask 027.
  • /etc/nginx 750, 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

nginx-hardening.sh