Type something to search...
How to secure an Nginx web server?

How to secure an Nginx web server?

To secure an Nginx web server, keep Nginx updated, hide its version number, serve everything over HTTPS with TLS 1.2 and 1.3 only, add security headers such as HSTS and X-Content-Type-Options, block access to hidden and sensitive files, disable directory listing, limit request sizes and rates, restrict admin areas, and make sure PHP only executes where it should. Run Nginx with minimal privileges and test every change with nginx -t before reloading.

Nginx is fast and secure by design, but its defaults are built for compatibility rather than strict security. A few careful configuration changes close the most common gaps. This guide walks through a practical hardening checklist for Nginx on Ubuntu or Debian (with notes for RHEL-based systems), including ready-to-use configuration snippets for TLS, headers, rate limiting, and PHP applications like WordPress.

Before You Start

  • Back up your configuration: Copy /etc/nginx/ before making changes.
sudo cp -a /etc/nginx /etc/nginx.backup-$(date +%F)
  • Always test before reloading: sudo nginx -t checks syntax. Only reload if it passes.
  • Reload, don't restart: sudo systemctl reload nginx applies changes without dropping connections.
  • Know your file layout: On Ubuntu and Debian, site configs usually live in /etc/nginx/sites-available/ with symlinks in sites-enabled/. On RHEL-based systems, they're typically in /etc/nginx/conf.d/.

Step 1: Keep Nginx Updated

Security fixes only help if you install them. Use your distribution's packages with automatic security updates, or the official Nginx repository if you need newer versions.

Check your version:

nginx -v

Nginx has two branches: mainline (newest features and fixes) and stable (fewer changes). Both receive security fixes. Either is fine, as long as you keep it updated.

Step 2: Hide the Nginx Version

By default, Nginx includes its version number in the Server header and on error pages. Hiding it doesn't fix vulnerabilities, but it stops you from advertising which version you run.

In the http block of /etc/nginx/nginx.conf:

server_tokens off;

The header will then just say Server: nginx.

Step 3: Use Strong TLS Settings

Serve every site over HTTPS and allow only modern protocols. Get free certificates with Certbot:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Then use a solid TLS configuration. This is based on Mozilla's "intermediate" recommendations, which balance security with broad browser support. Put it in the http block or a shared snippet like /etc/nginx/snippets/tls.conf:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

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

ssl_stapling on;
ssl_stapling_verify on;

The ssl_ciphers list applies to TLS 1.2 only. TLS 1.3 ciphers are secure by default. Note that some certificate authorities, including Let's Encrypt, have been phasing out OCSP, in which case stapling simply has no effect and can be removed.

Redirect HTTP to HTTPS

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

Certbot's Nginx plugin can manage the redirect for you, but it's good to understand what it does.

Step 4: Add Security Headers

Security headers tell browsers to enable protections. Put them in a snippet, for example /etc/nginx/snippets/security-headers.conf:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Include it in each HTTPS server block:

include snippets/security-headers.conf;

Important details:

  • The always parameter makes Nginx send headers on error responses too.
  • Header inheritance: If a location block has its own add_header, it replaces all headers from the parent level. Re-include the snippet inside any location that adds headers.
  • HSTS: Only add includeSubDomains if every subdomain supports HTTPS. Start with a short max-age while testing.
  • Content Security Policy: A CSP is the strongest header against cross-site scripting, but it must be tailored to your site. Start with Content-Security-Policy-Report-Only to see what would break.

Step 5: Block Hidden and Sensitive Files

Files like .git, .env, .htpasswd, and backup files should never be served. Inside each server block:

# Block hidden files and folders, but allow Let's Encrypt validation.
location ~ /\.(?!well-known) {
    deny all;
}

# Block common backup and config file extensions.
location ~* \.(bak|conf|dist|ini|log|old|orig|save|sql|swp|tmp)$ {
    deny all;
}

If you use version control on the server, a leaked .git folder can expose your entire source code, including secrets, so this rule matters.

Step 6: Disable Directory Listing

Directory listing is off by default in Nginx. Make sure you haven't enabled it anywhere:

sudo grep -rn "autoindex on" /etc/nginx/

If a location really needs a file listing, enable it only there, ideally behind authentication.

Step 7: Limit Request Size and Timeouts

Large or slow requests can be used to tie up server resources. In the http block:

client_max_body_size 16m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 15s;

Set client_max_body_size to match the largest upload your application needs. The default is 1 MB, which is often too small for CMS uploads, but don't set it much larger than necessary.

Step 8: Add Rate Limiting

Rate limiting slows down brute-force attacks and abusive clients. Define zones in the http block:

limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_status 429;

Apply them in your server or location blocks:

server {
    # ...
    limit_req zone=general burst=20 nodelay;
    limit_conn perip 20;

    location = /wp-login.php {
        limit_req zone=login burst=3 nodelay;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Tune the rates to your real traffic. Too strict, and you'll block legitimate visitors, especially those behind shared networks. If you're behind a CDN or proxy, configure the realip module so limits apply to the real client IP rather than the proxy:

set_real_ip_from 203.0.113.0/24;   # Replace with your proxy's IP ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Only trust headers from ranges you know belong to your proxy, or clients could spoof their IP.

Step 9: Restrict Allowed HTTP Methods

Most websites only need GET, HEAD, and POST. You can reject others:

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

Be careful with APIs. REST APIs, including the WordPress REST API, legitimately use PUT, PATCH, and DELETE. Apply this only to sites or locations where you know other methods aren't needed.

Step 10: Protect Admin Areas

Restrict sensitive paths by IP address:

location /admin/ {
    allow 203.0.113.10;
    deny all;
}

Or add HTTP Basic Authentication as an extra layer. Create a password file:

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd adminuser

Then protect the location:

location /admin/ {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

Step 11: Handle PHP Safely

Misconfigured PHP handling is one of the most serious Nginx mistakes. If Nginx passes any request ending in .php to PHP-FPM without checking the file exists, an attacker who uploads a file might get it executed.

Use a PHP location that only runs files that actually exist:

location ~ \.php$ {
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

On Ubuntu and Debian, include snippets/fastcgi-php.conf; does the file check for you. Also:

  • Block PHP in upload folders: For WordPress, add this before the main PHP location, so it matches first:
location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
}
  • Keep cgi.fix_pathinfo safe: Modern PHP-FPM packages combined with try_files protect against the old path-info issue, but setting security.limit_extensions = .php in your PHP-FPM pool adds another safeguard.

Step 12: Run Nginx With Minimal Privileges

The Nginx master process runs as root so it can bind to ports 80 and 443, but worker processes should run as an unprivileged user. Check the user directive in nginx.conf:

user www-data;

On RHEL-based systems it's usually nginx. Make sure web files are owned by a deploy user, not the web server user, and that only directories that genuinely need writes (like uploads or cache) are writable by PHP.

sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

Step 13: Set a Default Server

Requests with unknown Host headers (for example, scanners hitting your IP directly) shouldn't land on your real site. Add a catch-all default server that closes the connection:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    ssl_reject_handshake on;
    return 444;
}

ssl_reject_handshake (Nginx 1.19.4 and later) rejects TLS connections for unknown hostnames without needing a certificate. Status 444 is an Nginx-specific code that closes the connection without a response.

Step 14: Log and Monitor

Keep access and error logs enabled, and review them regularly:

sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

Pair Nginx logs with Fail2Ban to automatically block IPs that probe for vulnerabilities or trigger rate limits repeatedly. Make sure log rotation is configured (it is by default on most distributions) so logs don't fill your disk.

Step 15: Test Your Configuration

After changes:

sudo nginx -t && sudo systemctl reload nginx

Then check the result from outside:

curl -sI https://example.com

You should see your security headers and no version number. Online tools like SSL Labs' server test and Mozilla Observatory give you a detailed grade for TLS and headers.


FAQ: Securing Nginx

Nginx has a good security track record and sensible defaults, but it's configured for compatibility. You should still hide the version, enforce modern TLS, add security headers, block sensitive files, and handle PHP safely.

In Nginx, an add_header directive in a location block replaces all headers inherited from the server block. Include your security headers snippet inside any location that adds its own headers.

Yes. TLS 1.0 and 1.1 are deprecated and no longer supported by modern browsers. Allow only TLS 1.2 and TLS 1.3.

Define a limit_req_zone in the http block with a low rate, then apply limit_req with that zone inside a location block matching your login URL. Tune the rate and burst to avoid blocking real users.

Only slightly. It doesn't fix any vulnerability, but it stops you from advertising your exact version to automated scanners. Keeping Nginx updated is far more important.

Add a location block that matches PHP files inside your uploads path and returns deny all, and place it before your main PHP location so Nginx matches it first.


Conclusion

Securing Nginx is mostly a matter of tightening defaults: keep it updated, hide the version, enforce TLS 1.2 and 1.3, add security headers, block hidden and sensitive files, and limit request sizes and rates. Handle PHP carefully so only real files execute, and never in upload folders, and run workers with minimal privileges.

Put your shared settings in reusable snippets, test every change with nginx -t, and verify the result with curl and online scanners. With these practices in place, your Nginx server will present a much smaller attack surface while staying just as fast.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
How does GDPR affect website security?

How does GDPR affect website security?

GDPR affects website security by turning it from a good habit into a legal obligation. If your website collects personal data from people in the EU (

Dive Deeper