403 Forbidden means Nginx understood the request but refuses to serve it. Unlike a 404, the path usually exists — Nginx just isn’t allowed to (or configured to) show it. The error log almost always tells you which of these causes it is.
Step 1: read the error log
sudo tail -n 20 /var/log/nginx/error.log
Typical lines:
directory index of "/var/www/example.com/" is forbidden
open() "/var/www/example.com/index.html" failed (13: Permission denied)
access forbidden by rule, client: 198.51.100.7
Each maps to a cause below.
Cause 1: no index file
directory index of "…" is forbidden — someone requested a directory, there’s no index file, and directory listing is off (the safe default).
Fix: make sure the file exists and is listed in index:
index index.html index.htm index.php;
ls -la /var/www/example.com/
For a PHP app, index.php must be in the index directive and PHP must be configured; otherwise Nginx won’t use it.
Cause 2: the root path is wrong
The root points somewhere other than where your files are — for example /var/www/example.com while files are in /var/www/example.com/public (Laravel, Symfony) or /var/www/example.com/dist (front-end builds). Check:
sudo nginx -T | grep -n "root\|server_name"
Cause 3: file permissions
(13: Permission denied) — the Nginx worker user (www-data on Debian/Ubuntu, nginx on RHEL-family) can’t read the file. It needs:
- read (
r) on the file, and - execute (
x) on every directory in the path, from/down.
Check the whole path at once:
namei -l /var/www/example.com/index.html
f: /var/www/example.com/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwx------ alice alice example.com ← others have no x: Nginx can't enter
-rw-r--r-- alice alice index.html
Fix with standard permissions:
sudo find /var/www/example.com -type d -exec chmod 755 {} +
sudo find /var/www/example.com -type f -exec chmod 644 {} +
Or give the web server group access while keeping others out:
sudo chown -R alice:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} +
sudo find /var/www/example.com -type f -exec chmod 640 {} +
Files in a home directory
Serving from /home/alice/site often fails because home directories are 750 or 700 on modern distributions. Move the site to /var/www — better than loosening home directory permissions.
Cause 4: a deny rule
access forbidden by rule — an allow/deny directive blocked the request:
location /admin {
allow 203.0.113.0/24;
deny all;
}
location ~ /\. { deny all; } # hidden files
Check whether the request matches a rule you didn’t expect, for example a hidden-file rule catching /.well-known/ (needed for Let’s Encrypt). Exclude it explicitly:
location ~ /\.(?!well-known) { deny all; }
Behind a proxy or CDN, allow rules see the proxy’s IP unless you configure the real-IP module — see real IP behind Cloudflare.
Cause 5: SELinux (AlmaLinux, Rocky, Fedora)
Permissions look perfect, but you still get Permission denied? SELinux may block Nginx from reading files without the right label — common when files were moved (rather than copied) from a home directory or placed outside /usr/share/nginx or /var/www.
ls -Z /var/www/example.com
sudo ausearch -m avc -ts recent | grep nginx
Fix the labels:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
sudo restorecon -Rv /srv/www
For directories the app writes to, use httpd_sys_rw_content_t. Don’t disable SELinux to fix this.
Cause 6: autoindex disabled on purpose
If you want a directory listing (a downloads folder, say):
location /downloads/ {
autoindex on;
}
Only enable it where you intend to — directory listings can expose files you didn’t mean to publish.
Cause 7: PHP or the application returns 403
If the 403 page isn’t Nginx’s plain page, the application sent it: a WordPress security plugin, Laravel middleware, a WAF such as ModSecurity, or Cloudflare. Check the app’s logs or the plugin’s block log. A Cloudflare-branded 403 comes from a firewall rule or bot setting in your Cloudflare dashboard.
Quick checklist
- Error log message?
rootcorrect, index file present and listed?namei -l—xon every directory,ron the file for the Nginx user?- Any
denyrules matching? - SELinux denials?
- Is it the app, a WAF or a CDN rather than Nginx?
After fixing, sudo nginx -t && sudo systemctl reload nginx and test with curl -I https://example.com/. Related: Nginx 502 Bad Gateway and Nginx server blocks.