Troubleshooting

403 Forbidden error: what it means and how to fix it

A 403 error means the server understood the request and refuses it. What error 403 means, 401 vs 403 vs 404, and fixes for visitors and site owners.

Published ~11 min read
On this page

Short answer: a 403 Forbidden error means the server understood the request but refuses to serve it, and logging in again with the same account will not help. Visitors can check the address, clear the site’s cookies or switch off a VPN. Site owners usually find file permissions, a missing index file, an access rule or a WAF behind it.

If it is your own server, the web server’s error log names the reason in one line. Reload the page once, then read the newest entries:

Debian · Ubuntu · who sent the 403, and why
# From any machine: status line and Server header
curl -sI https://example.com/private/ | grep -iE '^(HTTP|server|cf-ray)'

# On the server: the newest error lines
sudo tail -n 20 /var/log/nginx/error.log        # nginx
sudo tail -n 20 /var/log/apache2/error.log      # Apache (Rocky, Alma: /var/log/httpd/error_log)

What does error 403 mean?

RFC 9110, the current HTTP specification, defines 403 as a server that “understood the request but refuses to fulfill it”. If the request carried credentials, the server considers them insufficient, and the client should not repeat it with the same ones. The RFC also says that a request can be forbidden for reasons that have nothing to do with credentials, and that a server which wants to hide that a resource exists may answer 404 instead.

A 403 is a decision, not a crash: a file the web server cannot read, a directory nobody may list, a rule that blocks your address, or a firewall that disliked the request. When the server sends no page of its own, Chrome shows Access to example.com was denied with HTTP ERROR 403. Otherwise the page tells you who answered:

What you seeWho sent itWhere to look
White page, 403 Forbidden, footer nginxnginxnginx error log
Forbidden. You don’t have permission to access this resource. with an Apache/2.4 footerApacheApache error log
403 Forbidden. Access to this resource on the server is denied!LiteSpeedLiteSpeed error log, .htaccess
403 - Forbidden: Access is denied or a detailed HTTP Error 403.14IIS on Windows ServerIIS section
Sorry, you have been blocked or Error 1020 Access denied, with a Cloudflare Ray IDCloudflare's WAF or a firewall ruleWAF section
The site's own styled page, or JSON from an APIThe applicationThe app's log: plugin, CSRF check, missing permission

401 vs 403 vs 404: what is the difference?

The three are often confused because all of them keep you from the page. They say different things, and only one of them is fixed by logging in:

Question401 Unauthorized403 Forbidden404 Not Found
What it saysWho are you? No valid credentialsUnderstood, and refusedNothing here, or nothing the server admits to
Does logging in help?Yes, with valid credentialsNo, not with the same accountNo
Required headerWWW-AuthenticateNoneNone
Typical cause on your serverauth_basic, an expired token or sessionPermissions, missing index file, deny rule, WAFWrong root, a typo, a missing route
Who fixes itThe visitor, by authenticatingThe site owner, or an owner granting accessWhoever moved or deleted the content

401 Unauthorized and 404 Not Found have their own guides. Some sites answer 404 for private pages on purpose, as RFC 9110 allows, so a 404 on a page you know exists can be a 403 in disguise.

How do I fix a 403 error as a visitor?

On a site you do not run, most 403s are deliberate and only the owner can lift them. These checks cover the cases you can fix yourself:

  • Check the address. A URL that ends at a folder, such as /images/ or /wp-content/uploads/, asks the server to list a directory, and most servers refuse that with a 403. Follow the link from the home page instead.
  • Log in with the right account. If you are logged in and still get 403, your account lacks the role for that page. A different account, or the owner granting you access, is the fix; logging in again with the same one is not.
  • Clear the site’s cookies. A stale session or security cookie can make an app refuse every request. Delete the cookies for that one site, or open it in a private window to test.
  • Switch the VPN or proxy off. Sites block address ranges that send abuse, the data centre ranges that VPNs use, and sometimes whole countries for licensing reasons. If the page works on mobile data but not on your VPN, the VPN’s address is blocked. A shared office or school connection can be blocked for the same reason.
  • Note the Ray ID on a block page. A Cloudflare Sorry, you have been blocked page means a security rule matched your request, and retrying will not help. Send the site owner the Ray ID and the time; it lets them find the rule.
  • Slow down. Some security tools answer a burst of requests, or a scraper’s user agent, with 403 rather than 429 Too Many Requests. Wait a few minutes before trying again.

Site owner: find the cause in the logs

On your own server, start with who answered. If the Server header reads cloudflare, repeat the request straight to the origin with --resolve example.com:443:203.0.113.10 (your server’s IP) to see whether your server or the CDN refused it.

The HTTP header checker shows the status line and headers from outside your network and says when a CDN’s bot protection answered instead of your server.

Then match the newest line in the error log:

Log lineServerCause
directory index of "/var/www/site/" is forbiddennginxNo index file, listings off
open() "/var/www/site/index.html" failed (13: Permission denied)nginxPermissions or SELinux
access forbidden by rulenginxAn allow/deny rule
AH01630: client denied by server configuration: /srv/site/ApacheA Require rule
AH00035: access to /index.html denied (filesystem path '/srv/site/index.html') because search permissions are missing on a component of the pathApacheA parent directory without x
AH01276: Cannot serve directory /srv/site/: No matching DirectoryIndex (index.html) found, and server-generated directory index forbidden by Options directiveApacheNo index file, listings off
ModSecurity: Access denied with code 403 (phase 2)nginx or Apache with ModSecurityA WAF rule
Nothing in the error log, 403 in the access logYour applicationThe app’s own check

A virtual host can have its own log file (sudo grep -R error_log /etc/nginx/ or ErrorLog in /etc/apache2/ finds it); in Plesk, the domain’s Logs page shows the same lines. Whether the 403 hits everyone or one address, the access log tells you:

Debian · Ubuntu · recent 403s by path and by client
# combined log format: the status follows the quoted request
sudo awk -F'"' '$3 ~ /^ 403 / {print $2}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
sudo awk -F'"' '$3 ~ /^ 403 / {split($1, a, " "); print a[1]}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

The nginx-specific walk-through, with the try_files trap and the Let’s Encrypt dotfile rule, is in nginx 403 Forbidden. The causes below apply to every web server.

Cause 1: the web server cannot read the files

The web server’s workers run as an unprivileged user, not as you and not as root. That user needs read permission on the file and execute (search) permission on every directory above it. One directory without x for that user is enough for a 403.

DistributionnginxApache
Debian, Ubuntuwww-datawww-data
Rocky, AlmaLinux, RHELnginxapache
Official nginx Docker imagenginxn/a

Check rather than assume with ps -o user=,args= -C nginx,apache2,httpd. Then let namei print the permissions of every component on the path at once:

namei -l: every directory on the path
namei -l /var/www/site/index.html
Example output
f: /var/www/site/index.html
drwxr-xr-x root   root   /
drwxr-xr-x root   root   var
drwxr-xr-x root   root   www
drwxr-x--- deploy deploy site
-rw-r--r-- deploy deploy index.html

Here site has no permissions for others, so www-data cannot enter it, and the file inside is unreachable however open it is. Apache logs exactly this case as AH00035, and a file that is not readable itself as AH00132 file permissions deny server access. The standard fix gives directories 755 and files 644:

Debian · Ubuntu · a web root the server can read
sudo chown -R deploy:deploy /var/www/site
sudo find /var/www/site -type d -exec chmod 755 {} +
sudo find /var/www/site -type f -exec chmod 644 {} +

# only what the app writes (uploads, cache) belongs to the web server user
sudo chown -R www-data:www-data /var/www/site/uploads

Sites in a home directory or in /root fail here by default, because those directories are closed to other users (Debian 12’s adduser creates homes with mode 0700); move the site to /var/www or /srv instead of opening your home directory. The chmod guide explains the digits, the chown guide covers ownership after a copy or a migration, and the chmod calculator translates modes both ways.

Cause 2: a directory without an index file

A request for / or /blog/ maps to a directory. The server looks for the index files it is configured with (index in nginx, DirectoryIndex in Apache), and if none exists and directory listings are off, it answers 403. nginx ships with autoindex off, so this is the usual result there. Debian’s Apache allows listings under /var/www (Options Indexes), so you get a file list instead until you switch it off with Options -Indexes; from then on a missing index is a 403 logged as AH01276.

  • The document root points at the wrong folder. Laravel serves from public/, most front-end builds from dist/ or build/.
  • A PHP site without index.php in the list. Use index index.php index.html; in nginx or DirectoryIndex index.php index.html in Apache.
  • The upload did not finish. An empty web root after a failed deploy looks exactly like this.

Do not turn listings on to make the 403 go away. That replaces the error with a public list of every file in the folder, backups and config copies included.

Cause 3: an access rule matches the request

On nginx the log says access forbidden by rule, and sudo nginx -T | grep -nE 'deny|allow' finds the rule. On Apache the message is AH01630, and the most common trigger is a document root outside /var/www. Debian’s apache2.conf denies the whole filesystem with Require all denied on <Directory /> and grants access only to /var/www and /usr/share. A site in /srv/site needs its own grant:

Apache · /etc/apache2/sites-available/example.com.conf
<Directory /srv/site>
    Options -Indexes +FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>
Debian · Ubuntu · test and reload
sudo apache2ctl configtest && sudo systemctl reload apache2

Other rules that end in a 403:

  • An .htaccess line such as Require ip 203.0.113.0/24, Require all denied or the old Apache 2.2 form Deny from all, often added by a security plugin to protect an admin area. The 2.2 directives need mod_access_compat, and Apache logs their refusals as AH01797.
  • An allow list behind a proxy. Behind Cloudflare or a load balancer, the server sees the proxy’s address, so an allow list for your office never matches. Configure set_real_ip_from and real_ip_header in nginx, or RemoteIPHeader and RemoteIPTrustedProxy from mod_remoteip in Apache, before you rely on the client address.
  • Hidden files. Many configs block every dotfile. That is right for .env and .git, and wrong for /.well-known/, which certificate validation needs.

Cause 4: SELinux on Rocky, AlmaLinux and RHEL

If the Unix permissions are right and the log still says Permission denied on a Red Hat family system, check SELinux. It is enforcing by default there and lets the web server read only files labelled for web content, such as httpd_sys_content_t; files moved with mv from a home directory keep their old label.

Rocky · AlmaLinux · RHEL
getenforce                                  # Enforcing?
ls -Zd /srv/site                            # which label?
sudo ausearch -m avc -ts recent | tail      # what SELinux refused

sudo semanage fcontext -a -t httpd_sys_content_t '/srv/site(/.*)?'
sudo restorecon -Rv /srv/site

Directories the app writes to need httpd_sys_rw_content_t, and sites served from users’ public_html need sudo setsebool -P httpd_enable_homedirs 1. Do not switch SELinux off to fix one label. The nginx 403 guide has more on labels and chcon versus semanage.

Cause 5: a WAF, a CDN rule or the application

When the web server’s own checks pass, a layer that inspects requests refused it. ModSecurity with the OWASP Core Rule Set writes a line like this to the error log, and the full request to its audit log (on Debian /var/log/apache2/modsec_audit.log):

Example log line
ModSecurity: Access denied with code 403 (phase 2). ... [id "949110"]
[msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [uri "/wp-admin/post.php"]

Rule 949110 is only the verdict; the lines just before it name the rules that added to the score. If one of them is a false positive for a known path, remove that rule for the path rather than turning the engine off:

Apache · ModSecurity exclusion for one path
<LocationMatch "^/wp-admin/post\.php$">
    SecRuleRemoveById 942100
</LocationMatch>
  • Cloudflare. A request that got a Cloudflare block page never reached your server. Filter the security events in the Cloudflare dashboard by the visitor’s Ray ID; the event shows the rule, the country and the address that matched.
  • The application. Django answers a failed CSRF check with a 403 page that reads CSRF verification failed. Request aborted., WordPress security plugins block by country or user agent, and APIs return 403 for a valid token that lacks a scope. The access log shows the 403 and the error log stays silent, so read the app’s log.
  • User agent filters. Some rules block curl or python-requests by name, so a test from the shell gets 403 while browsers do not. Compare with curl -sI -A 'Mozilla/5.0' URL.

Images or downloads that work on your site but fail when embedded elsewhere are usually hotlink protection: a rule that answers 403 when the Referer header names another site. In nginx it looks like this:

nginx · hotlink protection
location ~* \.(jpe?g|png|gif|webp)$ {
    valid_referers none blocked server_names *.example.com;
    if ($invalid_referer) {
        return 403;
    }
}

curl -sI -e https://other.example/ https://example.com/img/photo.jpg sends a foreign referer and shows whether the rule fires. If your own pages are refused, a new domain or CDN hostname is missing from the list.

403 on Windows Server and IIS

IIS adds a substatus. The one people meet most is HTTP Error 403.14 - Forbidden: the request maps to a folder without a default document while directory browsing is off, the IIS version of cause 2. Add the start page under Default Document in IIS Manager, or fix the site’s physical path. Two other substatuses are common: 403.4 when the site requires HTTPS and the request came over plain HTTP, and 403.503 when IP and Domain Restrictions rejected the client’s address. Missing NTFS read rights for the application pool identity or IUSR, by contrast, usually show up as 401.3, not as a 403. On a Windows VPS the detailed error page is shown only on the server itself, so reproduce the request there.

A custom 403 page, or a 404 instead

A branded 403 page is one line: error_page 403 /403.html; in nginx, or ErrorDocument 403 /403.html in Apache. Keep the status 403, make the page readable by the web server (otherwise the error page itself fails), and tell people what to do next: whom to contact, or how to log in with the right account. For admin areas you would rather not advertise, answering 404 is allowed by RFC 9110 and gives scanners less to work with.

Running the site on your own server

On a VPS or a dedicated server, every layer that can say 403 is yours, which is why the fix is quick once the log line is in front of you. Deploy as a normal user rather than root, so files never end up owned by an account the web server cannot read, and test every config before reloading. If you would rather not manage users and permissions per site at all, Plesk hosting sets up each site’s user and permissions for you.

Frequently asked

What does error 403 mean?

The server understood the request but refuses to fulfil it. RFC 9110 defines 403 Forbidden that way, and unlike a 401, sending the same credentials again will not change the answer. The usual reasons are file permissions, a directory without an index file, an access rule, a blocked IP address or country, or a web application firewall.

What is the difference between 401 and 403?

401 Unauthorized means the request carried no valid credentials, and logging in fixes it; the response must include a WWW-Authenticate header. 403 Forbidden means the server refuses the request anyway, so logging in again with the same account does not help. You need other rights, another account or a change on the server.

How do I fix a 403 Forbidden error on my own website?

Read the web server's error log right after the error. Permission denied means the web server user cannot read a file or pass through a directory: use 644 for files and 755 for directories. 'directory index of ... is forbidden' or AH01276 means there is no index file. 'access forbidden by rule' or AH01630 means an allow/deny or Require rule. On Rocky Linux or AlmaLinux, also check the SELinux labels.

Can a VPN cause a 403 error?

Yes. Many sites and firewalls block the address ranges that VPNs and data centres use, and some block whole countries. If a page loads on mobile data but returns 403 through the VPN, the VPN's address is blocked; switch the VPN off for that site or ask the owner to allow it.

Related articles

All troubleshooting