Search code examples
nginxdnslimit

How to limit request per virtual host on Nginx?


I have an NGINX server serving a web application where customers have their domains (CNAMES) that they access their websites.

Does NGINX have any way to limit the number of accesses to one of those domains for a period of time?

Example:

Restriction that I need: 2000 requests / domain / minute

So, in one specific period of time...

www.websiteA.com.br --- 1456 requests / minute OK!

www.websiteB.com.br --- 1822 requests / minute OK!

www.websiteC.com.br --- 2001 requests / minute LOCKED TEMPORARILY

Does anyone know how to make such a restriction?


Solution

  • I am using the rate-limiting features of nginx to define a number of rate limit rules and then apply these rules to particular hosts.
    There are a few hacks in here (if-statements are used as well as redirect via error_page handlers) - I have not found a better way of doing it yet. Please comment if you have better alternatives.

    nginx.conf

    http {
    
        # ...
    
        # Define rate-limit key to use
        map $http_authorization $rl_zone_key{
            default $binary_remote_addr;
            '~*.*'  $http_authorization;
        }
    
        # Define Rate Limits
        limit_req_zone $rl_zone_key zone=rateLimit_Standard:10m rate=1r/s;
        limit_req_zone $rl_zone_key zone=rateLimit_Class_A:10m rate=5r/s;
        limit_req_zone $rl_zone_key zone=rateLimit_Class_B:10m rate=10r/s;
        limit_req_zone $rl_zone_key zone=rateLimit_Class_C:10m rate=100r/s;
    
        # Define the rate Limit Category applied to the particular host
        map $http_host $apply_rate_limit{
            hostnames;
            default             rateLimit_Standard;
            example.com         rateLimit_Class_B;
            *.example.com       rateLimit_Class_A;
        }
    
        #upstream server definition for php fast-cgi using port 9000
        upstream phpfcgi {
            server 127.0.0.1:9000;
        }
    
        # ...
    
    }
    

    map $http_authorization $rl_zone_key

    The key that is used to compare whether two requests are coming from the same place, uses the $binary_remote_addr nginx variable unless an $http_authorization header exists, in which case it will use that. This means that until a user is authenticated (using digest auth), rate limits are applied by ip - thereafter it is applied by logged-in user session.

    limit_req_zone

    Here I set the rate limit caches.

    • Standard : 10MB cache, allows 1 request per second
    • Class A : 10MB cache, allows 5 requests per second
    • Class B : 10MB cache, allows 10 requests per second
    • Class C : 10MB cache, allows 100 requests per second

    map $http_host $apply_rate_limit

    With the map directive, I check the hostname in $http_host to determine the target domain and then define $apply_rate_limit with the name of the rate limit class I want to apply.

    example.com.conf

    server {
        listen              80;
        server_name         example.com;
        root                /var/www/example.com;
    
        location / {
            index               index.php;
            try_files           $uri =404;
    
            location ~ ^/index\.php(/|$) {
                error_page               420 =200 @rateLimit_Standard;
                error_page               421 =200 @rateLimit_Class_A;
                error_page               422 =200 @rateLimit_Class_B;
                error_page               423 =200 @rateLimit_Class_C;
    
                if ( $apply_rate_limit = 'rateLimit_Standard' ) {return 420;}
                if ( $apply_rate_limit = 'rateLimit_Class_A' ) {return 421;}
                if ( $apply_rate_limit = 'rateLimit_Class_B' ) {return 422;}
                if ( $apply_rate_limit = 'rateLimit_Class_C' ) {return 423;}
            }
    
        }
    
        location @rateLimit_Standard {
            limit_req                   zone=rateLimit_Standard burst=5;
            add_header                  X-Rate-Limit-Class $apply_rate_limit;
    
            include                     fastcgi_params;
            fastcgi_index               index.php;
            fastcgi_split_path_info     ^(.+\.php)(/.*)$;
            fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param               HTTPS off;
            fastcgi_pass                phpfcgi;
    
        }
    
        location @rateLimit_Class_A {
            limit_req                   zone=rateLimit_Class_A burst=10 nodelay;
            add_header                  X-Rate-Limit-Class $apply_rate_limit;
    
            include                     fastcgi_params;
            fastcgi_index               index.php;
            fastcgi_split_path_info     ^(.+\.php)(/.*)$;
            fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param               HTTPS off;
            fastcgi_pass                phpfcgi;
        }
    
        location @rateLimit_Class_B {
            limit_req                   zone=rateLimit_Class_B burst=100 nodelay;
            add_header                  X-Rate-Limit-Class $apply_rate_limit;
    
            include                     fastcgi_params;
            fastcgi_index               index.php;
            fastcgi_split_path_info     ^(.+\.php)(/.*)$;
            fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param               HTTPS off;
            fastcgi_pass                phpfcgi;
        }
    
        location @rateLimit_Class_C {
            limit_req                   zone=rateLimit_Class_C burst=1000;
            add_header                  X-Rate-Limit-Class $apply_rate_limit;
    
            include                     external_definition_of_fastcgi_parameters.conf
        }
    
    }
    

    In my example vhost file, I use the rate limit classes previously defined. This could be done in a more straightforward manner but in our scenario we have a single vhost file serving multiple domains, so defining which domain uses which rate limit class is better done outside the vhost file.
    I will provide a simplified example vhost file at the end that removes this abstraction and makes the vhost rate limit application more manual.

    error_page 420 =200 @rateLimit_Standard

    In nginx there is no convenient way to redirect to a named location outside of the try_files directive. A hack to get this done is to define an error handler (for an error code not in common use) and then provide the named location as the error_page handler.
    In this particular line, we are saying that when error code 420 is triggered, the error handler should redirect to the @rateLimit_Standard location and reset the HTTP code to 200.

    if ( $apply_rate_limit = 'rateLimit_Standard' ) {return 420;}

    In our nginx.conf file, $apply_rate_limit was defined based on the $http_host header. If it was set to rateLimit_Standard, this if statement will complete by returning from the http request with error code 420. This will be caught by the error_page handler we defined earlier and will be redirected to the named location @rateLimit_Standard

    location @rateLimit_Standard

    Once the request has been rerouted to @rateLimit_Standard, it will apply the rate limit and set the burst value: limit_req zone=rateLimit_Standard burst=5; It will then proceed to process the php request as per normal.
    For good measure I also add a header to track the applied rate limit: add_headerX-Rate-Limit-Class $apply_rate_limit;

    include external_definition_of_fastcgi_parameters.conf

    You will notice for each of the named locations, the fcgi headers included are identical. There is no formal inheritance in nginx config blocks, so this has to be repeated for each location in order forward this to php-fpm.
    One can, however, define the common properties in an external file, and use the include statement above to fetch all those duplicate config options from a single external file. I left this here because this is what you really should do.

    Simplified example vhost

    The above example.conf file shows fully how I abstracted the host <-> rate limit class pairing as we are using it in our environment.
    It can be a lot simpler if you are only using a single vhost entry for a single domain:

    simple-example.com.conf

    server {
        listen              80;
        server_name         simple-example.com;
        root                /var/www/simple-example.com;
    
        location / {
            index               index.php;
            try_files           $uri =404;
    
            location ~ ^/index\.php(/|$) {
                limit_req                   zone=rateLimit_Class_A burst=10 nodelay;
                add_header                  X-Rate-Limit-Class rateLimit_Class_A;
    
                include                     fastcgi_params;
                fastcgi_index               index.php;
                fastcgi_split_path_info     ^(.+\.php)(/.*)$;
                fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param               HTTPS off;
                fastcgi_pass                phpfcgi;
            }
    
        }
    
    }
    

    Disclaimer: All the code above was constructed from my own nginx config for the purpose of creating an example. This will probably not work out of the box and you will need to massage these snippets into your environment to get them working properly.

    Please vote if you found this useful.