Nginx Error: 'open() '/var/www/html/index.html' failed (13: Permission denied)' — Cause, Fix, and Troubleshooting Guide
Fix open() failed (13: Permission denied) on static files: give the nginx worker read plus traverse (x) permissions and the correct SELinux context.
- #nginx
- #web-server
- #troubleshooting
Stuck on this NGINX error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
What this error means
This error means NGINX found the requested static file on disk but the worker process was not allowed to read it (or to traverse one of the parent directories on the way to it). The 13 is the POSIX errno EACCES (“permission denied”), which is distinct from 2 (ENOENT, no such file) — the file exists, the permissions are wrong. A typical runtime log line looks like this:
2026/07/12 14:07:31 [error] 2913#2913: *4471 open() "/var/www/html/index.html" failed (13: Permission denied), client: 203.0.113.44, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
The client sees an HTTP 403 Forbidden. Every request that maps to that path (or anything under an unreadable directory) fails, so a permissions mistake on a document root or a deploy directory can take an entire site offline while the files are plainly sitting on disk.
Where it surfaces
- Browsers get
403 Forbiddenfor pages whose files clearly exist on disk. error.logshowsopen() "..." failed (13: Permission denied)with a real, present path.- The same file opens fine when you
catit asrootbut fails as the NGINX worker user. - Only some paths break — typically a newly deployed directory or one owned by a specific user.
curl -I http://example.com/returnsHTTP/1.1 403 Forbiddenwith no upstream involved.- The problem persists across reloads because it is a filesystem state, not a config typo.
Identity and permission causes
- Worker user lacks read permission on the file. NGINX workers run as
www-data(Debian/Ubuntu) ornginx(RHEL/CentOS), notroot. If the file is mode600/640owned by another user, the worker cannot read it. - A parent directory lacks the execute/traverse bit. To reach
/var/www/html/index.html, the worker needsxon/,/var,/var/www, and/var/www/html. A directory set to700or750without the worker in the group blocks traversal even if the file itself is world-readable. - Content served from a user home directory. Home dirs are frequently
700(e.g./home/deploy), so/home/deploy/site/...is unreachable by the worker no matter how the inner files are set. - SELinux file context is wrong. On RHEL-family systems with SELinux enforcing, static content must carry the
httpd_sys_content_ttype. Files copied from/rootor a home dir often inherituser_home_toradmin_home_tand are denied. - A restrictive
umaskor bad deploy step. A CI job orrsyncrunning withumask 077writes files as600, so the worker cannot read freshly deployed assets.
Tracing the failed authorization
First confirm the config is valid and the request really maps to the path you think it does:
sudo nginx -t
sudo nginx -T | grep -E "root|user|index"
Identify the user NGINX workers actually run as, then inspect the file and its full parent chain:
# Which user do the workers run as?
ps -o user= -C nginx | sort -u
# Permissions on the file itself
ls -l /var/www/html/index.html
# Traverse the whole path — every parent needs the execute (x) bit
namei -l /var/www/html/index.html
namei -l is the fastest way to spot a parent directory missing x. Then prove the point by reading the file as the worker user — if this fails, NGINX will fail identically:
sudo -u www-data cat /var/www/html/index.html # Debian/Ubuntu
sudo -u nginx cat /var/www/html/index.html # RHEL/CentOS
On RHEL-family hosts, check SELinux — a 13 with correct Unix permissions is almost always a context problem:
getenforce
ls -Z /var/www/html/index.html
sudo ausearch -m avc -ts recent | grep nginx
An AVC denial mentioning name_connect or read on user_home_t/default_t confirms SELinux, not Unix permissions, is blocking the read.
Resolution
1. Fix ownership and read/traverse permissions. Give the worker user read on files and x (traverse) on every directory. The standard, safe layout for a document root:
# Own the tree by the site owner, group the worker can read through
sudo chown -R deploy:www-data /var/www/html
# Directories: rwx for owner, r-x for group/other (traverse + list)
sudo find /var/www/html -type d -exec chmod 755 {} \;
# Files: rw for owner, r for group/other
sudo find /var/www/html -type f -exec chmod 644 {} \;
If content lives under a 700 home directory, either add the traverse bit to the parent (chmod o+x /home/deploy) or, cleaner, move the document root out of the home directory entirely to /var/www or /srv.
2. Match the user directive to the file owner group. Confirm the worker user in the main context is the one you granted access to:
# /etc/nginx/nginx.conf (main context)
user www-data;
3. Restore the SELinux context (RHEL/CentOS/Fedora). Label the content as web content so the httpd_t domain may read it:
# Persistently set the type for the tree, then apply it
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?"
sudo restorecon -Rv /var/www/html
Avoid chcon for anything permanent — restorecon and a logrotate/relabel will undo it. Use semanage fcontext so the label survives.
These are filesystem/label changes, not config edits, but validate and reload NGINX afterward so any accompanying config change (a corrected user or root) takes effect:
sudo nginx -t && sudo systemctl reload nginx
Note: if you changed the user directive itself, a reload is not enough — the master sets the worker identity at start, so run sudo systemctl restart nginx.
Hardening access
13means the file exists but is unreadable;2means it is missing. Read the errno before you startchmod-ing — chasing the wrong one wastes time.- Never
chmod 777to “just make it work.” World-writable web roots are a serious security hole.755dirs /644files with correct ownership is the right answer. - Traverse permission is per directory in the whole path. A world-readable file under a
700parent still fails; always check the full chain withnamei -l. - SELinux denials look identical to Unix permission errors in NGINX’s log. Always check
getenforceandausearchon RHEL-family systems before assuming Unix modes are the culprit. - Deploy tooling can silently reset permissions. Pin
umask 022in CI and confirm ownership after every deploy; alert on403spikes and on(13: Permission denied)appearing inerror.logvia your monitoring dashboard. - Restart, don’t just reload, when you change the
userdirective, or workers keep the old identity.
Related identity errors
- Nginx Error: Open Failed No Such File Static — the
(2: No such file or directory)sibling, where the path is wrong or missing rather than unreadable. - Nginx Error: 403 Forbidden — the HTTP status this
open()failure returns to the client, with the other403triggers. - Nginx Error: Permission Denied Connecting Upstream Selinux — the same
(13)errno, but on an upstream socket and driven by SELinux booleans rather than file modes.
See the NGINX category for more guides.
Fixed it? Get 500 NGINX & DevOps AI prompts — free
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.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
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.