DEVESSENTIALS

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.

4xx · Client errorRFC 9110 §15.5.4

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 (often www-data or nginx) needs read permission on the file and execute permission on every parent directory.
  • A directory with no index file and autoindex off: nginx logs directory 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/allow rules, an IP allowlist, Apache .htaccess rules or a cloud firewall.
  • Amazon S3 returns 403 AccessDenied for objects that don't exist when the caller lacks s3: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 curl or python-requests user 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 20

nginx: 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/site

nginx: 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

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

Browse all developer guides →

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.