TROUBLESHOOTING
July 27, 2026

How to Fix "403 Forbidden" Errors on Nginx

6 min read
Author
CloudStick Team
Security Specialist
Share this article
How to Fix "403 Forbidden" Errors on Nginx
CloudStick
403 Forbidden

What 403 Forbidden Actually Means

A 403 means Nginx received the request, found the file or path it was looking for, and deliberately refused to hand it over. That is the key difference from a 502 or 500: the server is not crashing, timing out, or failing to connect to PHP-FPM. It is working correctly and telling you no. That framing changes where you should look first — not at process managers or upstream sockets, but at permissions, index files, and explicit deny rules.

In practice, four causes account for almost every 403 you will ever see on a production Nginx site: the files or directories are owned by the wrong system user or have permission bits Nginx/PHP-FPM cannot read; there is no index.php or index.html in a directory and autoindex is off, so Nginx has nothing to serve and no listing to fall back on; a location block somewhere in the vhost config has an explicit deny all; or PHP-FPM's open_basedir directive is scoped too narrowly and blocks the app from reading a path it legitimately needs. Everything below works through those four in the order you should actually check them.

Read the Nginx Error Log First

The error log tells you exactly which of the four causes you are dealing with — do not guess before checking it. Two log lines look similar but point to completely different fixes:

tail -n 50 /home/<username>/logs/<sitename>/nginx-cs/error.log
# "directory index of \"/home/.../public\" is forbidden"
# -> missing index file + autoindex off
# "13: Permission denied) ... while reading upstream"
# -> ownership/permission problem on disk or PHP-FPM socket

On a CloudStick server you rarely need to SSH in to find this. The Server panel's Web Application Logs section streams the per-site Nginx access and error log directly in the dashboard, so you can spot the exact "forbidden" reason without opening a terminal. If the log says "Permission denied," skip straight to the ownership section below. If it says "directory index ... is forbidden," go straight to the missing index section instead — permissions are fine in that case.

Fix File Ownership and Permissions

Ownership problems are the single most common cause of 403s, and the fix is a recursive chown back to the correct site user, not a permissions free-for-all. Nginx and PHP-FPM run as a specific system user — on a CloudStick server that is the dedicated per-site user (something like cpuser followed by a random string), not root and not your own SSH login. If files got uploaded via SFTP as root, restored from a backup with the wrong owner, or extracted from a zip that preserved someone else's UID, the web server process literally cannot read them, and Nginx returns 403 rather than crash.

ls -la /home/cpuser4f21a/apps/example.com/public/
# check the owner:group column against the site user
chown -R cpuser4f21a:cpuser4f21a /home/cpuser4f21a/apps/example.com/
find /home/cpuser4f21a/apps/example.com/ -type d -exec chmod 755 {} \;
find /home/cpuser4f21a/apps/example.com/ -type f -exec chmod 644 {} \;
PREREQUISITE

Confirm the exact site username before running chown. You can find it from the site's Nginx vhost config at /etc/nginx-cs/vhosts.d/<site>.conf (look at the root and fastcgi_pass paths), or from the CloudStick dashboard under the site's overview panel. Running chown with the wrong user just moves the problem, it does not fix it.

Directories need the execute bit (755) so the web server can traverse into them; files only need to be readable (644). Resist the temptation to chmod 777 anything — that "fixes" the 403 by making the path world-writable too, which turns a permissions bug into a real security hole the moment an attacker finds an upload endpoint. If you would rather not touch a terminal at all, CloudStick's Advanced File Manager lets you fix ownership and permission bits visually from the dashboard — select the folder, apply recursive permissions, and confirm the owning user, all without an SSH session.

Missing Index Files and Leftover Deny Rules

If the error log said "directory index ... is forbidden," permissions are not your problem — Nginx found the directory, has no index.php or index.html to serve inside it, and autoindex is off, so it has nothing left to do but refuse. This happens constantly after a deploy that fails partway through, a build step that writes output to the wrong folder, or an app scaffold that expects an index file at a path that does not exist yet.

ls /home/cpuser4f21a/apps/example.com/public/
# no index.php or index.html present? that's the cause
grep -n "deny\|location" /etc/nginx-cs/vhosts.d/example.com.conf
# look for a stray "deny all;" left in a location block

The other common flavor is a hardening pass that added an explicit deny rule and never got cleaned up. It is normal and correct to have deny all; inside a location block that blocks access to .git, .env, or wp-config.php backups — but if that block's regex is too broad, it can accidentally swallow legitimate paths like an uploads directory or an API route. Open the site's config at /etc/nginx-cs/vhosts.d/<site>.conf and read every location block that has a deny directive; if the path being blocked overlaps with something the app actually needs to serve, narrow the regex rather than deleting the rule outright — the security intent behind it was probably correct.

Open_basedir Restrictions in PHP-FPM

When Nginx itself is fine but PHP still returns a forbidden-style error for specific requests, an overly strict open_basedir in the PHP-FPM pool config is usually the culprit. open_basedir restricts which directories a PHP script is allowed to touch, and it is a genuinely good security control — but if it is scoped to only the site's own document root, any app that needs to read a shared uploads path, a cache directory outside the webroot, or a symlinked plugin folder will get blocked, and depending on how the app surfaces the error, that can look identical to a permissions-driven 403.

grep open_basedir /etc/php83cs/fpm-pools.d/example.com.conf
# php_admin_value[open_basedir] = /home/cpuser4f21a/apps/example.com/:/tmp/
# add the missing path, then reload the pool
systemctl reload php83cs-fpm

Add the exact path the app needs — a shared media directory, a session storage path, a cache root — rather than widening open_basedir to something broad like the whole /home directory. After editing the pool file at /etc/php83cs/fpm-pools.d/<sitename>.conf, reload the matching PHP-FPM service so the new restriction takes effect against the socket at /run/<sitename>.sock. If you manage PHP versions and extensions through CloudStick's EasyPHP tool, this is also a good moment to confirm the site is actually running the pool you think it is — EasyPHP shows the active version per site at a glance.

Preventing Future 403 Errors

Most repeat 403s trace back to deploy scripts that do not preserve ownership, or backup restores that extract files as root. Standardize your deploy and restore process to always run a chown pass back to the site user as the last step, not an afterthought you remember only after the site breaks. If you use CloudStick's Git deployment feature, the pulled files are already written as the correct site user automatically, which removes this failure mode for anything deployed that way.

As a quick recap: check the error log to know which of the four causes you are looking at, verify ownership and permissions with ls -la before touching chmod, confirm the app actually has an index file to serve, scan the vhost config for stray deny rules, and check open_basedir if PHP itself is the one refusing. If a site gets stuck in a broken state that a config fix cannot untangle, CloudStick's Website Rebuild feature gives you a clean way to reset the site's file structure and permissions from scratch rather than debugging indefinitely.

Leave a comment
Full Name
Email Address
Message
Contents