HTTP 403 Forbidden
403 means the answer is no, and re-authenticating won't change it. Per RFC 9110, if credentials were provided the server considers them insufficient, and the client shouldn't automatically repeat the request with the same credentials.
403 Forbidden
The server understood the request but refuses to fulfil it.
Who fixes it: Either side. If the user lacks permission, it's working as intended. If it's a file permission, WAF rule or bucket policy, the server side has to change.
What 403 means
The server may explain why in the body, or not. RFC 9110 explicitly allows a server to respond 404 instead of 403 when it doesn't want to reveal that the resource exists.
A 403 can come from anywhere in the chain: your application (a role check), the web server (file permissions, deny rules), a CDN or WAF (bot protection, geo-blocking) or a storage service like S3.
Source: RFC 9110 §15.5.4.
Common causes
- nginx can't read the file. The error log shows
open() "/var/www/site/index.html" failed (13: Permission denied). The nginx worker user (oftenwww-dataornginx) needs read permission on the file and execute permission on every parent directory. - A directory with no index file and
autoindex off: nginx logsdirectory index of "/var/www/site/" is forbidden. - SELinux (RHEL, Fedora, Rocky) blocks nginx from files with the wrong security context, even when Unix permissions look fine.
deny/allowrules, an IP allowlist, Apache.htaccessrules or a cloud firewall.- Amazon S3 returns
403 AccessDeniedfor objects that don't exist when the caller lackss3:ListBucket, to hide whether the key exists. Bucket policies, Block Public Access and CloudFront origin access settings also cause it. - CSRF protection: Django and Spring Security respond 403 when the CSRF token is missing or wrong.
- Cloudflare or another WAF blocking the client, common for scripts using default
curlorpython-requestsuser agents. - The user is logged in but lacks the role, scope or plan, e.g. an OAuth token without the required scope (
insufficient_scope).
How to debug and fix a 403
Find out who sent it
Response headers reveal the layer: server: cloudflare and cf-ray mean Cloudflare blocked it; x-amz-request-id means S3; your own request ID header means your app.
curl -si https://example.com/admin | head -n 20nginx: fix file permissions
namei -l shows the permissions of every directory on the path; one missing x bit is enough to cause a 403.
# Which user runs the nginx workers?
ps -o user= -C nginx | sort -u
# Permissions along the whole path
namei -l /var/www/site/index.html
# Typical fix
sudo chmod 755 /var/www /var/www/site
sudo chmod 644 /var/www/site/index.html
# SELinux systems: restore the default file contexts
sudo restorecon -Rv /var/www/sitenginx: missing index file
Make sure the directory has the file named in index, and send a 404 rather than a 403 when nothing matches.
server {
root /var/www/site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Express: 401 vs 403 in a permission check
No user means 401; a known user without the role means 403.
function requireRole(role) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'unauthenticated' });
if (!req.user.roles.includes(role)) {
return res.status(403).json({ error: 'forbidden', required: role });
}
next();
};
}
app.delete('/api/projects/:id', requireRole('admin'), deleteProject);Headers that matter with 403
- WWW-Authenticate
- Bearer-token APIs can send
WWW-Authenticate: Bearer error="insufficient_scope"with a 403 to say which scope is missing (RFC 6750). - Server / Via / cf-ray
- Not specific to 403, but the quickest way to find out which hop refused the request.
403 vs similar status codes
- 401 Unauthorized: Not authenticated yet. Logging in or refreshing the token can fix it.
- 404 Not Found: Use 404 instead of 403 when revealing that the resource exists is itself a leak.
- 429 Too Many Requests: The right code for rate limits. Don't use 403 to slow clients down.
SEO impact of 403
Googlebot treats a 403 like other 4xx codes: the page isn't indexed, and existing URLs drop out. A common accident is a WAF or bot-protection rule that blocks Googlebot; Search Console's URL Inspection tool shows the status Googlebot actually received. Google advises against using 403 to reduce crawling; use 429 or 503 for that.
Related status codes
401Unauthorized
The request lacks valid authentication credentials for the target resource.
404Not Found
The server has no current representation of the target resource, or won't say whether one exists.
429Too Many Requests
The client has sent too many requests in a given amount of time.
405Method Not Allowed
The method is known to the server but not supported by the target resource.
Looking for another code? Search the full HTTP status codes list, or read which code your API should return in the REST API status codes guide.
Learn more
HTTP Status Codes for REST APIs: 200 vs 201 vs 204, 400 vs 422 vs 429
A practical guide to choosing the right HTTP status code in REST API design. Covers the most confusing pairs: 200 vs 201, 400 vs 422, 401 vs 403, and when to use 503 vs 429.
REST API Design Best Practices: A Practical Guide
The conventions that make REST APIs intuitive and consistent. Covers URL design, HTTP methods, status codes, versioning, pagination, and error responses.
CORS Explained: Why It Happens and How to Fix It
CORS errors happen when a browser blocks a request to a different origin. Learn how the Same-Origin Policy works, what preflight requests are, and how to configure CORS correctly in Express, FastAPI, and Nginx.
Frequently Asked Questions
What is the difference between 403 and 404?▾
403 admits the resource exists but refuses access; 404 says there's nothing there, or that the server won't tell you. Use 404 when revealing existence is itself a leak, such as another user's private repository.
How do I fix 403 Forbidden in nginx?▾
Read /var/log/nginx/error.log. Permission denied means the nginx user can't read the file or traverse a parent directory: fix it with chmod or chown, or restorecon on SELinux. A message saying the directory index is forbidden means there's no index file for that directory: add one, or fix the index and try_files directives.
Why does S3 return 403 instead of 404 for a missing file?▾
Without the s3:ListBucket permission, S3 won't reveal whether a key exists, so missing objects return 403 AccessDenied. Grant ListBucket on the bucket to get 404 for missing keys, and check the bucket policy and CloudFront origin access configuration.