A 403 means the server understood your request perfectly and is refusing to fulfil it. That is the crucial distinction from a 401: you are not being asked to identify yourself, you are being told that identifying yourself would not help. Repeating the request unchanged will fail identically.
403 is a broad refusal with a narrow message, which is why it generates so much confusion. The status code covers filesystem permissions, web server rules, application-level authorisation, and firewall blocks — four unrelated problems wearing the same number. Telling them apart is most of the work.
Table of contents
- 403 versus 401, and why the difference matters
- Diagnosing: which layer refused?
- Filesystem permissions
- Web server rules
- WAF and CDN blocks
- Application-level 403s
- How this fits the rest of the stack
- FAQ
403 versus 401, and why the difference matters
401 Unauthorized means authentication is missing or invalid. The response carries a WWW-Authenticate header, and supplying valid credentials will change the outcome. The name is a historical misnomer — it means unauthenticated.
403 Forbidden means the server knows who you are, or has decided it does not care, and the answer is still no. Credentials will not change it.
The spec also notes that a server may return 404 instead of 403 when it does not want to confirm a resource exists. That is deliberate — telling an unauthorised user that /admin/users/1042 exists is itself a small information leak.
401 -> "I do not know who you are." Try again with credentials.
403 -> "I know, and no." Do not bother retrying.
404 -> "Nothing here." ...possibly a polite 403.
Diagnosing: which layer refused?
Read the response headers before touching anything. They usually name the culprit.
curl -I -v https://example.com/path
# Full exchange including the response body, which often
# contains a vendor's block page
curl -s -D - https://example.com/path -o /dev/null
What to look for:
Server: nginxorApachewith a short generic body → web server rule or filesystem permissions.- A branded block page, or headers like
cf-ray,x-sucuri-id,x-amzn-waf-*→ a web application firewall. The origin never saw your request. - JSON body with an application error code → your own authorisation logic. The framework is refusing, not the server.
403on every asset including static files → filesystem permissions or a directory-level deny rule.
The single most useful discriminator: does the origin log show the request at all? If it does not, a CDN or WAF blocked it upstream and everything you change on the server is irrelevant.
Filesystem permissions
The classic cause on a self-managed server. The web server process — www-data, nginx, or apache — must be able to read the file and traverse every directory above it.
# What the server sees
namei -l /var/www/example.com/public/index.html
# Standard, correct permissions
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
The traversal requirement catches people. A perfectly readable file inside a directory with mode 750 owned by another user is unreachable. namei -l walks the whole path and shows exactly where it breaks.
On RHEL, Fedora, and CentOS, SELinux adds a second layer that looks identical from the outside:
# Are we being denied by SELinux rather than by permissions?
sudo ausearch -m avc -ts recent
# Correct context for web content
sudo chcon -R -t httpd_sys_content_t /var/www/example.com
sudo restorecon -Rv /var/www/example.com
Never respond to a 403 with chmod 777. It does clear the error, and it also makes every file writable by every process on the machine, which converts a broken page into a security incident.
Web server rules
Both Apache and Nginx return 403 for directory listing requests when no index file exists, and for anything matching an explicit deny.
# Nginx: a 403 on a directory means no index file and autoindex off
location / {
index index.html index.php;
# autoindex on; # would list the directory instead of 403
}
# Nginx: a deny rule -- intentional 403
location ~ /\.(?!well-known) {
deny all;
}
# Apache: the modern access-control syntax
<Directory /var/www/example.com/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Require all denied left in a config, or a missing Require all granted, is a very common Apache 403. So is mixing the Apache 2.2 Order/Allow syntax with the 2.4 Require syntax in the same file — the old directives are ignored unless mod_access_compat is loaded, and the result is a silent deny.
Check that the config you are editing is the one being used, since virtual host precedence surprises people:
apachectl -S # which vhost serves which name
nginx -T | grep -A5 'server_name example.com'
WAF and CDN blocks
Increasingly the most common 403 in production, and the most confusing, because nothing on your server is wrong.
A web application firewall inspects requests and blocks those matching its rules. False positives are routine: a legitimate POST containing SQL-like text in a code snippet, a form field with angle brackets, an unusual user agent, or a request rate that looks automated.
- Find the block ID. Most WAFs put a reference in the response body or a header. That ID maps to a specific rule in the vendor’s dashboard.
- Check whether the origin logged the request. If not, the block is upstream, full stop.
- Test from a different IP and user agent. A block that follows the IP is rate limiting or reputation; one that follows the request shape is a content rule.
- Look at what is in the payload. Content-inspection rules fire on the request body, and a code-sharing feature or a rich text editor will trip them regularly.
The fix is a rule exception scoped as tightly as possible — one path, one rule ID. Disabling a whole rule group because one endpoint tripped it trades a broken feature for a real hole.
Bot-protection rules are a related case worth calling out: they routinely block legitimate API clients and monitoring checks. If a health check started 403-ing without a deploy, look there before looking at your code — the response headers documentation covers what you control at the edge.
Application-level 403s
The last category is your own code, and it is the one where a 403 is working correctly. A user without the right role requests a resource, and the framework refuses.
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "InsufficientPermissions",
"message": "Deleting users requires the admin role."
}
Return a body that says why. A bare 403 with no explanation costs your support team hours and tells the client nothing actionable. The example above is from the MDN documentation for a reason — it is what a useful 403 looks like.
Two things worth deciding deliberately in your own API. First, whether to return 403 or 404 for resources the user may not see — 404 leaks less, 403 debugs more easily, and the right answer depends on how sensitive existence is. Second, log the denial with the user, the resource, and the rule that fired. Authorisation bugs are hard to reproduce and trivial to diagnose from a good log line.
That is a general point about 403s. Whichever layer produced it, the fix is fast when you can see the request on the server side and slow when you cannot — which is why runtime logs alongside the deploy are worth more here than any amount of guessing at configuration.
How this fits the rest of the stack
Work outside in: check whether the origin saw the request at all, then response headers, then server config, then permissions, then your own authorisation code. Most production 403s in 2026 are firewall false positives rather than anything on the server. And when you emit one yourself, say why in the body. If you are sizing a deployment where the runtime logs sit next to the deploy that produced them, the RunxBuild hosting calculator shows the services and bandwidth as separate figures.
Useful related references:
- n8n HTTP Request Node: The Auth and Error Playbook
- HTTP Error 509: Bandwidth Limit Exceeded (and How to Fix It)
- HTTP 504 Gateway Timeout: Reading the Error as a Map of Your Stack
- Services on RunxBuild
FAQ
What is the difference between 401 and 403?
A 401 means you have not authenticated and valid credentials would change the outcome. A 403 means the server has decided the answer is no regardless of who you are, so repeating the request with credentials will not help.
Why do I get 403 Forbidden on every file including images?
That pattern points at filesystem permissions or a directory-level deny rule rather than application logic. Run namei -l on the full path to find which directory the web server cannot traverse.
How do I know if a WAF is causing my 403?
Check whether the request appears in your origin server’s access log. If it does not, something upstream blocked it. Branded block pages and headers such as cf-ray or x-amzn-waf identify the vendor.
Should I use chmod 777 to fix a 403?
No. It clears the error by making every file writable by every process on the machine, which turns a broken page into a security problem. Use 755 for directories and 644 for files, owned by the web server user.
Should my API return 403 or 404 for resources a user cannot access?
Both are defensible. A 404 avoids confirming the resource exists, which matters when existence itself is sensitive. A 403 is easier to debug and support. Pick one deliberately and apply it consistently.