Nginx "403 Forbidden": read the error log, then fix the index, permissions or SELinux
Fix Nginx "403 Forbidden": the error log says if it's a missing index file, wrong permissions on a parent folder, a deny rule or SELinux. namei checks the whole path.
by OhSudo ··
Not sure this is your cause?Find it step by step: Website down →
Part of a learning path:Web servers: nginx and HTTPS in production
403 Forbiddennginx/1.24.0Nginx found what you asked for, or at least the folder, and it won’t serve it. The browser never gets the reason, but the error log does, and it comes down to a few causes that each have their own line.
- nginx workerruns as www-data or nginxwalks
- Every folder on the pathmust be enterable, the file readablerefused(fails)
- The fileno index, no access, SELinux, a deny rule(breaks here)
TL;DR
sudo tail -n 5 /var/log/nginx/error.lognamei -l /var/www/mysite/index.html # every folder in the pathps -o user= -C nginx | sort -u # who nginx runs as| The log says | Cause |
|---|---|
directory index of "…" is forbidden |
No index file (section 1) |
open() "…" failed (13: Permission denied) |
Permissions or SELinux (sections 2–3) |
access forbidden by rule |
A deny in your config (section 4) |
1. No index file
[error] 2143#2143: *7 directory index of "/var/www/mysite/" is forbidden, client: 203.0.113.7, server: example.com, request: "GET / HTTP/1.1", host: "example.com"Someone asked for a folder, and there’s no index.html. Nginx won’t list a folder by default, so it answers 403 rather than 404. See what’s there:
ls -la /var/www/mysite/sudo nginx -T 2>/dev/null | grep -E 'root |index 'The usual reasons:
- The wrong
root. Your files are in/var/www/mysite/dist/, orrootpoints somewhere else. Nginx doesn’t look in subfolders. - The build’s output hasn’t been deployed yet, so the folder is empty.
indexdoesn’t name your file. PHP sites needindex index.php index.html;.- A single-page app’s route, such as
/dashboard/, needs atry_filesfallback:
location / { try_files $uri $uri/ /index.html;}The $uri/ part is what’s checked for a folder, and with no index there, it gives this 403. The Nginx Config Generator sets this up for static sites.
Listing the folder is what you actually want? Then use autoindex on;, only for a folder with nothing private in it.
2. Permissions, on the whole path
[error] 2143#2143: *9 open() "/home/alex/site/index.html" failed (13: Permission denied), client: 203.0.113.7, server: example.com, request: "GET / HTTP/1.1"Nginx’s workers run as a low-privileged user, www-data on Debian and Ubuntu, and nginx on RHEL. It needs read on the file, and execute on every folder above it. That last part is the one people miss, and namei shows the whole chain:
namei -l /home/alex/site/index.htmlf: /home/alex/site/index.htmldr-xr-xr-x root root /drwxr-xr-x root root homedrwxr-x--- alex alex alex <-- nginx can't get past thisdrwxr-xr-x alex alex site-rw-r--r-- alex alex index.htmldrwxr-x--- on a home folder is the default on recent Ubuntu, so that’s a classic. To test as that user, without guessing:
sudo -u www-data cat /home/alex/site/index.htmlsudo -u www-data test -x /home/alex && echo okThe best fix is to move the site out of your home folder:
sudo mkdir -p /var/www/mysitesudo rsync -a --delete /home/alex/site/ /var/www/mysite/sudo chown -R root:www-data /var/www/mysitesudo find /var/www/mysite -type d -exec chmod 750 {} +sudo find /var/www/mysite -type f -exec chmod 640 {} +Folders need x, and files don’t, so they get different modes. The chmod Calculator explains these numbers. If it has to stay where it is, give only that access with an ACL, instead of opening your home folder to everybody:
sudo setfacl -m u:www-data:x /home/alex3. SELinux (RHEL, Rocky, AlmaLinux)
Permissions look right, sudo -u nginx cat works, and it’s still 13: Permission denied. That’s SELinux, and a file copied or moved from elsewhere keeps its old label:
ls -Z /var/www/mysite/index.htmlsudo ausearch -m avc -ts recent | tailunconfined_u:object_r:user_home_t:s0 /var/www/mysite/index.htmluser_home_t isn’t something Nginx may read, and web content should be httpd_sys_content_t:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/mysite(/.*)?"sudo restorecon -Rv /var/www/mysiteThat covers the usual location, /var/www, and any folder elsewhere with the semanage line.
4. A deny rule
[error] 2143#2143: *11 access forbidden by rule, client: 203.0.113.7, server: example.com, request: "GET /admin/ HTTP/1.1"Your own configuration refuses this client. Find the rule:
sudo nginx -T 2>/dev/null | grep -nE 'deny|allow|auth_'Rules are read in order, and the first match wins. Is your address what you think it is? Behind a proxy or Cloudflare, Nginx sees the proxy’s address, unless you’ve set real_ip_header and set_real_ip_from.
A rule such as location ~ /\. { deny all; } is good, as it protects .env and .git. It also blocks .well-known, which matters for Let’s Encrypt: see Certbot failed to authenticate.
5. No line in the log?
Then Nginx may be logging somewhere else, or maybe this Nginx didn’t answer at all:
sudo nginx -T 2>/dev/null | grep -E 'error_log|access_log'curl -sI https://example.com/ | grep -iE '^(HTTP|server)'A 403 from your application, through proxy_pass, has its own page, and Nginx just passes it on, with nothing in its error log. Cloudflare or a web application firewall gives its own 403 too, as the Server header or the page itself shows. And a request with limit_except is another that has its own 403.
Reload after each change, and check in a private window, since browsers cache:
sudo nginx -t && sudo systemctl reload nginxcurl -I https://example.com/Warning
Don’t chmod -R 777 your site. It lets any process on the server change your files, and it makes the permissions useless, so a hacked plugin can then rewrite your whole site. And user root; in nginx.conf makes each bug in your site root’s problem too. Fix the actual path instead.
Shell, we have a problem!
Similar, but different: 502 Bad Gateway and 504 Gateway Time-out come from the backend. 404 Not Found means the file isn’t there, and is what you get when root is wrong but a folder exists. 401 Unauthorized means a password is required. On RHEL, a 403 with correct permissions is often SELinux. Permission denied for a script is about the execute bit. To learn how permissions work, try the lesson What rwx means, and chmod. On Apache, see Apache 403 Forbidden.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.