Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 9 min read Last reviewed Jul 2026

Nginx Error: 'open() '/var/www/html/index.html' failed (13: Permission denied)' — Cause, Fix, and Troubleshooting Guide

Quick answer

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
Free toolkit

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 Forbidden for pages whose files clearly exist on disk.
  • error.log shows open() "..." failed (13: Permission denied) with a real, present path.
  • The same file opens fine when you cat it as root but 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/ returns HTTP/1.1 403 Forbidden with 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) or nginx (RHEL/CentOS), not root. If the file is mode 600/640 owned 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 needs x on /, /var, /var/www, and /var/www/html. A directory set to 700 or 750 without 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_t type. Files copied from /root or a home dir often inherit user_home_t or admin_home_t and are denied.
  • A restrictive umask or bad deploy step. A CI job or rsync running with umask 077 writes files as 600, 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

  • 13 means the file exists but is unreadable; 2 means it is missing. Read the errno before you start chmod-ing — chasing the wrong one wastes time.
  • Never chmod 777 to “just make it work.” World-writable web roots are a serious security hole. 755 dirs / 644 files with correct ownership is the right answer.
  • Traverse permission is per directory in the whole path. A world-readable file under a 700 parent still fails; always check the full chain with namei -l.
  • SELinux denials look identical to Unix permission errors in NGINX’s log. Always check getenforce and ausearch on RHEL-family systems before assuming Unix modes are the culprit.
  • Deploy tooling can silently reset permissions. Pin umask 022 in CI and confirm ownership after every deploy; alert on 403 spikes and on (13: Permission denied) appearing in error.log via your monitoring dashboard.
  • Restart, don’t just reload, when you change the user directive, or workers keep the old identity.

See the NGINX category for more guides.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.