NGINX Error Guide: '403 Forbidden' permissions, index, and SELinux
Fix NGINX 403 Forbidden errors: diagnose file permissions, missing directory index, autoindex, deny rules, wrong root path, and SELinux httpd_t context denials.
- #nginx
- #troubleshooting
- #errors
- #permissions
Overview
A 403 Forbidden means NGINX found the request valid but refused to serve it: the worker process either cannot read the file/directory, was told to deny the request, or was asked to list a directory with no index and autoindex off. Unlike a 404 (not found), a 403 means the resource exists or the path is reachable — NGINX is choosing not to return it.
The error log names the precise reason:
2026/06/23 19:14:02 [error] 8101#8101: *60233 open() "/var/www/app/index.html" failed (13: Permission denied), client: 203.0.113.31, server: app.example.com, request: "GET / HTTP/1.1", host: "app.example.com"
2026/06/23 19:14:30 [error] 8101#8101: *60240 directory index of "/var/www/app/" is forbidden, client: 203.0.113.31, request: "GET / HTTP/1.1"
13: Permission denied = filesystem or SELinux. directory index ... is forbidden = no index file and autoindex off. access forbidden by rule = a deny directive. Each has a different fix.
Symptoms
- Browser shows “403 Forbidden” and the default NGINX page.
- error.log records
Permission denied,directory index ... is forbidden, oraccess forbidden by rule. - Some paths 403 while others serve; or the whole site 403s after a content move.
- Files exist and are readable by you, but NGINX (running as
www-data/nginx) cannot read them.
curl -sI https://app.example.com/ | head -1
HTTP/1.1 403 Forbidden
sudo tail -10 /var/log/nginx/error.log
[error] 8101#8101: *60233 open() "/var/www/app/index.html" failed (13: Permission denied)
Common Root Causes
1. File or directory permissions block the worker
NGINX workers run as www-data (Debian) or nginx (RHEL). If the file is not readable, or a parent directory is not executable (searchable) by that user, you get 13: Permission denied.
sudo -u www-data stat /var/www/app/index.html
namei -l /var/www/app/index.html
drwxr-x--- root root /var/www/app
-rw-r----- root root index.html
[error] 8101#8101: *60233 open() "/var/www/app/index.html" failed (13: Permission denied)
www-data is neither owner nor in the group, and the directory lacks o+x, so the worker cannot traverse/read it.
2. Missing directory index with autoindex off
A request for a directory has no index file and autoindex is off (the default), so NGINX refuses to list it.
grep -RnE 'index|autoindex' /etc/nginx/conf.d/app.conf
ls -la /var/www/app/
index index.html index.htm;
total 8
drwxr-xr-x app.js styles.css # no index.html
[error] 8101#8101: *60240 directory index of "/var/www/app/" is forbidden
No index.html exists and autoindex is off, so the directory request is forbidden.
3. Wrong root or alias path
root/alias points at a directory that does not contain the expected files (or doubles a path segment), so NGINX lands somewhere it cannot serve.
grep -RnE 'root|alias' /etc/nginx/conf.d/app.conf
location /static/ {
alias /var/www/app/static; # missing trailing slash -> /static path mangling
}
[error] 8101#8101: *60255 open() "/var/www/app/staticindex.html" failed (2: No such file or directory)
A misconfigured alias (missing trailing slash) mangles the path; depending on permissions this surfaces as 403 or 404. Fix the root/alias value.
4. An explicit deny rule
A deny directive (IP allowlist, or blocking dotfiles/sensitive paths) is rejecting the request by design.
grep -RnE 'deny|allow' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
location ~ /\.(?!well-known) {
deny all;
}
allow 10.0.0.0/8;
deny all;
[error] 8101#8101: *60270 access forbidden by rule, client: 203.0.113.31, request: "GET /.env HTTP/1.1"
access forbidden by rule means a deny matched. Verify the client/path should actually be blocked — often this is correct.
5. SELinux context wrong on the web root
On RHEL-family hosts, files served by NGINX must have the httpd_sys_content_t SELinux type. Content copied from /home or /tmp keeps the wrong label and is denied even with correct Unix permissions.
ls -Z /var/www/app/index.html
sudo ausearch -m avc -ts recent | grep nginx
unconfined_u:object_r:user_home_t:s0 /var/www/app/index.html
type=AVC msg=audit(...): avc: denied { read } for pid=8101 comm="nginx" name="index.html" scontext=system_u:system_r:httpd_t:s0 tcontext=...:user_home_t:s0
[error] 8101#8101: *60233 open() "/var/www/app/index.html" failed (13: Permission denied)
The file is labeled user_home_t, not httpd_sys_content_t; SELinux denies the read. Restore the context.
6. NGINX user mismatch (config vs reality)
Workers run as a user that does not match the file ownership scheme because user in nginx.conf was changed or differs from expectations.
grep -E '^\s*user' /etc/nginx/nginx.conf
ps -o user= -C nginx | sort -u
user nginx;
www-data
nginx
[error] 8101#8101: *60233 open() ... failed (13: Permission denied)
If the configured worker user does not own/group the files, every read is denied. Align ownership or the user directive.
Diagnostic Workflow
Step 1: Read the exact 403 reason
sudo tail -20 /var/log/nginx/error.log
Permission denied -> permissions/SELinux; directory index ... is forbidden -> index/autoindex; access forbidden by rule -> a deny directive. This selects the branch.
Step 2: Find the worker user and the served path
grep -E '^\s*user' /etc/nginx/nginx.conf
ps -o user=,comm= -C nginx | sort -u
grep -RnE 'root|alias|index' /etc/nginx/conf.d/app.conf
Note the user the workers run as and the actual root/alias for the failing location.
Step 3: Test read access as the worker user
sudo -u www-data cat /var/www/app/index.html >/dev/null && echo OK || echo DENIED
namei -l /var/www/app/index.html
namei -l shows every path component’s permissions — a single non-searchable parent directory (drwx------) breaks the whole chain.
Step 4: For a directory 403, check for an index
ls -la /var/www/app/
grep -RnE 'index|autoindex' /etc/nginx/conf.d/app.conf
Either add the expected index.html, set the correct index directive, or enable autoindex on; if a listing is intended.
Step 5: On RHEL, check SELinux; then fix and reload
ls -Z /var/www/app/
sudo ausearch -m avc -ts recent | grep nginx
Restore the context if mislabeled, fix permissions, validate, and reload:
sudo chown -R www-data:www-data /var/www/app # or align with the worker user
sudo restorecon -Rv /var/www/app # RHEL family
sudo nginx -t && sudo systemctl reload nginx
Example Root Cause Analysis
After deploying a new site, https://app.example.com/ returns 403 for everyone, though the files are clearly present in /var/www/app/.
The error log:
[error] 8101#8101: *60233 open() "/var/www/app/index.html" failed (13: Permission denied)
Checking the path as the worker user:
sudo -u www-data cat /var/www/app/index.html >/dev/null && echo OK || echo DENIED
namei -l /var/www/app/index.html
DENIED
drwx------ deploy deploy /var/www/app
-rw-r--r-- deploy deploy index.html
The files themselves are world-readable, but the /var/www/app directory is drwx------ owned by deploy — www-data cannot traverse into it, so every open fails with permission denied.
Fix: make the directory traversable/readable by the worker user, validate, and reload:
sudo chmod 755 /var/www/app
sudo chown -R deploy:www-data /var/www/app
sudo find /var/www/app -type d -exec chmod 755 {} \;
sudo nginx -t
sudo systemctl reload nginx
The site now serves index.html with 200.
Prevention Best Practices
- Set web-root ownership and modes deliberately: directories
755, files644, owned by the deploy user with the NGINX worker user in the group. A single non-searchable parent directory 403s the whole tree. - Test new content as the worker user (
sudo -u www-data cat <file>) in your deploy step, so a permission problem fails the release instead of users. - Always ship the expected
indexfile (or setautoindex on;intentionally) — never rely on a directory listing you did not mean to expose. - On RHEL-family hosts, run
restorecon -Rvafter moving content into the web root so SELinux labels stayhttpd_sys_content_t. - Keep
deny/allowrules in version control and review them; an over-broaddeny allis a frequent cause of “it 403s for everyone”. - For fast triage of a 403 wave, the free incident assistant can map the error-log reason to permissions, index, or SELinux. More fixes live in the NGINX guides.
Quick Command Reference
# Read the exact 403 reason
sudo tail -20 /var/log/nginx/error.log
# Worker user + served path
grep -E '^\s*user' /etc/nginx/nginx.conf
ps -o user=,comm= -C nginx | sort -u
grep -RnE 'root|alias|index|autoindex|deny|allow' /etc/nginx/conf.d/app.conf
# Test read as the worker user
sudo -u www-data cat /var/www/app/index.html >/dev/null && echo OK || echo DENIED
namei -l /var/www/app/index.html
# SELinux (RHEL family)
ls -Z /var/www/app/
sudo ausearch -m avc -ts recent | grep nginx
sudo restorecon -Rv /var/www/app
# Fix permissions, validate, reload
sudo chown -R deploy:www-data /var/www/app
sudo find /var/www/app -type d -exec chmod 755 {} \;
sudo nginx -t && sudo systemctl reload nginx
Conclusion
A 403 Forbidden is NGINX refusing to serve something it could reach. The usual root causes:
- File/directory permissions block the worker user (
13: Permission denied). - A directory request with no index file and
autoindex off. - A wrong
root/aliaspath landing NGINX in the wrong place. - An explicit
denyrule (access forbidden by rule). - A wrong SELinux context on the web root (RHEL family).
- The configured worker
usernot matching file ownership.
Read the error-log reason first, then test access as the worker user, fix permissions/SELinux/index as indicated, and nginx -t before reloading. Most production 403s are a non-searchable parent directory or a mislabeled SELinux context.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.