How to fix "403 Forbidden" and "File not found" on Nginx with PHP

A 403 means Nginx found something but won't, or can't, show it. A bare "File not found." means PHP couldn't find the script Nginx asked for. The Nginx error log names the reason in both cases. Here is what each line means and how to fix it.

We tested every case in this guide on Debian 13 with Nginx 1.26 and PHP 8.4. We broke a working site one way at a time and copied the real messages. Ubuntu works the same way.

1. Read the error log

Load the broken page once, then:

sudo tail -n 20 /var/log/nginx/error.log

These are the lines we saw, and where to go next:

  • directory index of "/var/www/example/" is forbidden: Nginx doesn't know which file to show for the folder. See step 2.
  • open() "..." failed (13: Permission denied), or "..." is forbidden (13: Permission denied): Nginx isn't allowed to read the file or a folder above it. See step 3.
  • access forbidden by rule: an allow/deny line blocks it. See step 4.
  • Primary script unknown, and the page says only File not found.: PHP got the wrong file path. See step 5.
  • Unable to open primary script: ... (Permission denied), and the page says Access denied.: PHP isn't allowed to read the script. See step 3.

2. "directory index ... is forbidden"

Someone opened a folder, like /, and Nginx found none of the files on its index line. In our test, the site had index index.html; but only an index.php, so the home page gave 403. Add index.php:

server {
    root /var/www/example;
    index index.php index.html;
    ...
}

Then sudo nginx -t && sudo systemctl reload nginx. If the folder really has no index file, that's expected: Nginx doesn't list folder contents, and you don't want it to (leave autoindex off).

3. "Permission denied": check the whole path

Nginx runs as www-data. To read /home/anna/site/page.html, it needs to read the file and enter every folder on the way: /, /home, /home/anna and /home/anna/site. One folder that's closed is enough to block everything below it. This command shows the permissions of each step:

namei -l /home/anna/site/page.html

In our test, it showed the problem straight away:

drwxr-xr-x root root /
drwxr-xr-x root root home
drwx------ anna anna anna
drwxr-xr-x anna anna site
-rw-r--r-- anna anna page.html

The home folder anna was closed to everyone else (drwx------). Every file below it gave 403, and PHP pages gave 404, with stat() ... failed (13: Permission denied) in the log. Since Ubuntu 21.04, new home folders are closed to other users by default (drwxr-x---), so sites in /home often hit this.

There are two ways to fix it:

  • Best: move the site to /var/www/, which is made for this.
  • Or let others enter (but not list) the home folder: sudo chmod 711 /home/anna. Folders need the "x" permission to be entered.

A single file can also be closed. A file with -rw------- (600) gave 403 for an HTML file. For a PHP file, the page said Access denied., because PHP-FPM couldn't read it. Normal permissions are 644 for files and 755 for folders. If each site has its own PHP user, you can use 640 and 750 with the group www-data, so Nginx can read but nobody else can. Don't fix permission errors with 777: then every user and every hacked site on the server can change your files.

4. "access forbidden by rule", and a trap with folders

An allow or deny line is doing its job. If you didn't mean it, search your Nginx files for it:

sudo grep -Rn "deny" /etc/nginx/sites-enabled/ /etc/nginx/snippets/

Now the trap, which works the other way round. Say you want to close an admin folder to everyone but you:

location /admin/ {
    allow 203.0.113.10;
    deny all;
}

In our test, /admin/ gave 403, so it looks like it works. But /admin/index.php still ran for everyone. Nginx picks a regex rule, like location ~ \.php$, over a normal folder rule, so PHP files skipped the block. Add ^~, which tells Nginx to stop looking once this folder matches, and give the folder its own PHP block:

location ^~ /admin/ {
    allow 203.0.113.10;
    deny all;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    }
}

In our test, both /admin/ and /admin/index.php then gave 403 to others, and the allowed address could still use the PHP page. Always test the PHP file itself, not just the folder:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/admin/index.php

5. "File not found." and "Primary script unknown"

This bare page comes from PHP-FPM, not Nginx. Nginx sent a file path in SCRIPT_FILENAME, and no file was there. We saw it in two cases:

  • The path was wrong, for example an old folder name in a hand-written fastcgi_param SCRIPT_FILENAME line.
  • The path was missing. On Debian and Ubuntu, include fastcgi_params; doesn't set SCRIPT_FILENAME at all. Copied from old guides, it gave "File not found." on every PHP page.

The simplest fix on Debian and Ubuntu is the ready-made snippet. It sets the right path from your root line, and gives a clean 404 for PHP files that don't exist:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

Also check that root is set in the server block, not only inside location /. Otherwise the PHP block doesn't see it.

If you use your own lines instead of the snippet, the right path is:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Quick list

  • directory index ... is forbidden: add index.php to the index line.
  • 13: Permission denied: run namei -l on the file. Open the folder that's closed (711) or move the site to /var/www. Never use 777.
  • Access denied.: PHP-FPM can't read the script. Fix the file's owner or permissions.
  • access forbidden by rule: find the deny line with grep -Rn deny.
  • Protecting a folder: use location ^~ /folder/ with its own PHP block, and test a PHP file inside it.
  • File not found. / Primary script unknown: use include snippets/fastcgi-php.conf; and set root in the server block.

Official sources

Next steps: if you see 502 or 504 instead, see fixing 502 and 504 errors. To keep private files out of reach, see blocking .env, .git and backup files.

ServerNest is being built to set up each site's web server rules and permissions for you, and test them. Join early access to try it first.

← Back to the security blog