Nginx

How to Fix "Nginx 403 Forbidden" Error on Static File Hosting

4 min read by DebuggedIt

Quick answer

Nginx returns a 403 error for a static file you're sure exists and should be publicly accessible. Unlike a 404, this means Nginx found something at that path...

Nginx returns a 403 error for a static file you're sure exists and should be publicly accessible. Unlike a 404, this means Nginx found something at that path but actively refused to serve it β€” almost always a file permission problem, or a directory listing being blocked with no index file to fall back to.

The Problem

Requesting a file or directory that should work instead returns a forbidden error:

$ curl -i https://yourapp.com/images/logo.png
HTTP/1.1 403 Forbidden

The Nginx error log usually names the specific permission problem directly:

2026/08/07 17:02:11 [error] 8821#8821: *401 open() "/var/www/yourapp/images/logo.png"
failed (13: Permission denied), client: 203.0.113.5, server: yourapp.com,
request: "GET /images/logo.png HTTP/1.1"

It also shows up when browsing to a directory that has no index file and directory listing is disabled:

2026/08/07 17:03:40 [error] 8821#8821: *402 directory index of "/var/www/yourapp/uploads/" is forbidden

Why It Happens

Nginx's worker processes run as a specific, usually unprivileged, system user (commonly www-data or nginx), and that user needs read (and for directories, execute) permission on every file and directory in the path leading to the requested resource. A 403 fires when:

  • File permissions don't grant read access to the Nginx worker's user, often because the file was uploaded or created by a different user (like root during a deploy) without adjusting ownership afterward.
  • Missing execute permission on a parent directory β€” Linux requires execute permission on every directory in a path to traverse into it, not just read permission on the final file, so a single restrictively-permissioned parent directory blocks everything beneath it.
  • No index file and autoindex off β€” requesting a directory URL directly (like /uploads/) with no index.html inside and directory listing disabled (the safe default) produces exactly this error.
  • SELinux context issues on RHEL/CentOS-based systems, where file permissions look correct but SELinux's own security context separately blocks Nginx from accessing the file.

The Fix

Check the actual ownership and permissions on the file in question:

ls -la /var/www/yourapp/images/logo.png
-rw------- 1 root root 48213 Aug  7 12:00 logo.png

Confirm which user Nginx's worker processes actually run as:

ps aux | grep nginx
www-data  8821  0.0  0.1  nginx: worker process

Fix ownership so the Nginx user (or a group it belongs to) can read the file:

sudo chown -R www-data:www-data /var/www/yourapp/images
sudo find /var/www/yourapp -type d -exec chmod 755 {} \;
sudo find /var/www/yourapp -type f -exec chmod 644 {} \;

Check every directory in the path leading to the file for missing execute permission, since a single blocking directory anywhere along the way is enough:

namei -l /var/www/yourapp/images/logo.png

This prints the permissions of every path segment, making it easy to spot exactly which directory is the actual blocker. If the 403 is happening on a directory URL rather than a specific file, either add an index file or intentionally enable listing if that's the desired behavior:

location /uploads/ {
    autoindex on; # only enable this if directory listing is actually intended
}

On SELinux-enabled systems, check and fix the security context separately from standard Unix permissions:

ls -Z /var/www/yourapp/images/logo.png
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/yourapp(/.*)?"
sudo restorecon -Rv /var/www/yourapp

Still Not Working?

If ownership and permissions all look correct but the 403 persists, check for an explicit deny rule in your Nginx configuration itself that's blocking the request independently of filesystem permissions β€” a leftover security rule from a template or a previous configuration change can silently block legitimate requests:

grep -rn "deny\|allow" /etc/nginx/sites-enabled/

A rule like deny all; inside a location block that unintentionally matches your static file path will produce this exact error regardless of how correctly the underlying file permissions are set, and the fix is adjusting or removing that rule rather than continuing to chase filesystem permissions that were never actually the problem.