wordpress wp-login.php page restriction by ip in nginx

I am trying to restrict access to wp-login page based on IP, with the following code, I was able to restrict access to wp-admin, but login.php is still accessible:

server {
listen 80;
root /app/;
index index.php;

location = /favicon.ico {
    log_not_found off;
    access_log off;
}

location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

location / {
    try_files $uri $uri/ /index.php?$args;
}

location ~ /\. {
    deny all;
}

location ~* /(?:uploads|files)/.*\.php$ {
    deny all;
}

location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
    expires max;
    log_not_found off;
}

location ~ \.php {
    include                     fastcgi_params;
    fastcgi_pass                php:9000;
    fastcgi_index               index.php;
    fastcgi_read_timeout        10s;
    fastcgi_intercept_errors    on;
    fastcgi_param               HTTP_X_FORWARDED_FOR $http_x_real_ip;
    fastcgi_param               REMOTE_ADDR $http_x_real_ip;
    fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

   location ~ ^/(wp-admin|wp-login.php) {
        allow x.x.x.x;
        allow 172.17.0.0/16;
        deny all;
   }

}

I have a feeling that it's related to wp-login.php being a plain php file, which might require special handling and more configuration. I have also tried to put in the simplest form which didn't work either:

   location = wp-login.php {
    allow x.x.x.x;
    allow 172.17.0.0/16;
    deny all;

}

nginx logs are showing the following:

172.17.0.1 - - [21/Aug/2017:13:00:02 +0000] "GET /wp-login.php HTTP/1.1" 200 2338 "-" "Mozilla/5.0 (Linux; Android 7.1.2; Pixel Build/NJH47F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36" "94.197.xxx.xxx,
172.17.0.5"

I have also tried to at the following which block access to wp-login.php even from whtilisted ip:

   location = /wp-login.php {
        allow x.x.x.x;
        allow 172.17.0.0/16;
        deny all;
   }

172.30.3.207 - - [21/Aug/2017:13:25:08 +0000] "GET /wp-login.php HTTP/1.1" 403 572 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36" "x.x.x.x, 172.17.0.3"

and if I keep refreshing a couple of times, it will download the actual wp-login.php file.


The underlying issue you're facing is that only a single location directive will match each request to specify the parameters for request processing. Additionally, as the other answer mentions, the order of certain directives matters in nginx — all else equals, the location with the first regular expression to match gets the whole cake, so, it makes no sense to define a more specific regex location after a less-specific one at the same level.

Taking in account the revelation from Drifter104 comment that nested locations are fully supported and are a good practice as per https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/, we can then derive the following configuration with the geo map for access control:

geo $wpadmin {
    default 0;
    172.17.0.0/16 1;
}
server {
    …
    location /wp-admin {
        if ($wpadmin = 0) {
            return 403 "no wp-admin for you!\n";
        }
        try_files $uri $uri/ /index.php;
    }
    location ~ \.php$ {
        location ~ /wp-(admin/|login\.php\b) {
            if ($wpadmin = 0) {
                return 403 "no wp-admin/login for you!\n";
            }
            fastcgi_pass …
        }
        fastcgi_pass …
    }
    …
}

Do note, however, that since only a single location directive can be used to specify how to process the request, we effectively have to copy-paste all of those fastcgi_pass et al directives in two separate locations (e.g., you might want to use the include directive as per a prior suggestion), as well as implement the /wp-admin/ face control for both the static and dynamic content.


Your configuration is incorrectly nested (and indented). The part containing "wp-admin" should go before the *.php-block, as the blocks are being processed in the order specified in the documentation:

  • First, all exact string matches are being tested (e.g. location /)
  • Secondly, all matches with ^~ are tested
  • Third come regex-like matches with ~ and ~*
  • Last, the rest

This means, your two location blocks get checked in the order they are put in the config file, causing nginx to stop looking for another directive after having found the .php-directive. I think, you want the fastcqi options for wp-login.php, too. I recommend putting this in a seperate file:

myserver.conf

server {
    listen 80;
    root /app/;
    index index.php;

    # everything is fine here...
    # ...
    # ...
    location ~ ^/(wp-admin|wp-login.php) {
        include php-config.conf;
        allow x.x.x.x;
        allow 172.17.0.0/16;
        deny all;
    }

    location ~ \.php {
        include php-config.conf;
        allow all;
    } 
}

php-config.conf

include                     fastcgi_params;
fastcgi_pass                php:9000;
fastcgi_index               index.php;
fastcgi_read_timeout        10s;
fastcgi_intercept_errors    on;
fastcgi_param               HTTP_X_FORWARDED_FOR $http_x_real_ip;
fastcgi_param               REMOTE_ADDR $http_x_real_ip;
fastcgi_param               SCRIPT_FILENAME $document_root$fastcgi_script_name;

About nesting location blocks, the following is stated in the nginx documentation:

While nginx’s configuration parser is technically capable of reading nested location blocks, this is neither recommended nor supported.