2026年11月12日

2026年11月12日

Restrict WordPress Admin Access by IP: Apache 2.4 and Nginx Setup

This article is a localized edition of the Japanese original. Read the Japanese original

By default, anyone can open the WordPress admin area (/wp-admin/) and login screen (/wp-login.php). If the people who manage the site always connect from known addresses, you can use the web server configuration to allow only those IP addresses. Password brute-forcing and requests aimed at the admin area are then stopped before they ever reach WordPress.

This article covers, in order:

  • Restricting access with .htaccess on Apache 2.4
  • Restricting access on Nginx
  • Files to exclude from the restriction (such as admin-ajax.php) and why
  • Caveats about IPv6, CDNs and reverse proxies, and connections without a static IP
  • Testing the setup, and getting back in if you lock yourself out

Before you start

Your IP address: find the IP address of the connection you normally use to open the admin area. You may be connecting over IPv6, so check both.

curl -4 https://ifconfig.me   # IPv4 address
curl -6 https://ifconfig.me   # IPv6 address (errors on connections without IPv6)
  • Whether it is a static IP: home connections often change IP address, and you would have to update the configuration every time it changes. If you do not have a static IP, read "Without a static IP" below first.
  • A way back in: a mistake in the configuration can lock you out of the admin area too. Make sure you can edit .htaccess over FTP, SFTP or SSH, or with your hosting provider's file manager.

Method 1: Restrict with .htaccess on Apache 2.4

Apache 2.4 uses Require for access control. The Order deny,allow / Deny from all / Allow from lines common in older articles are Apache 2.2 syntax and do not work in 2.4 without the compatibility module (mod_access_compat).

Restrict /wp-admin/

Create an .htaccess file in the wp-admin directory (WordPress core normally has no .htaccess in wp-admin). Replace the IP addresses with your own.

# /wp-admin/.htaccess
# Allow access only from these IP addresses (multiple Require ip lines: matching any one is enough)
Require ip 203.0.113.10
Require ip 198.51.100.0/24
Require ip 2001:db8:1234::/48

# Files called by public-facing features stay open to everyone
<FilesMatch "^(admin-ajax|admin-post)\.php$">
    Require all granted
</FilesMatch>
  • admin-ajax.php may be called from public pages by themes and plugins (infinite scroll, form submissions, like buttons and so on). Restricting it breaks those features, so exclude it.
  • admin-post.php is sometimes used by plugins as the target of forms on public pages. If you do not use it, you do not need to exclude it, but even when excluded, WordPress still checks login status and permissions.
  • To use Require in .htaccess, the server configuration must allow AuthConfig (or All) in AllowOverride. Otherwise, Require in .htaccess is either ignored or causes a 500 error.

Restrict wp-login.php as well

Restricting only /wp-admin/ leaves the login screen, wp-login.php, open to anyone because it lives in the site root, so brute-force attempts still get through. If the goal is to protect the admin area, limit wp-login.php to the same IP addresses.

Add this to the .htaccess in the WordPress root. WordPress rewrites everything between # BEGIN WordPress and # END WordPress when permalinks are saved, so put it outside that block (for example, above # BEGIN WordPress).

# .htaccess in the WordPress root (above # BEGIN WordPress)
<Files "wp-login.php">
    Require ip 203.0.113.10
    Require ip 198.51.100.0/24
    Require ip 2001:db8:1234::/48
</Files>

Once wp-login.php is restricted, the following also stop working from any IP address you have not allowed. Decide based on how your site is used.

  • Entering the password for password-protected posts (the form submits to wp-login.php?action=postpass)
  • User registration and password resets (on sites that accept member sign-ups)

xmlrpc.php is also used for login attempts from outside. If you do not use it, see "How to disable XML-RPC in WordPress" to turn it off.

Method 2: Restrict on Nginx

On Nginx, you put allow and deny in a location. The catch is that giving a prefix match priority, as in location ^~ /wp-admin/, stops the location ~ \.php$ regex from being used. As is, PHP files in wp-admin would not be passed to PHP-FPM, so nest the PHP handling inside the location.

# Put this inside the server block. Adjust fastcgi_pass to your environment
# File called by public-facing features (exact matches take top priority)
location = /wp-admin/admin-ajax.php {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

# Admin area
location ^~ /wp-admin/ {
    allow 203.0.113.10;
    allow 198.51.100.0/24;
    allow 2001:db8:1234::/48;
    deny all;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

# Login screen
location = /wp-login.php {
    allow 203.0.113.10;
    allow 198.51.100.0/24;
    allow 2001:db8:1234::/48;
    deny all;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
  • allow and deny are evaluated from the top, and the first match wins. Put deny all; last.
  • The nested location ~ \.php$ inherits the outer allow / deny.
  • To exclude admin-post.php as well, add a location = /wp-admin/admin-post.php block in the same form as admin-ajax.php.
  • Match the fastcgi_pass socket path to your existing location ~ \.php$ configuration. If you use snippets/fastcgi-php.conf on Debian or Ubuntu, you can include that instead.

After editing, check the syntax and then reload.

sudo nginx -t && sudo systemctl reload nginx

Basic Nginx configuration is also covered in "Combining Basic authentication and IP restrictions on Nginx".

Things to watch out for

If you connect over IPv6

If your connection supports IPv6, the browser may connect over IPv6. If you allow only your IPv4 address, you will get a 403 yourself. If curl -6 shows an IPv6 address, add that IPv6 address (or the range your provider assigned you) to the allowed list.

If you use a CDN or reverse proxy

With a CDN such as Cloudflare or a reverse proxy in front, the web server sees the CDN's or proxy's IP address as the client. IP restrictions then do not work as intended. First configure the server to restore the real IP address from X-Forwarded-For or similar headers sent by trusted proxies (mod_remoteip on Apache, real_ip_header and set_real_ip_from on Nginx), and then add the restriction.

Requests from the server itself

WordPress sends HTTP requests to its own server (loopback requests), for example for Site Health checks. If, after adding the restriction, Tools > Site Health reports a loopback request error, add the server's own IP addresses (127.0.0.1 and the server's public IP address) to the allowed list.

wp-cron.php and the REST API

wp-cron.php is in the site root and is not affected by the /wp-admin/ restriction. The REST API used by the block editor (/wp-json/) is also outside /wp-admin/, so this restriction does not block it.

Without a static IP

If your IP address changes, consider one of these:

Some hosting providers offer a control panel setting that restricts access to the admin area from outside the country. Setting that up together with .htaccess makes it harder to tell which one is causing a problem, so keep a note of where each restriction is configured.

Test the setup

Test from both an allowed connection and one that is not allowed (such as a phone on mobile data).

# From an allowed connection: 302 (redirect to wp-login.php) if not logged in
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-admin/

# From a connection that is not allowed: 403 means the restriction works
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-admin/
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php

# admin-ajax.php: 400 means the request reached WordPress (no action was given). 403 means you forgot to exclude it
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-admin/admin-ajax.php

Also try features that use admin-ajax.php, such as contact forms on public pages or a theme's "load more" button, once from a connection that is not allowed.

In the access log you can count where POSTs to the login screen come from (with the default Apache or Nginx log format).

# Sources sending the most POSTs to wp-login.php (top 20)
grep 'POST /wp-login.php' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# Number of requests blocked with 403
awk '$9 == 403 && $7 ~ /^\/(wp-admin|wp-login\.php)/' /var/log/nginx/access.log | wc -l

For Apache, read the log path as /var/log/apache2/access.log (Debian/Ubuntu) or /var/log/httpd/access_log (RHEL-based systems).

If you lock yourself out

If the admin area returns 403 because your IP address changed or you forgot to allow IPv6, revert as follows over FTP, SFTP, SSH or your hosting provider's file manager.

  • Apache: rename /wp-admin/.htaccess (for example, to .htaccess.off) or add your new IP address to the Require ip lines. Fix the <Files "wp-login.php"> section in the root .htaccess the same way
  • Nginx: add your new IP address to allow in the configuration file, run sudo nginx -t, then sudo systemctl reload nginx

To narrow down the cause of a 403 error in the admin area, also see "How to fix a 403 error in wp-admin".

Settings to combine with IP restrictions

IP restrictions narrow down who can open the admin area; they do not reduce the damage after someone logs in. Setting the following constant in wp-config.php disables editing theme and plugin files from the admin area.

<?php
// Put this in wp-config.php above "That's all, stop editing!"
define( 'DISALLOW_FILE_EDIT', true ); // Disable the theme and plugin file editors

Setting DISALLOW_FILE_MODS to true also prevents adding or updating plugins and themes from the admin area, as well as updating WordPress core. Unless you handle updates with WP-CLI or some other method, leaving it unset helps you avoid missed updates.

Summary

  • On Apache 2.4, put Require ip in /wp-admin/.htaccess and exclude admin-ajax.php (and admin-post.php if needed) with Require all granted. Order / Deny / Allow is Apache 2.2 syntax.
  • To protect the admin area, also limit wp-login.php in the root to the same IP addresses.
  • On Nginx, PHP is not passed to PHP-FPM unless you nest the PHP handling inside location ^~ /wp-admin/.
  • Decide which IP addresses to allow with IPv6, CDNs and reverse proxies, and requests from the server itself in mind, and confirm that a connection you have not allowed gets a 403.

References: Apache HTTP Server 2.4 – Access Control, Apache – mod_authz_core (Require), nginx – ngx_http_access_module, WordPress – Brute Force Attacks, WordPress – Hardening WordPress

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲