Harden NGINX Security and Boost Performance: Step-by-Step Guide
Leaving NGINX at its default settings exposes your website to unnecessary security risks and performance problems. Protecting your server is crucial to safeguard your data and ensure a reliable experience for your visitors.
This guide walks you through practical, easy-to-follow steps to harden your NGINX configuration, making your website both safer and faster.
Prerequisites #
Before you begin, ensure you have:
- A domain name registered and managed through Cloudflare.
- A deployed website.
- Website files located under
/var/www/<name>/public_html
(adjust path as needed). - Basic familiarity with the command line.
Step 1: Modify Main Configuration #
Backup current configuration:
Creating a backup ensures you can quickly restore the original settings if needed.
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
Open configuration file:
sudo vim /etc/nginx/nginx.conf
Modify:
Snippets make managing NGINX easier by breaking settings into smaller files. This helps you find and change settings faster while keeping your configuration file clean.
# Main NGINX Configuration
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
multi_accept on;
}
http {
# Core Settings
include /etc/nginx/snippets/core.conf;
# Compression Settings
include /etc/nginx/snippets/compression.conf;
# SSL/TLS Settings
include /etc/nginx/snippets/ssl.conf;
# Security Headers and Policies
include /etc/nginx/snippets/security-headers.conf;
# Rate Limiting and DDoS Mitigation
include /etc/nginx/snippets/rate-limiting.conf;
# Logging
access_log /var/log/nginx/access.log;
# Virtual Hosts
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Snippet: core.conf
#
Create snippet:
sudo vim /etc/nginx/snippets/core.conf
Add:
# HTTP Core Behaviour
sendfile on;
tcp_nopush on;
tcp_nodelay on;
types_hash_max_size 2048;
server_tokens off;
# MIME Types
include /etc/nginx/mime.types;
default_type text/plain;
# Client Body and Header Limits
client_max_body_size 10M;
client_body_buffer_size 128K;
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;
# Timeouts
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
keepalive_timeout 10s;
Explanation
File Handling and Performance
sendfile on; # Enable efficient file transfer
tcp_nopush on; # Delay packet transmission to optimise file delivery
tcp_nodelay on; # Transmit data immediately without waiting
types_hash_max_size 2048; # Increases MIME type lookup performance
- What it does: These settings control how files are sent from the server to a visitor.
- Why it matters: They help send data faster and more efficiently. Think of it as the difference between delivering one package at a time versus bundling them smartly. This reduces delay and makes the website feel faster.
Security by Hiding Server Info
server_tokens off; # Remove Nginx version info from error pages and headers
- What it does: Hides the version of NGINX being used.
- Why it matters: Prevents attackers from knowing which version you're running, making it harder for them to find and use known security weaknesses.
File Type Handling
include /etc/nginx/mime.types; # Load list of known file extensions and types
default_type text/plain; # Use plain text when type is unknown (safe default)
- What it does: Tells the server what kind of content it's dealing with (images, text, video).
- Why it matters: Helps the browser display things correctly. If the type isn't known, it defaults to safe plain text, avoiding risks like unintended downloads or script execution.
Limiting Request Size
client_max_body_size 10M; # Set maximum upload size to 10MB
client_body_buffer_size 128K; # Define buffer size for incoming request body
client_header_buffer_size 4k; # Define size limit for a single header line
large_client_header_buffers 4 8k; # Allow four large headers, each up to 8KB
- What it does: Limits how much data a visitor can send to your site in one go (like file uploads or form data).
- Why it matters: Prevents someone from overloading the server by sending huge or malformed data. It's like not letting someone bring a shipping container to your front desk.
Timeout Settings
client_body_timeout 10s; # Wait up to 10 seconds for full body from client
client_header_timeout 10s; # Wait up to 10 seconds for full header from client
send_timeout 10s; # Wait up to 10 seconds to transmit response to client
keepalive_timeout 10s; # Hold connection open for reuse for up to 10 seconds
- What it does: These set how long the server waits for data before giving up.
- Why it matters: Stops slow or abusive connections from wasting server resources. This keeps things responsive for legitimate users.
Summary
Setting Area | Purpose | Benefit |
---|---|---|
File Handling | Speeds up data delivery | Faster websites |
Server Info Hiding | Removes version numbers | Increases security |
MIME Types | Defines content types | Prevents display errors or exploits |
Size Limits | Caps data sent by users | Blocks abusive uploads |
Timeouts | Cuts off slow users | Frees up server for real visitors |
Snippet: compression.conf
#
Create snippet:
sudo vim /etc/nginx/snippets/compression.conf
Add:
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_disable "msie6";
gzip_types
text/plain
text/css
image/avif
image/svg+xml
application/json
application/javascript
application/xml
application/xml+rss
text/javascript
application/font-woff
application/font-woff2;
Explanation
Enable Compression
gzip on; # Enable gzip compression
- What it does: Activates gzip compression on the server.
- Why it matters: Makes websites load faster by reducing file size.
Adjust Behaviour for Proxies and Clients
gzip_vary on; # Add 'Vary: Accept-Encoding' for proxy caching
gzip_proxied any; # Allow compression for all proxied requests
gzip_disable "msie6"; # Disable gzip for Internet Explorer 6 (incompatible)
- gzip_vary on: Informs proxies and browsers that both compressed and uncompressed versions are available.
- gzip_proxied any: Allow compression even when requests come through a proxy (e.g. content delivery networks).
- gzip_disable "msie6": Disable gzip for Internet Explorer 6, which cannot handle it properly.
Set Compression Efficiency and Buffering
gzip_comp_level 6; # Set moderate compression level (balanced speed and efficiency)
gzip_buffers 16 8k; # Allocate 16 buffers of 8KB for compressed output
gzip_http_version 1.1; # Enable gzip only for HTTP/1.1 clients
- gzip_comp_level 6: Balance between compression strength and CPU usage. Level 6 gives good compression without high cost.
- gzip_buffers 16 8k: Allocate buffers for compressed data. Helps handle larger responses efficiently.
- gzip_http_version 1.1: Only apply compression for modern browsers that support HTTP/1.1.
Choose File Types to Compress
gzip_types # List file types to compress (text, fonts, scripts, etc.)
text/plain
text/css
image/avif
image/svg+xml
application/json
application/javascript
application/xml
application/xml+rss
text/javascript
application/font-woff
application/font-woff2;
- What it does: Specifies the types of content to compress.
- Why it matters: Focuses on types that benefit most from compression, like text, code, fonts, and vector images.
Summary
Setting Area | Purpose | Benefit |
---|---|---|
Compression On | Reduces data size | Speeds up site loading |
Proxy Handling | Ensures compatibility | Works with CDNs and older clients |
Compression Level | Adjusts CPU vs size savings | Efficient performance |
MIME Types | Targets specific content types | Optimises what gets compressed |
Snippet: ssl.conf
#
Create snippet:
sudo vim /etc/nginx/snippets/ssl.conf
Add:
# TLS Configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
Explanation
TLS Protocol Version
ssl_protocols TLSv1.3; # Allow only TLS version 1.3 for stronger security and better speed
- What it does: Only allows the latest, most secure version of TLS.
- Why it matters: Older versions have known weaknesses. Using only TLS 1.3 protects against many attacks and improves speed.
Cipher Suites and Preferences
ssl_prefer_server_ciphers on; # Force server to choose preferred strong ciphers over client preferences
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; # Use strong encryption algorithms ensuring confidentiality and integrity
- What it does: The server chooses the best encryption methods (ciphers) from a secure list.
- Why it matters: Ensures strong encryption that is hard to break, even if the client supports weaker options.
Session Management
ssl_session_timeout 1d; # Set session timeout to one day to balance security and performance
ssl_session_cache shared:SSL:10m; # Allocate 10MB of shared memory to cache SSL sessions for faster reconnects
ssl_session_tickets off; # Disable session tickets to prevent replay attacks
- What it does: Controls how long encrypted sessions last and how session data is stored.
- Why it matters: Proper session handling improves performance and prevents certain attacks, like replay attacks.
Summary
Setting Area | Purpose | Benefit |
---|---|---|
TLS Version | Use only TLS 1.3 | Maximum security and protection |
Cipher Suites | Specify strong ciphers and preferences | Ensure strong encryption |
Session Management | Control session timeout and caching | Balance speed and security |
Snippet: security-headers.conf
#
Create snippet:
sudo vim /etc/nginx/snippets/security-headers.conf
Add:
# Hardened Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=()" always;
add_header Cache-Control "public, max-age=3600, must-revalidate";
# Cross-Origin Isolation
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
Explanation
Strict Transport Security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
- What it does: Forces browsers to use secure HTTPS connections only.
- Why it matters: Protects users from attackers trying to intercept or change data.
Frame Options
add_header X-Frame-Options "SAMEORIGIN" always;
- What it does: Prevents your site from being displayed inside a frame on other sites.
- Why it matters: Stops clickjacking attacks where users are tricked into clicking hidden buttons.
Content Type Options
add_header X-Content-Type-Options "nosniff" always;
- What it does: Stops browsers from guessing content types.
- Why it matters: Prevents execution of malicious scripts disguised as safe files.
Cross-Site Scripting (XSS) Protection
add_header X-XSS-Protection "1; mode=block" always;
- What it does: Enables basic browser protection against some types of XSS attacks.
- Why it matters: Blocks detected malicious scripts from running.
Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- What it does: Controls how much information about your site is sent when users click links.
- Why it matters: Protects user privacy by limiting sensitive data exposure.
Permissions Policy
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=()" always;
- What it does: Restricts access to device features like camera and microphone.
- Why it matters: Prevents websites from requesting unnecessary or risky permissions.
Cache Control
add_header Cache-Control "public, max-age=3600, must-revalidate";
- What it does: Controls how browsers and proxies cache content.
- Why it matters: Ensures users get fresh content while optimising load speed.
Cross-Origin Isolation Headers
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
- What it does: Protect against certain web attacks by isolating your site’s resources.
- Why it matters: Enable advanced browser features safely, like improved security and performance.
Summary
Header | Purpose | Benefit |
---|---|---|
Strict-Transport-Security | Enforce HTTPS | Protects data from interception |
X-Frame-Options | Prevent framing | Stops clickjacking attacks |
X-Content-Type-Options | Disable content sniffing | Avoid execution of malicious files |
X-XSS-Protection | Enable browser XSS filter | Block some cross-site scripting attacks |
Referrer-Policy | Control referrer info | Protect user privacy |
Permissions-Policy | Restrict device API access | Prevent unauthorised access to sensitive features |
Cache-Control | Manage caching behaviour | Balance speed and content freshness |
Cross-Origin Policies | Enforce resource isolation | Improve security and enable modern web features |
Snippet: rate-limiting.conf
#
Create snippet:
sudo vim /etc/nginx/snippets/rate-limiting.conf
Add:
# Rate Limiting to Protect Against Abuse
# Define Rate Limit Zone (10 requests/sec per IP)
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
Explanation
Rate Limiting
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
- What it does: Limits each visitor (identified by their IP address) to a maximum of 10 requests per second.
- Why it matters: Prevents abusive or excessive use that could slow down or crash the website, like stopping someone from making too many phone calls at once.
Summary
Feature | Purpose | Benefit |
---|---|---|
Rate limiting zone | Track and limit visitor requests | Protect server from overload or abuse |
Limit of 10r/s | Max 10 requests per second per IP | Prevents denial of service or slowdowns |
Test and Enforce Rules #
Test configuration and reload NGINX if no errors:
sudo nginx -t && sudo systemctl reload nginx
Step 2: Whitelist Cloudflare IPs #
Whitelisting Cloudflare IPs ensures that only trusted Cloudflare servers can connect directly to your NGINX server. This protects your website from direct attacks, hides your server’s real IP, and helps prevent abuse or malicious traffic bypassing Cloudflare’s security features.
Cloudflare publishes its IP ranges here:
Download and format Cloudflare IP lists:
{
curl -s https://www.cloudflare.com/ips-v4
echo ""
curl -s https://www.cloudflare.com/ips-v6
} | sed 's/^/allow /; s/$/;/' | sudo tee /etc/nginx/cloudflare-ips.conf > /dev/null
Explanation:
curl -s https://www.cloudflare.com/ips-v4
fetches IPv4 addressesecho ""
ensures a newline between IPv4 and IPv6 entriescurl -s https://www.cloudflare.com/ips-v6
fetches IPv6 addressessed
formats each line asallow <IP>;
for NGINX syntaxsudo tee
writes the output with elevated permissions
Verify generated file:
sudo vim /etc/nginx/cloudflare-ips.conf
Open site configuration:
sudo vim /etc/nginx/sites-enabled/example.com
Add:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
# Allow only Cloudflare IP ranges
include /etc/nginx/cloudflare-ips.conf;
# Block all other IPs
deny all;
...
}
Test configuration and reload NGINX if no errors:
sudo nginx -t && sudo systemctl reload nginx
Step 3: Verify Website Functionality #
Open in browser:
https://example.com