Nginx’s map and geo modules let you route, block, or rate-limit traffic based on IP address, country code, or request header — all evaluated before PHP is invoked, making them far more efficient than WordPress-level blocking.
Problem: A WordPress VPS is exposed to IP-based scrapers, country-specific bots, and targeted admin brute-force attempts that basic fail2ban rules do not block early enough — the requests reach PHP before being rejected.
Solution: Use Nginx's map module to build IP and CIDR blocklists that are evaluated before any PHP execution, and the geo module to restrict admin URLs to known office IP ranges. Both operate at the Nginx layer, returning 444 (no response) or 403 before PHP-FPM is invoked.
The examples below use geo to block an IP blocklist, map to whitelist admin access by country, and combine both to protect wp-login.php and the REST API.
# /etc/nginx/conf.d/geo-block.conf
# 1. Block specific IPs — geo module runs at nginx startup (zero runtime cost)
geo $blocked_ip {
default 0;
192.0.2.1 1; # known bad actor
198.51.100.0/24 1; # entire subnet
203.0.113.42 1;
}
# 2. Allow wp-login.php only from specific countries (using GeoIP2 + ngx_http_geoip2_module)
# Requires: apt install libnginx-mod-http-geoip2 + MaxMind GeoLite2 databases
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
$geoip2_country_code country iso_code;
}
map $geoip2_country_code $admin_allowed {
default 0;
US 1;
CA 1;
GB 1;
UA 1;
}
Apply the variables in the server block:
# /etc/nginx/sites-available/helloadmin.com
server {
listen 443 ssl http2;
server_name helloadmin.com;
root /var/www/html;
# Block known bad IPs for all requests
if ( $blocked_ip ) {
return 444; # 444 = Nginx closes connection silently
}
# Protect wp-login.php by country
location = /wp-login.php {
if ( $admin_allowed = 0 ) {
return 403;
}
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Protect wp-admin by country
location /wp-admin/ {
if ( $admin_allowed = 0 ) {
return 403;
}
try_files $uri $uri/ /index.php?$args;
}
# Rate-limit the REST API
limit_req_zone $binary_remote_addr zone=rest_api:10m rate=30r/m;
location /wp-json/ {
limit_req zone=rest_api burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
}
NOTE: Always reload Nginx with nginx -t && systemctl reload nginx after configuration changes — never restart, as a reload is graceful and zero-downtime. Test the geo block with curl -H "X-Forwarded-For: 192.0.2.1" https://yoursite.com/wp-login.php to verify it returns 403 or 444.