3

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.

  • 1
    Why is your admin block nested within the "location /" block? Move it outside that block and see if it helps. I suspect it's not being triggered because your admin page is being generated by the PHP block, and each request is handled by one block only. – Tim Aug 15 '17 at 18:51
  • I have tried this. It didn't work! – Mahmoud Khateeb Aug 15 '17 at 20:25
  • 1
    Please try to be more precise. Also, demonstrate the problem, for example with a curl ideally from IPs that should and shouldn't trigger the rule, and include relevant logs. The location blocks generally shouldn't be nested without good reason, so leave them out, and suggest you edit your question to update your config above. You need to restart Nginx / reload the config to apply changes. – Tim Aug 15 '17 at 20:29
  • 1
    The above is an enough explanation of the issue. I know how to test it, and I know that I need to reolad nginx. I don't see how adding what you're requesting is gonna help identifying the issue. Thanks though! – Mahmoud Khateeb Aug 15 '17 at 20:34
  • 1
    The allow / deny statements look correct, so I'm trying to help you problem solve to work out what the real problem is. Location block nesting was my first idea, and nothing else comes to mind immediately. That's why I asked for additional information, even if not 100% relevant it might help you or someone else here come up with an idea. For now I suggest you go back to basics - create a server with a single location serving a static page. Add your allow / block statements and check it works. Then expand it slowly to what you need, testing after each change. – Tim Aug 15 '17 at 20:57
4
+50

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.

  • 172.30.3.207 - - [22/Aug/2017:10:53:38 +0000] "GET /wp-login.php HTTP/1.1" 403 21 "-" "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" "<the whitlisted ip>, 172.17.0.3" – Mahmoud Khateeb Aug 22 '17 at 10:55
  • I have discovered the issue, nginx is running behind trafiek so it's blocking access based on trafiek (remote_addr) rather than my actual ip (the first part of http_x_forwarded_for). I am not sure if there is a module similar to geo that would allow me to run the above based on the real ip. – Mahmoud Khateeb Aug 22 '17 at 11:36
  • @MahmoudKhateeb, can you elaborate on your new spec? Is it contained in a single variable as a whole, or in a part of a single variable? (E.g., why does your log seemingly indicate 3 different IPv4 addresses?) Likewise, do you want to block based on regex or subnet specification? Basically, the idea is still the same, and geo accepts an optional parameter where you specify the IPv4 address. – cnst Aug 22 '17 at 22:08
  • @MahmoudKhateeb, BTW, nginx.org/en/docs/http/ngx_http_realip_module.html might help you. – cnst Aug 26 '17 at 0:56
  • the first block for location /wp-admin is unnecessary, as the second block catches everything. – jeffmcneill Dec 22 '18 at 9:29
1

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.

  • now it's working intermittently. I am getting the weirdest behaviour ever. It gives me 403 on both devices (the one should have access and the one shouldn't), upon refreshing multiple times, it works fine (sometimes with no style). – Mahmoud Khateeb Aug 21 '17 at 16:10
  • 2
    "While nginx’s configuration parser is technically capable of reading nested location blocks, this is neither recommended nor supported." not true, nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls – Drifter104 Aug 21 '17 at 16:20
  • I would guess that /wp-admin might contain stuff other than php files, which would then result issues with this configuration. – cnst Aug 21 '17 at 23:39
  • I have discovered the issue, nginx is running behind trafiek so it's blocking access based on trafiek (remote_addr) rather than my actual ip (the first part of http_x_forwarded_for). – Mahmoud Khateeb Aug 22 '17 at 11:19

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.