Skip to content

Fix: Nginx 403 Forbidden – Permission Denied or Directory Index Disabled

FixDevs ·

Quick Answer

How to fix the Nginx 403 Forbidden error caused by file permissions, missing index files, SELinux, or incorrect root path configuration.

The Error

You try to open a page served by Nginx and get:

403 Forbidden

In your Nginx error log (/var/log/nginx/error.log), you see one of these:

directory index of "/var/www/html/" is forbidden
open() "/var/www/html/index.html" failed (13: Permission denied)
stat() "/var/www/html/" failed (13: Permission denied)
access forbidden by rule

All of these mean Nginx received the request and found the correct server block, but it cannot serve the requested resource. The reasons range from filesystem permissions to explicit deny rules in the configuration.

Why This Happens

Unlike a 502 Bad Gateway where Nginx cannot reach a backend, a 403 means Nginx itself is trying to serve the file (or directory listing) and something is blocking it. Common causes:

  • Wrong file or directory permissions. The Nginx worker process runs as a specific user (usually www-data or nginx) and that user cannot read the files or traverse the directories in the path.
  • Directory listing is disabled. You requested a directory (like /images/) but there is no index file inside it, and autoindex is off. Nginx refuses to show the directory contents.
  • Missing index file. The index directive expects index.html or index.php, but the file does not exist in the document root.
  • SELinux is blocking access. On RHEL, CentOS, Rocky Linux, AlmaLinux, and Fedora, SELinux prevents Nginx from reading files outside of its expected context, even if Unix permissions are correct.
  • Wrong root path in the server block. The root directive points to a directory that does not exist or that Nginx cannot access.
  • Nginx is running as the wrong user. The user directive in nginx.conf specifies a user that does not have read access to the web files.
  • Symlink restrictions. The disable_symlinks directive is enabled and Nginx refuses to follow symbolic links in the document root.
  • IP-based deny rules. An allow/deny block in the configuration is explicitly blocking your IP address.

Fix 1: Fix File and Directory Permissions

This is the most common cause. Nginx needs read access to every file it serves and execute (traverse) access to every directory in the path leading to that file.

Check current permissions:

# Check permissions on the document root
ls -la /var/www/html/

# Check permissions on every directory in the path
namei -l /var/www/html/index.html

The namei command is critical here. It shows permissions for each component of the path. If any directory in the chain is not traversable by the Nginx user, you get a 403.

Check which user Nginx runs as:

grep "^user" /etc/nginx/nginx.conf
# or
ps aux | grep "nginx: worker"

On Debian/Ubuntu this is usually www-data. On RHEL/CentOS it is usually nginx.

Fix the ownership and permissions:

# Set ownership to the Nginx user
sudo chown -R www-data:www-data /var/www/html/

# Directories need 755 (read + execute for everyone, write for owner)
sudo find /var/www/html/ -type d -exec chmod 755 {} \;

# Files need 644 (read for everyone, write for owner)
sudo find /var/www/html/ -type f -exec chmod 644 {} \;

Replace www-data with nginx if you are on a RHEL-based distro.

A common pitfall: If your document root is inside a user’s home directory (like /home/john/mysite/), the home directory itself often has 700 permissions. Nginx cannot traverse it even if the files inside have the right permissions:

# Check the home directory permissions
ls -la /home/john/

# Fix: add execute permission for others
sudo chmod o+x /home/john/

A better approach is to avoid serving files from home directories entirely. Use /var/www/ or /srv/ instead.

Related: For a deeper explanation of how Linux permissions work and why Permission denied errors happen, see Fix: EACCES Permission Denied.

Common Mistake: Setting correct permissions on the files inside /var/www/html/ but forgetting that Nginx also needs execute permission on every parent directory in the path. If /home/john/ is 700, Nginx can’t traverse it to reach /home/john/mysite/, even if mysite/ itself is world-readable.

Fix 2: Fix Directory Index (autoindex)

When you request a URL that maps to a directory (e.g., http://example.com/files/), Nginx looks for an index file specified by the index directive. If it cannot find one and autoindex is off, it returns a 403.

Check your Nginx config for the index directive:

sudo nginx -T 2>&1 | grep -E "index |autoindex"

A typical config looks like this:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    index index.html index.htm index.php;
}

If you want directory listings enabled (for a file server, download directory, etc.):

location /files/ {
    autoindex on;
    autoindex_exact_size off;    # Show human-readable file sizes
    autoindex_localtime on;      # Show local time instead of UTC
}

Only enable autoindex for specific locations, not the entire server. Exposing your full directory tree is a security risk.

If you do not want directory listings, make sure the index file exists:

# Check if the index file is actually there
ls -la /var/www/html/index.html

# If it is missing, create a placeholder
echo "<h1>It works</h1>" | sudo tee /var/www/html/index.html

Also check the index directive matches the actual file name. If your framework generates index.php but the index directive only lists index.html, Nginx will not find the index file and return a 403 for directory requests.

Fix 3: Fix the Root Path

If the root directive points to a non-existent directory or one with wrong permissions, every request returns a 403.

Check what root path your server block uses:

sudo nginx -T 2>&1 | grep "root "

Verify the directory exists and is accessible:

# Check if the path exists
ls -la /var/www/html/

# Check the full path is traversable by Nginx
sudo -u www-data stat /var/www/html/

If stat fails with “Permission denied”, Nginx cannot access that path either.

Common mistakes with root:

Wrong:

# Missing trailing content -- this serves files from /var/www, not /var/www/html
location /html/ {
    root /var/www;
}

This configuration maps http://example.com/html/page.html to /var/www/html/page.html, which might be correct. But many people confuse root with alias:

# alias replaces the matched location prefix
location /static/ {
    alias /var/www/assets/;
}
# /static/style.css -> /var/www/assets/style.css

# root appends the full URI to the path
location /static/ {
    root /var/www;
}
# /static/style.css -> /var/www/static/style.css

Getting root and alias mixed up sends Nginx to the wrong directory, which often results in a 403 (directory without an index file) or a 404.

Important: When using alias, always include a trailing slash on both the location and the alias path. Missing slashes cause subtle path resolution bugs.

Fix 4: Fix SELinux Context (RHEL/CentOS/Fedora)

On RHEL-based systems, SELinux adds a mandatory access control layer on top of standard Unix permissions. Even if chmod and chown are correct, SELinux can still block Nginx from reading files.

Check if SELinux is enforcing:

getenforce

If it returns Enforcing, check for denials:

sudo grep nginx /var/log/audit/audit.log | grep denied

You will see entries like:

type=AVC msg=audit(...): avc:  denied  { read } for  pid=1234 comm="nginx" name="index.html" ... scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 ...

The key part is tcontext=...user_home_t. Nginx expects files to have the httpd_sys_content_t context, but these files have user_home_t (they were copied from a home directory or created outside of /var/www).

Fix the SELinux context:

# Set the correct context for web content
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?"

# Apply the context recursively
sudo restorecon -Rv /var/www/html/

For writable directories (upload directories, cache, etc.):

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/uploads(/.*)?"
sudo restorecon -Rv /var/www/html/uploads/

Quick test to confirm SELinux is the problem:

# Temporarily set SELinux to permissive (allows everything but logs denials)
sudo setenforce 0

# Test your site -- if it works now, SELinux was blocking it
curl -I http://localhost

# Re-enable enforcing
sudo setenforce 1

Do not leave SELinux in permissive mode on production servers. Use the semanage and restorecon commands above to fix the labels properly.

Related: SELinux can also block Nginx from connecting to upstream servers, causing 502 Bad Gateway errors when reverse proxying.

Fix 5: Fix the Nginx User

Nginx’s master process runs as root, but the worker processes (which actually serve requests) run as the user defined in nginx.conf:

grep "^user" /etc/nginx/nginx.conf

If this says user nobody; or user nginx; but your files are owned by www-data (or vice versa), the workers cannot read the files.

Fix option 1: Change the Nginx user to match the file owner:

# /etc/nginx/nginx.conf
user www-data;

Fix option 2: Change file ownership to match the Nginx user:

sudo chown -R nginx:nginx /var/www/html/

Fix option 3: Use group permissions. Add the Nginx user to the group that owns the files:

sudo usermod -aG www-data nginx

Then make sure files are group-readable:

sudo find /var/www/html/ -type f -exec chmod 640 {} \;
sudo find /var/www/html/ -type d -exec chmod 750 {} \;

Restart Nginx after changing the user directive:

sudo systemctl restart nginx

Related: Permission problems are a common theme across Linux tools. If you are also seeing permission errors in shell scripts or CLI tools, see Fix: Linux Permission Denied.

If your document root or any path component contains a symbolic link, the disable_symlinks directive can cause a 403.

Check if disable_symlinks is set:

sudo nginx -T 2>&1 | grep disable_symlinks

The directive has three modes:

# Default: follow all symlinks (no restriction)
disable_symlinks off;

# Block if any path component is a symlink
disable_symlinks on;

# Block if symlink and target have different owners
disable_symlinks if_not_owner;

If disable_symlinks is set to on or if_not_owner and your setup relies on symlinks (common for deployment rollbacks, Let’s Encrypt certificate paths, or shared content directories), you have two options:

Option 1: Disable the restriction:

server {
    disable_symlinks off;
    # ...
}

Option 2: Fix symlink ownership. If using if_not_owner, the symlink and its target must be owned by the same user:

# Check symlink and target ownership
ls -la /var/www/html/current
ls -la /var/www/releases/v2.1.0/

# Fix: make sure both are owned by the same user
sudo chown -h www-data:www-data /var/www/html/current
sudo chown -R www-data:www-data /var/www/releases/v2.1.0/

Note the -h flag on chown — it changes the ownership of the symlink itself, not the target.

Fix 7: Fix IP Deny Rules

Nginx can restrict access by IP address using allow and deny directives. If your IP is denied, you get a 403 with this message in the error log:

access forbidden by rule, client: 203.0.113.50, server: example.com

Find the deny rules in your config:

sudo nginx -T 2>&1 | grep -E "allow|deny"

A typical access control block:

location /admin/ {
    allow 192.168.1.0/24;
    allow 10.0.0.0/8;
    deny all;
}

This allows only internal IPs and denies everyone else. If you are connecting from an IP outside these ranges, you get a 403.

Fixes:

  • Add your IP to the allow list.
  • If you are behind a load balancer, CDN, or proxy, Nginx sees the proxy’s IP, not yours. Use the real_ip module to extract the real client IP:
# Trust the load balancer / CDN
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
  • Check for deny rules in included config files. Nginx configs often include files from /etc/nginx/conf.d/ or /etc/nginx/snippets/ that might contain deny rules you did not write:
sudo grep -r "deny" /etc/nginx/conf.d/ /etc/nginx/snippets/ 2>/dev/null

Also check .htaccess-style blocks. Nginx does not use .htaccess files, but some configurations convert Apache rules into Nginx deny directives during migration.

Pro Tip: Run namei -l /var/www/html/index.html to see the permissions on every component of the path at once. This instantly reveals which directory in the chain is blocking Nginx, saving you from checking each one individually with ls -la.

Still Not Working?

Read the error log carefully

Every 403 investigation starts here:

sudo tail -100 /var/log/nginx/error.log

The log tells you exactly what Nginx tried to do and why it failed. Match the message to the relevant fix above:

  • Permission denied -> Fix 1 (file permissions) or Fix 4 (SELinux)
  • directory index ... is forbidden -> Fix 2 (autoindex / missing index file)
  • access forbidden by rule -> Fix 7 (IP deny rules)
  • failed (2: No such file or directory) -> Fix 3 (wrong root path) — this usually shows as a 404, but can trigger a 403 if Nginx falls through to a directory without an index

Test with a minimal config

If you have a complex configuration with many includes, snippets, and locations, isolate the problem with a minimal server block:

server {
    listen 8888;
    server_name _;
    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Test it:

sudo nginx -t && sudo systemctl reload nginx
curl -I http://localhost:8888/

If this works, the problem is in your main configuration, not the filesystem. Add back config sections one at a time until the 403 reappears.

Check parent process limits

In rare cases, Nginx workers inherit restrictive file access limits from their parent process. Check:

# See the Nginx worker's open file limit
cat /proc/$(pgrep -f "nginx: worker" | head -1)/limits | grep "open files"

If the limit is very low, increase it in the systemd service file or in nginx.conf:

worker_rlimit_nofile 65535;

Docker volume permissions

If Nginx runs in a Docker container and serves files mounted via a volume, the UID inside the container may not match the UID of the file owner on the host. The Nginx official Docker image runs workers as UID 101 (nginx user inside the container).

# Check what UID the files have on the host
ls -ln /path/to/site/

# Option 1: Change file ownership to match container UID
sudo chown -R 101:101 /path/to/site/

# Option 2: Run Nginx as a different user in the container
docker run -d --user $(id -u):$(id -g) -v /path/to/site:/usr/share/nginx/html nginx

Related: For Docker Compose startup failures, see Fix: Docker Compose Up Errors. For SSL certificate issues when configuring HTTPS with Nginx, see Fix: SSL Certificate Problem.


Related: If Nginx can reach the files but cannot connect to a backend, the error changes from 403 to 502 Bad Gateway. For general Linux permission denied errors in the shell, see Fix: EACCES Permission Denied. For Docker-specific permission problems, see Fix: Docker Compose Up Errors.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles