36

I'm able to use limit_req to rate-limit all requests to my server.

However I'd like to remove the rate restriction for certain IP addresses (i.e. whitelist) and use a different rate restriction for certain others (i.e. certain IPs I'd like as low as 1r/s).

I tried using conditionals (e.g. if ( $remote_addr = "1.2.3.4" ) {}) but that seems to work only with rewrite rules, not for rate-limit rules.

Kyle Brandt
  • 82,107
  • 71
  • 302
  • 444
Jason Cohen
  • 1,067
  • 3
  • 14
  • 21

2 Answers2

45

It is really better to avoid using the "if" directive. When the key in limit_req_zone (and limit_conn_zone) is empty the limits are not applied. You can use this in conjunction with the map and geo modules to create a whitelist of IPs where the throttle limits are not applied.

This example shows how to configure a limit for both concurrent requests and request rate from a single IP.

http {
    geo $whitelist {
       default 0;
       # CIDR in the list below are not limited
       1.2.3.0/24 1;
       9.10.11.12/32 1;
       127.0.0.1/32 1;
    }

    map $whitelist $limit {
        0     $binary_remote_addr;
        1     "";
    }

    # The directives below limit concurrent connections from a 
    # non-whitelisted IP address to five

    limit_conn_zone      $limit    zone=connlimit:10m;

    limit_conn           connlimit 5;
    limit_conn_log_level warn;   # logging level when threshold exceeded
    limit_conn_status    503;    # the error code to return

    # The code below limits the number requests from a non-whitelisted IP
    # to one every two seconds with up to 3 requests per IP delayed 
    # until the average time between responses reaches the threshold. 
    # Further requests over and above this limit will result 
    # in an immediate 503 error.

    limit_req_zone       $limit   zone=one:10m  rate=30r/m;

    limit_req            zone=one burst=3;
    limit_req_log_level  warn;
    limit_req_status     503;

The zone directives must be placed at the http level, however the other directives can be placed further down, e.g. at the server or the location level to limit their scope or further tailor the limits.

For futher information refer to the Nginx documentation ngx_http_limit_req_module and ngx_http_limit_conn_module

shonky linux user
  • 1,031
  • 10
  • 13
  • What's the difference between these 2 modules? – mente Jan 15 '16 at 15:09
  • 1
    As per the comments, the first limits concurrent connections, the second limits the rate of connections – shonky linux user Jan 17 '16 at 23:03
  • Can you explain why you do the mapping in two stages, with `geo` followed by `map`, rather than just use `geo` to set `$limit` directly? – Marcus Downing Apr 08 '16 at 12:04
  • 2
    It seems `geo` cannot map to a variable so if you specify `$binary_remote_addr` as a mapping value this will translate to the literal string `"$binary_remote_addr"`, not the value of the variable. – ColinM May 10 '16 at 23:15
  • 1
    I would like to add that if the IP in question is already in the zone, you _must_ restart nginx; a reload is not enough. – Halfgaar Oct 24 '18 at 13:48
  • Explanation: 1) geo creates variables with values depending on the client IP address 2) map creates variables with values depending on other variables. So if the client IP is in the list $whitelist = 1, therefore $limit = "". If the client IP is not in the list $whitelist = 0, therefore $limit = the clients IP and so this IP will be limited – andymel Oct 20 '20 at 22:15
5

You can safely use named locations, such as "@location" in an if() block.

See: http://wiki.nginx.org/IfIsEvil

Something like this should work:

http {

   limit_req_zone $binary_remote_addr zone=delay:10m rate=1r/m;

   server {
      ...

      error_page 410 = @slowdown;

      if( $remote_addr != "1.2.3.4" ) {
         return 410;
      }

      location @slowdown {
         limit_req zone=delay burst 5;
         ...
      }

      location / {
         ...
      }
   }

Fill in "location @slowdown { }" with the same information as "location / { }, such as proxy_pass if you're using nginx as a reverse proxy.

Robert Suh
  • 51
  • 1
  • 3
  • 1
    I'm not sure I understand the 410 part? Do the client actually see a http 410 status code? – svrist Feb 27 '12 at 14:35
  • 2
    Wow, this actually works! Very nifty `error_page` trick, +1! @svrist, see https://serverfault.com/a/870170/110020 for complete explanation of how something like this would work, and why. – cnst Aug 24 '17 at 00:11
  • @svrist I think the 410 is just one of the status codes the Robert chose to use as a flag to jump to the `@slowdown` block. – run_the_race Jan 20 '22 at 20:50