How to Diagnose a 403 Forbidden Error

27 Oct 2026, 16:38:28
HTTP 403 Forbidden means that the server received and understood the request but refuses to fulfill it. Unlike 404, the resource may exist, but access to it is denied. The refusal can originate at different layers, and the goal of troubleshooting is to identify which layer is responsible:
Client → CDN/proxy → WAF → web server (Nginx/Apache) → filesystem → application (PHP, CMS)
Do not change permissions or disable security “at random”: identify the cause first.

1. Check the server's HTTP response

Make sure that 403 is returned consistently and determine exactly where it comes from.
curl -I https://example.com/Example response:
HTTP/2 403
server: nginx
date: Tue, 27 Oct 2026 11:15:42 GMT
content-type: text/html
content-length: 153
For more detailed output (TLS, request and response headers):
curl -v https://example.com/Compare different URLs to determine the scope of the problem:
curl -sS -o /dev/null -w '%{http_code}\n' https\://example.com/              # homepage
curl -sS -o /dev/null -w '%{http_code}\n' https\://example.com/admin/        # directory
curl -sS -o /dev/null -w '%{http_code}\n' https\://example.com/style.css     # static file
curl -sS -o /dev/null -w '%{http_code}\n' https\://example.com/index.php     # PHP file
How to interpret the results:

Result
Where to look
403 everywhere
Root directory permissions, virtual host configuration, WAF/CDN
403 only for /admin/
A location block, .htaccess, application restrictions
403 only for .php
PHP handler settings, WAF, application rules
403 for a directory, but a file inside it opens
No index file; directory listing is disabled
Important: the server: nginx or server: Apache header only shows which server responded; it does not explain why access was denied.

2. Check the web server logs

This is the most effective step. The log often states the reason directly.

Nginx
tail -n 100 /var/log/nginx/error.logTo watch log entries as they appear, open two terminals. In the first one:
tail -f /var/log/nginx/error.logIn the second, run curl -I https://example.com/admin/ (or open the page in a browser) and check what appears in the first terminal.
Examples of messages and what they mean:
No index file; directory listing is disabled:
2026/10/27 12:15:42 [error] 1234#1234: *5 directory index of "/var/www/example.com/public/uploads/" is forbidden, client: 203.0.113.5, server: example.com, request: "GET /uploads/ HTTP/2.0", host: "example.com"
There is no index.html/index.php in the uploads directory, and autoindex is disabled. This is normal behavior, not a malfunction.

No permission to access the file or one of the directories in its path:
2026/10/27 12:17:03 [error] 1234#1234: *7 open() "/var/www/example.com/private/file.php" failed (13: Permission denied), client: 203.0.113.5, server: example.com, request: "GET /private/file.php HTTP/2.0", host: "example.com"
Error 13: Permission denied means that the user running Nginx (usually www-data) cannot access the file or one of the parent directories.

If the log is empty, it may be located elsewhere:
nginx -T 2>&1 | grep -E 'error_log|access_log'Example output:
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log warn;
This means that the site has its own log files, which you should check.

Apache
tail -n 100 /var/log/apache2/error.log
tail -f /var/log/apache2/error.log
Examples of messages:
[Tue Oct 27 12:20:11.123456 2026] [authz_core] [pid 2345] [client 203.0.113.5:51234] AH01630: client denied by server configuration: /var/www/example.com/public/admin/
Access is denied by configuration directives (Require all denied, Require ip, etc.) or by rules in .htaccess.

[core] [pid 2345] [client 203.0.113.5:51240] AH01276: Cannot serve directory /var/www/example.com/public/uploads/: No matching DirectoryIndex (index.html,index.php) found, and server-generated directory index forbidden by Options directive
This is the Nginx equivalent of directory index ... is forbidden: there is no index file, and Options -Indexes prohibits directory listing.

[core] [pid 2345] [client 203.0.113.5:51250] AH00035: access to /index.html denied (filesystem path '/var/www/example.com') because search permissions are missing on a component of the path
The x (execute/traverse) permission is missing on one of the directories in the path. This is a classic permissions issue.

If Nginx runs in front of Apache as a proxy, check the logs of both servers: either one may have returned 403.
Log paths depend on the operating system, configuration, and control panel (for example, on CentOS/RHEL, Apache writes to /var/log/httpd/error_log).

3. Check file and directory permissions

Assume the site is located in /var/www/example.com/public/.
ls -ld /var/www/example.com
ls -ld /var/www/example.com/public
ls -l  /var/www/example.com/public/
To access a file, the process needs read permission on the file itself and execute (x) permission on every directory in the path. The easiest way to inspect the entire path is:
namei -l /var/www/example.com/public/index.htmlExample of problematic output:
f: /var/www/example.com/public/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwx------ deploy deploy example.com <-- problem here
drwxr-xr-x deploy deploy public
-rw-r--r-- deploy deploy index.html
The example.com directory has permissions 700 and is owned by deploy. The www-data user cannot traverse it, even though the permissions on public are correct. Nginx will log Permission denied.

Find out which user the web server runs as:
ps aux | grep -E 'nginx|apache2|httpd' | grep -v grep
root 812 ... nginx: master process /usr/sbin/nginx
www-data 813 ... nginx: worker process
The worker processes run as www-data. In Nginx, the user is set by the user directive in nginx.conf.

Possible fixes (choose deliberately rather than applying them mechanically):
Option 1: allow everyone to traverse the directory (x)
chmod 755 /var/www/example.comOption 2: safer if the web server only needs group access
chgrp www-data /var/www/example.com
chmod 750 /var/www/example.com
Commonly used permissions:
  • Directories: 755;
  • Regular files: 644.
This is not a universal rule. It depends on file ownership, the web server user, PHP-FPM, and the application's requirements. For example, a WordPress uploads directory must be writable by the user running PHP.
Do not do this:
chmod -R 777 /var/www/example.com   # insecure and often does not solve the problem

4. Check whether an index file exists

If 403 occurs when opening a directory or the homepage:
ls -la /var/www/example.com/public/Example of a problematic situation:
drwxr-xr-x 3 deploy deploy 4096 Oct 27 12:00 .
drwxr-xr-x 4 deploy deploy 4096 Oct 27 11:50 ..
-rw-r--r-- 1 deploy deploy 120 Oct 27 12:00 home.html
drwxr-xr-x 2 deploy deploy 4096 Oct 27 11:55 assets
There is no index.html or index.php file; only home.html exists. The server looks for an index file in its configured list, cannot find one, and returns 403 because directory listing is disabled.
Nginx
server {
   listen 80;
   server_name example.com;
   root /var/www/example.com/public;
   index index.php index.html;
}
If you want home.html to be the homepage, add it to the index directive:
index home.html index.php index.html;For a PHP site, also check that requests are passed to PHP-FPM:

location ~ \.php$ {
   include snippets/fastcgi-php.conf;
   fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
Apache
    DirectoryIndex index.php index.html
    Options -Indexes
    Require all granted
Note Require all granted: if this directive is missing (and the main configuration denies access to the directory by default), Apache will return 403 even if file permissions are correct.
Do not add index files blindly: first make sure the application actually uses that file. If you intentionally need to display a directory's contents, enable this separately (autoindex on in Nginx or Options +Indexes in Apache). Do this only if you understand the security implications.

5. Check the web server configuration

Nginx
Look for directives that explicitly deny access:
location /admin/ {
   allow 192.0.2.10;
   deny all;
}
location /private/ {
    return 403;
}
In the first example, only the IP address 192.0.2.10 is allowed into /admin/; everyone else gets 403. The allow/deny rules are checked in order, and the first matching rule takes effect.
A common mistake:
location /admin/ {
   deny all;
   allow 192.0.2.10;   # unreachable: deny all takes effect first
}
View the effective configuration, including all included files:
nginx -T
nginx -T > /tmp/nginx-config.txt 2>&1
grep -n -E 'server_name|deny|allow|return 403' /tmp/nginx-config.txt
Always check the syntax before reloading:
nginx -t && systemctl reload nginx
Apache
    Require ip 192.0.2.10Access is allowed only from one IP address. To deny everyone:
Require all deniedFind all '.htaccess' files on the site:
find /var/www/example.com -name .htaccess -type fPrint access-control directives from them:
grep -rn -E 'Require|Deny|Allow|Order|RewriteRule.*\[F' /var/www/example.com --include=.htaccessA mixture of old (Apache 2.2) and new (Apache 2.4) syntax is often found in .htaccess files:
Old syntax; in Apache 2.4, it works only if mod_access_compat is enabled:
Order deny,allow
Deny from all
Modern Apache 2.4 syntax:
Require all deniedThe [F] flag in RewriteRule also returns 403:
RewriteRule ^secret/ - [F,L]Check the Apache configuration syntax:
apachectl configtest

6. Check the WAF and security mechanisms

If permissions and configuration are correct and the web server logs do not explain the issue, 403 may be returned by a security layer: ModSecurity, the hosting provider's WAF, a CMS plugin (such as Wordfence), fail2ban, or a CDN.

ModSecurity

A typical scenario: a form or URL contains “suspicious” characters, such as SQL-like text or  in a parameter. The request is blocked even though it is not malicious.
Example log entry (the format depends on the version):
ModSecurity: Access denied with code 403 (phase 2). Matched "Operator 'Ge' with parameter '5' against variable ' TX:ANOMALY_SCORE' (Value: ' 10') [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 10)"] [uri "/search"] [unique_id "ZxYz..."]
Important fields: id (the ID of the triggered rule), msg (description), and uri (the blocked URL). Find entries around the time of the request:
grep -i modsecurity /var/log/apache2/error.log | tail -n 20Or, if a separate audit log is configured:
tail -n 100 /var/log/modsec_audit.logRule ID 949110 is the general anomaly threshold in OWASP CRS: it indicates that too many points have accumulated from other rules. Look at nearby entries with the same unique_id to find the cause.
If a rule is triggered incorrectly, it is usually best to create a targeted exclusion (for the specific id and URL) rather than disabling ModSecurity entirely.

Cloudflare and other CDNs/proxies
First determine who is returning 403. Check the headers:
curl -I https://example.com/
HTTP/2 403
server: cloudflare
cf-ray: 8a1b2c3d4e5f6789-AMS
The server: cloudflare and cf-ray headers indicate that the response came from Cloudflare, not your server. The response body often contains a code such as Error 1020 Access denied, indicating that a firewall rule was triggered.
Now check the origin server directly, bypassing the CDN, by substituting its IP address (ORIGIN_IP is your server's IP):
curl -I --resolve example.com:443:ORIGIN_IP https\://example.com/
  • If the direct request returns 200 but the request through the CDN returns 403, the issue is with the CDN rules (WAF, Bot Fight Mode, country or IP blocking).
  • If the direct request also returns 403, the cause is on your server.
Do not disable the entire security layer. Find the specific rule that was triggered (in the CDN dashboard, check the security events log) and adjust that rule only.

7. Check IP address restrictions

A typical symptom: the site opens on your computer, but returns 403 for a client or from a server (or vice versa).
Example comparison. From your local computer:
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https\://example.com/
HTTP 200
From a server or another connection (for example, mobile internet):
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https\://example.com/
HTTP 403
Now determine your public IP address and look for it in the configuration and logs:
curl -s https://ifconfig.me
grep 203.0.113.5 /var/log/nginx/access.log | tail
grep -rn 203.0.113.5 /etc/nginx/ /etc/apache2/
If the IP appears in a deny rule, a fail2ban ban list, or WAF rules, you have likely found the cause. Check fail2ban:
fail2ban-client status
fail2ban-client status sshd        # the jail name may differ (nginx-http-auth, apache-badbots, etc.)
Different responses help narrow down the search, but do not prove that the IP address itself is the cause. Differences can also be caused by DNS, proxies, routes, User-Agent, and headers. Check those as well:
curl -I -A "Mozilla/5.0 (X11; Linux x86_64)" https\://example.com/If a “browser-like” User-Agent gets 200, while curl/8.x gets 403, a rule based on User-Agent is blocking the request (in the WAF, .htaccess, or a security plugin).

8. Check PHP and application logs

If 403 occurs only on specific pages, in the admin panel, or when submitting a form, the cause is often in the application itself.
Examples of cases where an application deliberately returns 403:
  • Django: “CSRF verification failed. Request aborted” (the CSRF token validation failed);
  • WordPress: a security plugin blocks a request to wp-admin or admin-ajax.php;
  • the application checks the user's role and denies access if the user lacks the required permissions;
  • a request limit (rate limit) is implemented at the application level.
Where to look for messages:
PHP-FPM service (the name depends on the version):
journalctl -u php8.2-fpm --since "30 minutes ago"PHP error log, if its path is known:
php -i | grep -i error_log
tail -n 100 /var/log/php8.2-fpm.log
WordPress: enable logging in wp-config.php:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
Then check wp-content/debug.log:
tail -n 50 /var/www/example.com/public/wp-content/debug.logKeep in mind that PHP errors usually result in 500, not 403. If the PHP logs are clean, that does not rule out blocking at the web server or WAF level.

9. Match the request time against the logs

If the cause is not obvious, make one test request and immediately look for it in the logs.
date
curl -sS -o /dev/null -w 'HTTP status: %{http_code}\n' https\://example.com/admin/
tail -n 50 /var/log/nginx/error.log
If there is nothing in error.log, check the access log:
tail -n 100 /var/log/nginx/access.logExample entry in the standard format:
203.0.113.5 - - [27/Oct/2026:12:15:42 +0100] "GET /admin/ HTTP/2.0" 403 153 "-" "curl/8.5.0"
This shows the client IP, time, method and URL, the 403 response code, and the User-Agent. Quickly find all 403 responses:
awk '$9 == 403' /var/log/nginx/access.log | tail -n 20(Field number 9 is correct for the standard combined format; it will be different if the log format has been changed.)
For intermittent errors, compare the timestamps of several requests across three sources: the web server log, the WAF log, and the application logs.

10. Quick troubleshooting checklist

  1. Check the response code: curl -I https://example.com/. 
  2. Determine who is responding: the server and cf-ray headers and the response body. Is it your server, a proxy, or a CDN?
  3. Repeat the request and check the web server's error.log.
  4. If the log contains Permission denied, run namei -l on the file path and check the web server user.
  5. If a directory is being requested, check the index file and the index / DirectoryIndex directives, as well as Require all granted in Apache.
  6. If the log says “denied by server configuration”, inspect deny/allow/return 403 in Nginx, or Require/Deny and .htaccess in Apache.
  7. If the web server logs do not explain the issue, check WAF/ModSecurity, the CDN, and CMS security plugins.
  8. If the result depends on the connection, compare responses from different IP addresses and with different User-Agent values.
  9. Check the application logs: the site's own code may have returned 403.
  10. Make one change, repeat the request, and verify that the error is gone. Do not change several settings at once, or you will not know which change fixed the issue.

Conclusion

HTTP 403 can have many different causes, from incorrect directory permissions to a triggered WAF rule. Therefore, do not immediately run chmod 777 or disable security.
Start by checking the web server logs at the time of the error: messages such as Permission denied, directory index ... is forbidden, or client denied by server configuration almost always indicate where to look. If the logs provide no clues, check access configuration, WAF and CDN, IP restrictions, and application logs in sequence.