ERROR · 18 · VERIFIED 2026-10allwpcare.com/errors/403-forbidden/
Errors / 403-forbidden

403 Forbidden in WordPress

The server refused the request. On Apache, read the error log: the AH code names the cause, a deny rule in .htaccess or a file permission. Then fix that one.

The message you see
1−Forbidden
2−You don't have permission to access this resource.
Last verified Oct 2026Tested with WordPress 7.1Applies to WordPress 7.1 (tested on Apache 2.4) · PHP 7.4+

Why it happens

  • A rule in .htaccess or the server config denies the request, such as Require all denied, a Require ip allow list that does not include your address, or an old Order/Deny block. The log line is AH01630 or AH01797, "client denied by server configuration".
  • A file or folder cannot be read by the user Apache runs as. For a file the log line is AH00132, "file permissions deny server access". For a folder it is AH00529, "unable to check htaccess file".
  • The address is a folder with no index file, and listing folders is turned off. The log line is AH01276, "Cannot serve directory". On a site's front page this usually means index.php is missing.
  • A symbolic link is not allowed, because FollowSymLinks is off. The log line is AH00037.
  • A rewrite rule with the [F] flag, often a block on a user agent. This writes no line to the error log, only to the access log.
  • A firewall, CDN or security plugin blocks the visitor and sends the 403 itself. The server error log has nothing for it.

How to fix it

Try the fixes in this order. Each one is independent: stop as soon as the error is gone.

FIX 01

Find out who said 403

  1. Open the page that fails and look at what it says. A plain white page headed "Forbidden" with the line "You don't have permission to access this resource." is from Apache. WordPress never ran. This page covers that case.
  2. On the command line, curl -si https://example.com/wp-admin/ | head -12 shows the headers. An Apache refusal has Content-Type: text/html; charset=iso-8859-1 and no X-Powered-By line. A refusal sent by WordPress or a plugin has charset=UTF-8, and on my test site it also had X-Powered-By: PHP/8.3.36 and a Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private line. These headers are hints, not proof. A host or plugin can change the charset or remove them. I turned X-Powered-By off to check, and the Cache-Control line stayed on my test site. The Apache error log is the real test.
  3. If the page has a firewall's or CDN's name, or a block ID or ray ID, the CDN or firewall answered. Look in its dashboard for the blocked request. I did not reproduce this case.
  4. If WordPress shows a styled page, such as "Sorry, you are not allowed to access this page.", use that error page instead.
FIX 02

Read the Apache error log

  1. In your hosting panel look for Logs or Error log, or download error_log over SFTP. On a server you manage it is often /var/log/apache2/error.log or /var/log/httpd/error_log.
  2. Find the line with the time you loaded the page. The AH code and the path at the end tell you which fix to use. The table below lists what I produced.
  3. If there is no line for your request, the 403 did not come from a deny rule or a permission. Check for a RewriteRule ... [F] in .htaccess, then for a plugin or firewall.
FIX 03

Remove or correct a deny rule in .htaccess

  1. Connect by SFTP, turn on hidden files and open the .htaccess file named in the log path. Rules for /wp-admin/ can be in wp-admin/.htaccess. WordPress does not ship that file, so someone or a plugin added it.
  2. Copy the file first. Then delete the rule that denies the request, or rename the whole file to .htaccess.off. On my test site, removing Require all denied from wp-admin/.htaccess took /wp-admin/ back to a normal answer at once.
  3. Moving the root .htaccess aside also drops your permalink rules and any redirect or security rules in it. After the site loads again, open Settings → Permalinks and click Save Changes. WordPress writes its own block again. I did this after moving a root .htaccess aside: /hello-world/ went from 404 to 200.
  4. If you want to keep an allow list for the admin area, add your current address to it, for example Require ip 203.0.113.5, and also allow admin-ajax.php, as shown below. Home and office addresses change, so a list of one address locks you out later.
FIX 04

Fix a file or folder the server cannot read

  1. Log in by SSH or use your host's file manager. Run ls -l on the file or folder from the log line, and on each folder above it.
  2. Apache needs to read every file it serves and to enter every folder on the way. A file with mode 600 owned by another user, or a folder with mode 000, is refused. I tested a file owned by root with mode 600 (403 with AH00132), then chown to the site user (200). The same file owned by root with mode 644 was served normally.
  3. The WordPress handbook gives 755 or 750 for folders, 644 or 640 for files, and 440 or 400 for wp-config.php. These are its numbers for shared hosts that run PHP as your own user (suexec). Other setups can need different modes, so ask your host if unsure. Set the one that is wrong: chmod 755 folder or chmod 644 file. Avoid 777: it lets everyone write, which is more than reading needs.
  4. If ownership is the problem, ask your host for the right user, or run chown to the account that runs PHP.
  5. A PHP file that Apache cannot read gives a different answer from a picture or style sheet. On my test site an unreadable hello.php returned 500, not 403. See 500 Internal Server Error for that.
FIX 05

Give a folder an index file, or restore index.php

  1. If the front page of the site shows Forbidden, check that index.php is in the folder with wp-config.php. I renamed it and the home page returned 403 with AH01276. Putting it back returned 200. Upload index.php from a fresh copy of WordPress of the same version.
  2. A 403 for a folder such as /wp-content/uploads/ is on purpose. The server will not list a folder's files. Link to a file, not the folder. Do not turn listing on to remove the message.
  3. If you do want a visible index page for a folder, add an index.html to that folder.
FIX 06

Allow symbolic links, or stop blocking a visitor

  1. If the log says AH00037 for a file that is a symbolic link, remove Options -FollowSymLinks from the .htaccess that holds it. I tested +FollowSymLinks: the same link went from 403 to 200.
  2. If the log has nothing and .htaccess has a RewriteCond %{HTTP_USER_AGENT} rule with [F], that rule is sending the 403. Remove it, or make the pattern narrower.
  3. For a security plugin's block, open the plugin's own log or Blocked list and remove your address. Check the plugin's page, not this one, for the steps.

A 403 means the server understood the request and refuses to answer it. Signing in again does not change it. Most often it is one line in .htaccess, a file or folder the server cannot read, or a security tool that blocked you. On Apache, the error log names which one, and each case below has its own fix.

Everything on this page was tested on Apache 2.4.69 with WordPress 7.1.4 and PHP 8.3. I did not test nginx. It has no .htaccess, and its rules live in the server config, so the codes and files below do not apply there.

What you see#

When Apache refuses a request, the page is plain and unstyled. It has the heading "Forbidden" and one line under it. The footer shows the Apache version unless your host hides it.

A plain white page with the heading Forbidden, the line You don't have permission to access this resource, and a footer line that reads Apache/2.4.69 (Debian) Server at localhost Port 8907.
The Apache 403 page on a test site. WordPress did not run: the page has no WordPress styling. It came from a one-line rule, Require all denied, in wp-admin/.htaccess.

The same request in a terminal:

Terminal
curl -si http://localhost:8907/wp-admin/ | head -12
Output
HTTP/1.1 403 ForbiddenDate: Fri, 30 Oct 2026 07:09:24 GMTServer: Apache/2.4.69 (Debian)Content-Length: 316Content-Type: text/html; charset=iso-8859-1<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don't have permission to access this resource.</p>

Who can say 403#

Four different places can send a 403, and they need different fixes. The quickest way to tell them apart is the page, the headers and the server error log.

Four stacked boxes. 1: a firewall or CDN in front, which shows its own page and its name in the Server header. 2: server rules in .htaccess or the Apache config, which log AH01630 or AH01797. 3: file or folder permissions, which log AH00132, AH00529 or AH00037 with Permission denied. 4: WordPress or a plugin, which sends a styled page or short text, with an X-Powered-By header and no server log line.
Who can send a 403, and the clue for each. I reproduced rows 2, 3 and 4 on Apache. Row 1 needs a real firewall or CDN, which I did not test.

WordPress sends 403 too. A subscriber who opens Settings → General gets a styled page titled "WordPress › Error" that says "Sorry, you are not allowed to access this page." Core sends it with wp_die( ..., 403 ) in wp-admin/includes/menu.php. The REST API answers 403 with JSON such as {"code":"rest_cookie_invalid_nonce","message":"Cookie check failed","data":{"status":403}}. A security plugin that blocks a visitor does the same with wp_die() or status_header( 403 ). I wrote a small test plugin that does both. Neither wrote a line to the Apache error log, which is the clearest sign that the server did not refuse.

WherePageContent-TypeX-Powered-ByError log line
Apache rule or permissionPlain "Forbidden" with Apache footeriso-8859-1noneyes, with an AH code
wp_die() from WordPress or a pluginStyled page, or short textUTF-8PHP/8.3.36 (hosts can hide it)none

The log lines I produced#

Each row is one request, and the line it wrote to the Apache error log. Paths are shortened.

What I didLog code and textStatus
Require all denied in wp-admin/.htaccessAH01630: client denied by server configuration: /var/www/html/wp-admin/403
Old Order deny,allow and Deny from allAH01797: client denied by server configuration (the module is access_compat)403
Require ip 203.0.113.5 (not my address)AH01630, same text403
Same line with my address addednone200
index.php renamed, then home pageAH01276: Cannot serve directory /var/www/html/: No matching DirectoryIndex (index.php,index.html) found, and server-generated directory index forbidden by Options directive403
Text file with mode 000AH00132: file permissions deny server access: …/a.txt with (13)Permission denied403
Text file owned by root, mode 600AH00132, same text403
Folder with mode 000AH00529: …/.htaccess pcfg_openfile: unable to check htaccess file, ensure it is readable and that '…/' is executable (level crit)403
Symbolic link with Options -FollowSymLinksAH00037: Symbolic link not allowed or link target not accessible403
RewriteRule .* - [F,L] for a user agentno error log line, only the access log403
PHP file with mode 000PHP Fatal error: Failed opening required500, not 403

The folder case also changes the page. Apache adds a sentence to the same line: "You don't have permission to access this resource.Server unable to read htaccess file, denying access to be safe". That sentence means the server could not read a .htaccess file in a folder on the way.

Apache error log
[core:crit] (13)Permission denied: AH00529: /var/www/html/wp-content/uploads/.htaccess pcfg_openfile: unable to check htaccess file, ensure it is readable and that '/var/www/html/wp-content/uploads/' is executable

Why the fix works#

Apache checks three things before it serves a file. It reads every .htaccess file from the web root down to the folder. It applies the Require rules found there. Then it opens the file as the user it runs as. A refusal at any of these steps is a 403, and each step writes its own AH code, so the code tells you which step refused. Changing a mode on a file whose problem is a Require rule does nothing, and removing a rule does nothing for a folder Apache cannot enter.

Locking down wp-admin without locking yourself out#

People add Require ip or Require all denied to wp-admin/.htaccess to protect the admin. It works, but it also blocks wp-admin/admin-ajax.php. Some themes and plugins call that file from the public site, so visitors then get errors. With Require all denied in place, I got 403 on admin-ajax.php too. This version keeps it open and still locks the rest:

wp-admin/.htaccess
Require ip 203.0.113.5<Files admin-ajax.php>Require all granted</Files>

On my test site, admin-ajax.php then answered 400, which is its normal answer when no action is sent, and /wp-admin/ stayed 403 for any other address. If your site sits behind a proxy or CDN, Apache sees the proxy's address, not the visitor's. Then an IP list either blocks everyone or nobody. Ask your host for the right setup before you use one.

The .htaccess generator builds rules like these with an undo for each step.

Edge cases I tested#

  • Plugins that deny by file name. A <Files "xmlrpc.php"> block with Require all denied returned 403 for xmlrpc.php and left wp-login.php at 200. Blocking xmlrpc.php on purpose looks exactly like an error.
  • Opcache can hide a bad mode. When I set wp-login.php to mode 000, the login page still returned 200. A brand-new PHP file with mode 000 returned 500 on its first request. So a wrong chmod does not always show up on the file you changed. Test the page after you change a mode, and test files you added recently.
  • Default folder listing differs. The Docker image I used turns folder listing off in its Apache config. A stock Debian Apache turns it on, and then a folder with no index file lists its files, not a 403. A 403 for a folder is not a fault.
  • A 403 for a missing page. MDN notes that servers may answer 404 instead of 403 to hide that a file exists. If a file you know exists shows 404, look at the rules too.

If it keeps happening#

A rule that comes back after you remove it is being written by something else, such as a plugin or a host tool. Look at its settings, and see the hardening guide for what is worth blocking. If you hid the login page on purpose, changing the login URL explains why the old address now refuses you. If the log has nothing and the page is not from Apache, ask your host or CDN for the log entry for the blocked request. For a different failure that also stops the page from loading, see 500 Internal Server Error. If the Updates screen shows a JSON message rather than a refusal, see the response is not a valid JSON response.

Sources

  1. Apache HTTP Server: Access control · Require ip, Require all, and the 2.2 Order/Allow/Deny directives marked deprecated
  2. Apache HTTP Server: mod_authz_core · Require all denied: access is denied unconditionally
  3. Apache HTTP Server: core directives, Options · Options, Indexes and FollowSymLinks in .htaccess
  4. MDN: 403 Forbidden · What 403 means and how it differs from 401 and 404
  5. WordPress: file permissions · 755/750 for folders, 644/640 for files, 440/400 for wp-config.php, as given for suexec hosts
  6. WordPress core: wp-admin/includes/menu.php (6.8.3) · WordPress's own 403: wp_die() with Sorry, you are not allowed to access this page.
Newsletter

One email when something breaks or changes.

New plugins and re-tested guides, about twice a month. Never a sales pitch.