Skip to content
Let’s plan the right software for your processes. Call us for a demo or a quote: +90 546 737 48 29

TR EN DE

Nginx security settings: rate limiting, headers, access restrictions

Nginx is the first thing every request to your site meets. That means many protections can be put in place here without touching the application's code: slowing down addresses that send too many requests, closing off files that should never be exposed, opening the admin page to certain addresses only, and cutting off requests that are too large or too slow.

These settings do not fix vulnerabilities in the application; they are a front layer that reduces the impact of a mistake and stops most automated scans before they reach the application. This guide explains what each setting does and how it is written in the Nginx configuration. Setting up HTTPS is covered in a separate guide: HTTPS in Nginx.

In brief

  • Requests for unknown host names are rejected in a default server block.
  • limit_req and limit_conn cap the requests and connections coming from a single address.
  • If a child block has its own add_header, the parent's headers are not inherited; shared headers must be repeated.
  • Hidden files, the admin path and the upload directory are each protected by their own location block.

Caution

Add the settings one at a time and test with "nginx -t" after every change. A wrong IP restriction or an overly strict rate limit can lock out you and genuine visitors too; choose the values to suit your own traffic.

On this page

Hardening in seven steps

  1. Reject unknown host names and hide the version

    Nginx matches the Host header of an incoming request against the server_name lines. If none matches, it hands the request to the default server for that port; unless you say otherwise, that is the first server block defined in the configuration. The result: automated scans that reach the server by IP address, or under a domain name that is not yours, end up at your real site.

    To prevent this, define a default (catch-all) block that belongs to no site. return 444; is a code specific to Nginx: it closes the connection without sending a response.

    Nginx
    # Do not show the version number on error pages or in the Server header
    server_tokens off;
    
    # Default block: requests whose Host matches no site
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;
        server_name _;
    
        # Reject TLS handshakes for unknown names
        ssl_reject_handshake on;
    
        # Close the connection without sending a response
        return 444;
    }

    ssl_reject_handshake on; rejects HTTPS connections for unknown names without presenting a certificate and is available in current Nginx versions; on older versions you need to define a self-signed certificate for this block. server_tokens off; removes the version number from error pages and from the Server header. On its own this is not protection; it merely gives less information to scans that pick targets by version. The real measure is keeping Nginx up to date.

    Diagram: a request whose Host header matches a configured domain goes to the site block; a request by IP address or with an unknown name is closed with 444 in the default block : Enlarge
  2. Rate limiting: limit_req and limit_conn

    Rate limiting caps the number of requests a single address can send in a given time. There are two separate tools:

    • limit_req_zone + limit_req: limits the request rate (for example 10 requests per second).
    • limit_conn_zone + limit_conn: limits the number of connections open at the same time.

    The …_zone directives are defined once in the http block: they set the key (usually the client address, $binary_remote_addr), the name and size of the shared memory zone and, for limit_req_zone, the rate as well. The limit itself is applied inside a server or location.

    Nginx
    # In the http block: zones are defined once
    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=conn:10m;
    
    # Return 429 instead of 503 for rejected requests
    limit_req_status  429;
    limit_conn_status 429;
    
    server {
        listen 443 ssl;
        server_name example.com;
    
        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
        # Whole site: 10 requests per second, a queue of 20 requests
        limit_req  zone=general burst=20 nodelay;
        # At most 20 simultaneous connections from one address
        limit_conn conn 20;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
        }
    
        # Login form: 5 requests per minute
        location = /login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://127.0.0.1:8080;
        }
    }

    Two parameters shape the behaviour:

    • burst: how many requests above the rate are queued rather than rejected straight away. Without it, every request over the rate is rejected, which is too harsh for the run of image and script requests that follows a page load.
    • nodelay: queued requests are processed immediately instead of being held back, while the slots in the queue free up at the configured rate. Without it, queued requests are delayed to fit the rate.

    Rejected requests receive a 503 by default. Use limit_req_status 429; and limit_conn_status 429; to return 429, "too many requests", instead; this sets them apart from genuine server errors in the logs. Give sensitive addresses such as the login form a separate, lower limit. If limit_req is defined inside a location, the limit_req from the level above does not apply in that block.

    Note: if Nginx sits behind a CDN or another proxy, $binary_remote_addr is the address of the server in front, not of the visitor, and the limit applies to all visitors together. In that case, first define the real client address with the set_real_ip_from and real_ip_header directives (only for proxy addresses you trust). Rate limiting alone does not stop high-volume attacks; for the layers involved, see the DDoS and bot attacks guide, and for login attempts the Login and session security guide.

    Diagram: three cases of rate limiting; with rate only the excess is rejected, with burst the excess is queued and delayed, with burst and nodelay queued requests are processed at once and anything beyond the queue receives 429 : Enlarge
  3. Security headers and the add_header inheritance trap

    Security headers switch on the browser's built-in protections. In Nginx they are added with add_header; the trailing always makes sure the header is also sent with error responses such as 404 and 500. For what each header means, see the Security headers and HTTPS guide.

    There is an important trap specific to Nginx: add_header directives are inherited from the level above only if the current block has no add_header at all. The moment you write a single add_header inside a location (a cache header, say), all the security headers from the server level disappear in that block.

    Nginx
    # server level: on all responses
    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 "geolocation=(), camera=(), microphone=()" always;
    
    location /assets/ {
        # This block has an add_header: the four headers above are NOT inherited here
        add_header Cache-Control "public, max-age=2592000";
    
        # Fix: define the shared headers in this block too
        # (the file contains the four add_header lines above)
        include /etc/nginx/snippets/security-headers.conf;
    }

    The reliable fix is to keep the shared headers in a separate file and add it with include to every block that uses add_header. After the change, check not just the home page but also static files and error pages with curl -sI.

    Comparison: once a single add_header is written inside a location, the security headers from the server level are not inherited; the right approach is to define the shared headers in that block too, with include : Enlarge
  4. Block access to hidden files

    Files and directories whose names start with a dot (.env, .git, .htpasswd) may contain passwords, keys and source code, and they are among the first places automated scans look. The one exception is the .well-known directory: it must stay open for standard tasks such as certificate validation.

    Nginx
    # Exception: .well-known stays open (certificate validation etc.)
    location ^~ /.well-known/ {
        allow all;
    }
    
    # Every file and directory starting with a dot: .env, .git, .htpasswd ...
    location ~ /\. {
        deny all;
    }
    
    # Backup and dump files forgotten in the web root
    location ~* \.(?:sql|bak|old|orig|log|ini)$ {
        deny all;
    }

    The ^~ modifier means that when this prefix matches, regular-expression location blocks are not checked, so requests under .well-known are not caught by the general ban. The third block closes off backup and dump files forgotten in the web root. Even so, the best approach is not to keep such files in the web root at all: Server and hosting security.

  5. Protect the admin path and the upload directory

    If the admin panel is only used from certain places, an IP restriction cuts off password-guessing attempts before they reach the login form. allow and deny rules are checked in the order they are written and the first match wins, which is why deny all; comes last.

    Nginx
    # ^~ : when this prefix matches, the outer regular-expression blocks are not checked
    location ^~ /admin/ {
        allow 203.0.113.10;        # office
        allow 198.51.100.0/24;     # VPN
        deny  all;
    
        # The restriction also applies to the PHP handler inside the block
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass unix:/run/php/php-fpm.sock;   # path varies by distribution
        }
    }

    There is a common mistake here. PHP sites usually have a regular-expression block of the form location ~ \.php$. If you put the restriction in an ordinary location /admin/ block, a request for /admin/index.php lands in the regular-expression block and the restriction is not applied. Using ^~ and placing the PHP handler inside the block, as in the example, prevents this. The PHP-FPM socket path varies by distribution.

    The same logic works in reverse for the upload directory: no script should ever run in a directory visitors can upload files to. The block below serves the files in the directory as static files only and returns 403 for requests with script extensions.

    Nginx
    # Upload directory: static files only
    location ^~ /upload/ {
        # Requests with script extensions are neither executed nor served
        location ~* \.(?:php|phtml|phar|pl|py|cgi|sh)$ {
            return 403;
        }
    }

    This is the second layer that stops an uploaded file from being executed on the server; the first layer is the checking done at upload time: File upload security.

    Diagram: a request for /admin/index.php is handled by the regular-expression PHP block rather than the ordinary prefix block, so the IP restriction is skipped; with ^~ the prefix block is chosen and the restriction applies : Enlarge
  6. Limit large and slow requests

    Requests with very large bodies consume disk space and memory, and requests that are deliberately sent very slowly tie up connections for a long time. Three directives limit both:

    DirectiveWhat does it limit?DefaultWhen exceeded
    client_max_body_sizeThe maximum size of the request body (an uploaded file, for example)1m413
    client_body_timeoutThe time between two successive read operations while the body is being read60s408
    client_header_timeoutThe time allowed for reading the whole of the request headers60s408
    Nginx
    # General limits
    client_max_body_size  2m;
    client_body_timeout   15s;
    client_header_timeout 15s;
    
    # A higher body limit only for the address where files are uploaded
    location /upload-file {
        client_max_body_size 20m;
        proxy_pass http://127.0.0.1:8080;
    }

    Keep the general limit low and give the address where files are uploaded a separate, higher value. If you use PHP, the upload_max_filesize and post_max_size settings must be consistent with this value as well. Making timeouts too short also cuts off real users on slow mobile connections; lower the values gradually and watch for 408 responses in the logs.

  7. Test, reload and verify from outside

    After every change, test the syntax and reload Nginx. Then confirm that the settings really work by sending requests to your own site from outside: hidden-file addresses should return 403 or 404, the headers should appear on static files too, and a request to the server's IP address should be closed without a response.

    Bash
    # Test the syntax, then reload
    sudo nginx -t
    sudo systemctl reload nginx
    
    # Headers: home page and a static file
    curl -sI https://example.com/
    curl -sI https://example.com/assets/site.css
    
    # Are hidden files blocked? (403 or 404 expected)
    curl -s -o /dev/null -w "%{http_code}\n" https://example.com/.env
    curl -s -o /dev/null -w "%{http_code}\n" https://example.com/.git/config
    
    # Request to the server's IP address: should be closed without a response
    curl -sI http://203.0.113.20/

    Keep a second session open while you try out an IP restriction; if a wrong rule locks you out of the panel, you can revert the configuration from that session.

    Layer diagram: default server, rate limiting, request limits, access restrictions, security headers and, innermost, the application : Enlarge

What do these settings not solve?

  • Application vulnerabilities: SQL injection, XSS and access-control errors are fixed in the code; an Nginx setting is no substitute.
  • High-volume attacks: traffic that fills the server's bandwidth has to be absorbed before it reaches the server (at the hosting provider or CDN level).
  • Distributed slow attempts: a small number of requests from each of many different addresses does not hit a per-address limit; login security must be built into the application too.
  • Compromised accounts: an IP restriction and a rate limit do not stop someone arriving from an allowed address with a valid password; two-factor authentication is needed.

How should you choose the values?

The numbers in the examples are a starting point, not a recommendation. The right value depends on your site's traffic:

  • Look at how many requests the browser sends when a page loads; burst should cover that number.
  • Many users leaving the same corporate network may appear under a single IP address; loosen the limit accordingly or treat known addresses separately.
  • After enabling the limit, watch the error log for rejected requests and the access log for 429 responses; if real users are being caught, raise the value.
  • In HTTP/2 and HTTP/3, each concurrent request counts as a separate connection for limit_conn; do not set the value too low.
  • Consider whether you need to exempt search engine bots and your own monitoring tools from the limit.

Checklist

  • A default block is defined for ports 80 and 443; requests with an unknown Host do not reach the site.
  • server_tokens off; is set and Nginx is up to date.
  • There is a general rate limit and a specific one for the login address; rejected requests receive 429.
  • Behind a proxy, the real client address is defined correctly.
  • Security headers are defined with always and repeated in child blocks that use add_header.
  • Files starting with a dot and backup extensions are blocked; .well-known is open.
  • The admin path is restricted by IP and the restriction also applies to .php requests.
  • No script runs in the upload directory.
  • Body size and timeout limits are defined; the upload address has its own value.
  • Every change has been tested with nginx -t and verified from outside.

Frequently asked questions

Does rate limiting affect search engine bots?

A very low limit slows bots down too, and a 429 response delays crawling. Set the limit according to your real traffic and watch the logs to see which requests are being rejected.

Does server_tokens off remove the Server header completely?

No. Only the version number is removed; the header still says "nginx". This setting reduces information leakage but is no substitute for updating.

Can I return 403 or 404 instead of 444?

Yes. Because 444 closes the connection without sending a response, it gives automated scans the least information. If a monitoring tool or load balancer expects a response from that address, use a standard code.

My headers are on the home page but not on images. Why?

Most likely the location block for static files has its own add_header. In that case Nginx does not inherit the headers from the level above into that block; define the shared headers there as well.

Can I make these settings on shared hosting?

Usually not; the Nginx configuration is in the hands of the server administrator. You can ask your hosting provider for help, or enable similar protections from the control panel or through a CDN.

BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.

Related guides

Let us talk about your website infrastructure

BYK Yazılım builds corporate websites. Write to us with any questions about your site.

Contact us Our corporate website service