IP allowlist playbook
IP allowlist for nginx and wp-admin
Lock admin panels, dashboards and WordPress wp-admin to your team’s IPs with nginx, also called an IP whitelist.
An admin panel behind nginx can drop everyone who isn’t on your list before they ever see a login page. It takes four lines, and two mistakes undo it: trusting the wrong client address behind a proxy, and a location block that never runs.
Vendor facts checked on .
What this control covers
- Any HTTP path nginx serves: an internal dashboard, a back office, an MCP server,
/wp-admin/and/wp-login.php. - IPv4 and IPv6 addresses and CIDR ranges.
What it doesn’t cover
- Requests that reach the app without passing through this nginx, for example straight to the app’s port or to the origin behind a CDN. Firewall those too.
- The application’s own login: keep SSO or strong passwords with MFA behind the allowlist.
The plan you need
Any nginx: allow and deny come from the access module, which is built in. Reading the real client IP behind a proxy needs the realip module, which nginx’s own docs say is not built by default; most distribution packages include it, and nginx -V lists it as --with-http_realip_module. nginx: ngx_http_access_module.
Why an allowlist needs fixed IPs
An IP allowlist is only as good as the addresses on it. Home and mobile IPs change without notice, so a list of your team’s own addresses locks people out every few weeks, and someone ends up adding a broad range or switching the control off.
A shared VPN looks like a fix, but its exit IP belongs to every customer of that VPN server. Allowlisting it lets them in too.
What works is a small set of addresses that belong to your team alone and don’t change: your team’s traffic to the service leaves from them, and nothing else does.
Set it up
Allow your IPs on the admin location
nginx checks
allowanddenyin order and stops at the first match, so the addresses come first anddeny alllast. A refused request gets a 403.nginx.conf location /admin/ { allow 203.0.113.10; allow 203.0.113.11; deny all; proxy_pass http://127.0.0.1:8080; }For WordPress, lock wp-login.php and wp-admin, but not admin-ajax.php
Front-end plugins call
/wp-admin/admin-ajax.phpfor logged-out visitors, and logged-out forms post to/wp-admin/admin-post.php; WordPress warns that blocking all of/wp-admincan break them. Exact-match locations take priority, so they stay open. The^~is what makes it work: without it, the site’s usuallocation ~ \.php$catches/wp-admin/*.phpfirst and the allow rules never run.wordpress.conf location = /wp-admin/admin-ajax.php { include fastcgi.conf; fastcgi_pass php; } location = /wp-admin/admin-post.php { include fastcgi.conf; fastcgi_pass php; } location = /wp-login.php { allow 203.0.113.10; allow 203.0.113.11; deny all; include fastcgi.conf; fastcgi_pass php; } location ^~ /wp-admin/ { allow 203.0.113.10; allow 203.0.113.11; deny all; location ~ \.php$ { include fastcgi.conf; fastcgi_pass php; } } location = /xmlrpc.php { deny all; }Decide about xmlrpc.php
Blocking
/xmlrpc.phpstops a common password-guessing route, but it also breaks the WordPress mobile app, Jetpack and some single sign-on plugins. Keep the block unless you use one of them.Behind a load balancer or Cloudflare, restore the real client IP
Behind a proxy, nginx sees the proxy’s address, so allowing it lets everyone in. Trust only the proxy’s own ranges with
set_real_ip_from, and read the client address from its header; never trust0.0.0.0/0, or anyone can send the header themselves. Cloudflare publishes its ranges at cloudflare.com/ips and says the list needs updating regularly, so refresh it from a script.nginx.conf, http block # One line per range from https://www.cloudflare.com/ips-v4/ and /ips-v6/ set_real_ip_from 173.245.48.0/20; real_ip_header CF-Connecting-IP;Close the way around the proxy
If the origin also answers on its public IP, anyone can skip Cloudflare and the header with it. Let the origin’s firewall accept web traffic only from the proxy’s ranges.
Test and reload
Run
nginx -t, thennginx -s reload.
Check that it works
From outside, expect 403
From a network that isn’t on the list, the admin path should answer 403.
shell curl -s -o /dev/null -w '%{http_code}\n' https://example.com/wp-login.php # 403Through your fixed IPs, expect the login page
Run the same command from a device whose traffic to the site leaves from an allowed IP: it should print 200.
Check the public endpoints still work
A request to
/wp-admin/admin-ajax.phpfrom outside must not get 403; WordPress usually answers 400 to an empty request, which is fine. Then click through any front-end form or plugin that loads content on the page.
Where the fixed IPs come from
Teicho sends your team’s traffic to the service through a pair of Gates dedicated to your team, each with a fixed IP. Allowlist both, and the service sees only your people and machines, each signed in as themselves, while everyday browsing still goes straight to the internet. Teicho is in early access.
FAQ
Questions
Is an nginx allowlist the same as an IP whitelist?
Yes. `allow` and `deny` in nginx are what people mean by whitelisting an IP: listed addresses get in, everyone else is refused.
Should I return 444 instead of 403?
`return 444` closes the connection without any response, which tells a scanner less. `deny` always answers 403, so for 444 you need `return 444` in a location of its own, as for xmlrpc.php. Either way the request never reaches the app.
Does nginx allow IPv6 addresses?
Yes. `allow` takes IPv6 addresses and CIDR ranges such as `2001:db8::/32`. If your team’s IPs include IPv6, list them too, or a device that prefers IPv6 is refused.
What about a long list of addresses?
nginx’s docs suggest the geo module for many addresses. With fixed team IPs the list stays at two lines, which is the point.
Our CI deploys through the admin API. Will it be blocked?
Hosted CI runners change addresses on every run. Send their traffic through your team’s fixed IPs, or deploy through a path that doesn’t need the admin location.
Sources
- nginx: ngx_http_access_module
- nginx: ngx_http_realip_module
- WordPress: brute force attacks
- WordPress: AJAX in plugins
- Cloudflare: restoring original visitor IPs
Plans, limits and steps on this page were checked against the vendor’s documentation on 2 November 2026. Vendors change them; if something here is out of date, tell us at hello@teicho.io.
Put a wall around what matters.
Teicho is in early access. Tell us what you want to protect and we’ll be in touch.