Blog
3 min read

403 Forbidden Error: What It Means and How to Fix It

A 403 Forbidden means the server understood your request and refuses to allow it. The common causes — file permissions and missing index files in Nginx, wrong API keys or roles, private S3 buckets, CSRF and CORS confusion, Cloudflare and WAF blocks — and how to tell which one you've hit.

403 Forbidden means: "I know what you're asking for, and you're not allowed to have it." The server received the request fine and deliberately said no.

That's different from its neighbours:

Code Meaning
401 Unauthorized "I don't know who you are" — log in or send credentials
403 Forbidden "I know who you are (or don't care), and the answer is no"
404 Not Found "There's nothing here" — some servers return 404 instead of 403 to hide that a resource exists

(401 vs 403)

First: who sent the 403?

Check the response before guessing. Open your browser's DevTools → Network tab, click the failed request, and look at the response body and headers. (Browser Developer Tools for Beginners)

  • A page saying nginx or Apache → your web server.
  • A Cloudflare page with a Ray ID → Cloudflare's firewall or bot protection.
  • JSON like {"error":"forbidden"} → your app or an API.
  • XML with AccessDenied → an S3-style storage bucket.

Cause 1: Nginx or Apache can't read the files

The web server process (often the www-data user) needs permission to read your files and to enter every folder above them.

ls -la /var/www/myapp
namei -l /var/www/myapp/index.html

namei -l shows the permissions of each folder on the path. A home directory set to 700 blocks the web server even if the files themselves are readable. (Linux File Permissions)

Cause 2: No index file, directory listing off

Visit https://site.com/ and there's no index.html in the root, and directory listing is (correctly) disabled → 403. Make sure your build output actually landed in the folder Nginx serves. For single-page apps, check your try_files line. (Nginx Location Blocks and try_files)

Cause 3: Your app's own permission checks

If the 403 comes from your API, your code decided this user can't do this. Usual suspects:

Cause 4: The API key doesn't allow it

Third-party APIs return 403 when your key is valid but lacks permission: the wrong scope, a feature not on your plan, a key restricted to certain domains or IPs, or an account that's been suspended. Read the error message — APIs usually say which.

Cause 5: Private storage buckets

S3, R2 and similar return 403 (AccessDenied) when a file isn't public and the request isn't signed. Either make that path public deliberately, or use presigned URLs. (Presigned URLs for Direct Uploads)

Cause 6: Cloudflare or a firewall blocked you

Cloudflare's WAF, bot protection or a country block can return 403 before the request reaches your server. Check Security → Events in Cloudflare for the Ray ID shown on the page. (What Is Cloudflare?)

What about CORS?

A CORS error isn't a 403 — it's the browser refusing to hand your code a response. But the server can also answer a preflight with 403. If the console says "CORS", start there. (CORS Errors Explained)

The summary

  • 403 = the server understood and refused.
  • Find out who refused: web server, Cloudflare, your app, an API or a bucket.
  • Then fix the matching cause: file permissions, index file, roles, API scopes, bucket policy or firewall rule.

EasySpawn sets up the web server, permissions and HTTPS for your app on its own server, and Claude Code can read the logs to tell you exactly which layer sent the 403. See how it works or join the waitlist.

Related: HTTP Status Codes Explained · 500 Internal Server Error · Why App Works Locally but Not in Production · What Is Nginx?

Keep reading