Search code examples
nginxwebserverurl-rewritingrate

nginx limit_req does not work


I'm trying to implement a simple rate limiting system with nginx (v1.6.2)

sites-available/mysite.com:

limit_req_zone $binary_remote_addr zone=myzone:10m rate=2r/m;

server {
    listen                      80;
    server_name                 mysite.com;
    root                        /var/www/vhosts/mysite.com;
    error_log                   [..];
    access_log                  [..];

    include                     conf.d/php-fpm.conf;

    location = / {
        limit_req               zone=myzone burst=3 nodelay;
        index                   index.html;
    }

    location / {
        try_files               $uri =404;
    }

    location ^~ /pages {
        include                 conf.d/php-fpm.conf;
        internal;
    }

    location = /email {
        rewrite ^(.*)$          /pages/email.html;
    }

    location = /email/subscribe {
        limit_req               zone=myzone burst=2 nodelay;
        rewrite ^(.*)$          /pages/email.php?action=subscribe;
    }

    location ~ /api {
       limit_req                zone=myzone burst=5 nodelay;
       rewrite ^(.*)$           /pages/api.php;
    }
}

conf.d/php-fpm.conf:

location ~ \.php$ {
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }
    fastcgi_pass                unix:/var/run/php5-fpm.sock;
    fastcgi_index               index.php;
    fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_split_path_info     ^(.+?\.php)(/.*)$;
    fastcgi_param               PATH_INFO $fastcgi_path_info;
    include                     fastcgi_params;
}

nginx.conf: Nothing interesting, only
include sites-enabled/*;

Rate limiting / works fine. I get an error 503 if I do too many requests to this page.
The problem: Neither /email/subscribe, /api nor /api/test is rate limited, and I don't know why. It must have to do something with rewrite, but what's the problem?
Any ideas? I tried everything!

Please note: I've changed filenames and URL endpoints.


Solution

  • The problem is that nginx process request in several phases and rewrite phase goes before preaccess one (this is where limit_req is applied). So in you config requests are rewritten to /pages/... before they had a chance to be limited. To avoid this you either should stay in the same location block after rewrite (using break flag) or a little hack with try_files.

    I prefer first option, so your config could look like this:

    limit_req_zone $binary_remote_addr zone=myzone:10m rate=2r/m;
    
    server {
        listen                      80;
        server_name                 mysite.com;
        root                        /var/www/vhosts/mysite.com;
        error_log                   [..];
        access_log                  [..];
    
        include                     conf.d/php-fpm.conf;
    
        location = / {
            limit_req               zone=myzone burst=3 nodelay;
            index                   index.html;
        }
    
        location / {
            try_files               $uri =404;
        }
    
        location ^~ /pages {
            include                 conf.d/php-fpm.conf;
            internal;
        }
    
        location = /email {
            rewrite ^(.*)$          /pages/email.html;
        }
    
        location = /email/subscribe {
            limit_req               zone=myzone burst=2 nodelay;
            rewrite ^(.*)$          /pages/email.php?action=subscribe break;
            fastcgi_pass            unix:/var/run/php5-fpm.sock;
            fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include                 fastcgi_params;
        }
    
        location ~ /api {
           limit_req                zone=myzone burst=5 nodelay;
           rewrite ^(.*)$           /pages/api.php break;
           fastcgi_pass             unix:/var/run/php5-fpm.sock;
           fastcgi_param            SCRIPT_FILENAME $document_root$fastcgi_script_name;
           include                  fastcgi_params;
        }
    }
    

    If you go with second option, your config will be a little cleaner, but a bit hacky. We will use the fact that try_files phase runs after limit_req phase and that try_files makes internal redirect to it's last argument.

    limit_req_zone $binary_remote_addr zone=myzone:10m rate=2r/m;
    
    server {
        listen                      80;
        server_name                 mysite.com;
        root                        /var/www/vhosts/mysite.com;
        error_log                   [..];
        access_log                  [..];
    
        include                     conf.d/php-fpm.conf;
    
        location = / {
            limit_req               zone=myzone burst=3 nodelay;
            index                   index.html;
        }
    
        location / {
            try_files               $uri =404;
        }
    
        location ^~ /pages {
            include                 conf.d/php-fpm.conf;
            internal;
        }
    
        location = /email {
            rewrite ^(.*)$          /pages/email.html;
        }
    
        location = /email/subscribe {
            limit_req               zone=myzone burst=2 nodelay;
            try_files SOME_NONEXISTENT_FILE /pages/email.php?action=subscribe;
        }
    
        location ~ /api {
           limit_req                zone=myzone burst=5 nodelay;
           try_files SOME_NONEXISTENT_FILE /pages/api.php;
        }
    }