Nginx can accept or refuse a request based on the visitor's IP address before it touches PHP or your app. That makes it a cheap, fast way to keep an admin panel private or shut out a scraper that will not stop. This guide covers the allow and deny rules, how they are evaluated, how to keep long lists tidy, and what to send back to blocked visitors.
Before you start
- A Linux VPS with Nginx serving your site.
- Your own public IP address. Search "what is my IP" in a browser, or check the access log after visiting the site.
- If the site is behind Cloudflare or another proxy, set up real visitor IPs first, or Nginx will see only the proxy. See Show real visitor IPs in Nginx behind Cloudflare.
- SSH access with sudo.
How allow and deny work
- Rules can go in the
http,serverorlocationcontext. - Nginx checks them in order and stops at the first match.
- An IP that matches nothing is allowed.
- Rules in a
locationreplace rules inherited from theserverblock; they are not merged.
So the order matters. This allows one IP and blocks everyone else:
allow 198.51.100.20;
deny all;Reversed, deny all would match first and block you too.
Restrict an admin path to your IP
Put the rules in the location for the admin area:
location ^~ /admin/ {
allow 198.51.100.20;
allow 203.0.113.0/24;
deny all;
try_files $uri $uri/ /index.php?$args;
}CIDR ranges such as 203.0.113.0/24 cover a whole block of addresses. IPv6 works the same way: allow 2001:db8::/48;.
^~ makes sure this prefix location wins over regex locations such as your PHP handler. If the protected area contains PHP files, nest the PHP block inside this location so they are protected too. For WordPress:
location = /wp-login.php {
allow 198.51.100.20;
deny all;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}Copy the PHP lines from your own config; socket paths differ between systems.
Warning: If your home or mobile IP changes, you lock yourself out of the admin area. The site itself stays up and you can always fix the rule over SSH, but consider password protecting the folder with satisfy any as a fallback.
Block abusive IPs
To block a few addresses across a whole site, add deny lines to the server block:
server {
server_name example.com;
deny 192.0.2.15;
deny 192.0.2.0/24;
}Keep your existing directives; only add the deny lines. Everyone not listed is still allowed.
Keep long lists in include files
Lists grow. Put them in their own file so you can edit them without touching site configs:
sudo nano /etc/nginx/snippets/blocklist.confdeny 192.0.2.15;
deny 192.0.2.0/24;
deny 2001:db8:bad::/48;Then include it in each site's server block:
include /etc/nginx/snippets/blocklist.conf;On AlmaLinux and Rocky Linux, create the snippets folder first with sudo mkdir -p /etc/nginx/snippets, or keep the file directly in /etc/nginx/. Do not put it in conf.d, because files there are loaded at the http level automatically.
For very long lists (thousands of entries), a geo block is faster than many deny lines. In the http context:
geo $blocked_ip {
default 0;
include /etc/nginx/snippets/blocked-geo.conf;
}Where each line of blocked-geo.conf looks like 192.0.2.0/24 1;. Then in the server block:
if ($blocked_ip) {
return 403;
}403 or 444
denyreturns 403 Forbidden. The visitor sees an error page, so it is clear what happened. Use this for admin areas, where a real person may hit it.return 444is special to Nginx: it closes the connection without sending any response. Bots get nothing back and waste no bandwidth. Use it in thegeoexample above for known abusers.
You can also show a friendlier message for 403s:
error_page 403 /403.html;
location = /403.html {
root /var/www/errors;
allow all;
internal;
}allow all inside this location ensures blocked visitors can still see the error page.
Apply the change
sudo nginx -t && sudo systemctl reload nginxCheck it works
From an address that should be blocked, or from the server itself if 127.0.0.1 is not allowed:
curl -I https://example.com/admin/Expected:
HTTP/2 403From your allowed IP you should get 200 or a normal redirect. Blocked requests are also logged in the error log:
sudo grep "access forbidden by rule" /var/log/nginx/error.log | tailTroubleshooting
- You are blocked from your own admin area: your IP changed or you are behind a VPN. Check which IP Nginx logs for you and update the rule.
- Blocking does nothing behind Cloudflare: Nginx sees Cloudflare's IPs. Configure the real IP module, or block at Cloudflare instead.
- PHP files in the admin folder are not protected: the PHP regex location matched first. Use
^~and nest the PHP block. - Rules from the server block stop working in one location: that location has its own
allow/deny, which replaces the inherited ones. Repeat the rules or include the snippet there. - The abuser keeps coming back from new IPs: static lists cannot keep up. Automate it with Fail2ban jails for Nginx or block at the firewall.
Next steps
- Slow down abuse that is not worth a ban: Rate limit requests in Nginx.
- Block at the firewall instead of the web server: Block an IP or subnet with UFW.
