Skip to main content
DevOpsintermediate

Fix "403 Forbidden" in Nginx — Permissions and Index

Fix nginx 403 Forbidden. Match the error.log line — directory index forbidden, open() failed (13: Permission denied), or access forbidden by rule — to the right fix for permissions, missing index, SELinux, or deny rules.

9 min readUpdated August 2026

Nginx returns 403 Forbidden when it locates the request but refuses to serve it. Unlike a 404, the path exists — something is denying access.

403 Forbidden
nginx/1.24.0

Step 1: Read the Error Log

The error log names the exact reason. Do not guess:

sudo tail -n 30 /var/log/nginx/error.log

You will see one of these:

directory index of "/var/www/html/" is forbidden, client: 203.0.113.9

open() "/var/www/html/index.html" failed (13: Permission denied), client: 203.0.113.9

access forbidden by rule, client: 203.0.113.9

Each maps to a different fix. If the log is not where you expect, see Where Are Nginx Logs Stored?.


Cause 1: directory index ... is forbidden — No Index File

Nginx was asked for a directory, found no index file, and refuses to list the contents because autoindex is off by default.

Check what is actually there:

ls -la /var/www/html/

Fix A — provide an index file. If your app's entry point is not index.html, tell nginx:

server {
    root /var/www/html;
    index index.php index.html index.htm;
}

Nginx tries each name in order and serves the first that exists.

Fix B — enable directory listing, only if you actually want one:

location /downloads/ {
    autoindex on;
}

Be deliberate about this. A listing exposes everything in the directory — backups, dotfiles, .git, stray archives. Enable it on a purpose-built download directory, never on a general web root.


Cause 2: (13: Permission denied) — Filesystem Permissions

The nginx worker cannot read the file. This has two halves, and the second is the one people miss.

First, identify the worker user:

ps -o user= -C nginx | sort -u

www-data on Debian/Ubuntu, nginx on RHEL/Rocky/AlmaLinux. The root entry you also see is the master process — it is the worker user that matters.

Test access as that user directly:

sudo -u www-data stat /var/www/html/index.html

If that fails, you have found the problem without any guesswork.

Check the whole path, not just the file. Nginx needs execute (x) on every directory from / down to the file. One directory missing it blocks everything beneath:

namei -l /var/www/html/index.html

namei -l prints the permissions of each component in the path — the fastest way to spot the one directory at fault.

Apply correct permissions:

sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;

755 for directories, 644 for files. Do not use chmod -R 777. It makes every file writable by every user and process on the box, so any compromised service can rewrite your site — and it does not fix anything 755 does not.

Serving from a home directory is a frequent trigger, because home directories are usually mode 700 and nginx cannot traverse in at all. Moving the content to /var/www or /srv is cleaner than loosening permissions on someone's home directory.


Advertisement

Cause 3: SELinux (RHEL, Rocky, AlmaLinux)

Permissions look perfect, sudo -u nginx stat succeeds, and you still get 403. That is SELinux.

Confirm it is enforcing and check for denials:

getenforce
sudo ausearch -m avc -ts recent | grep nginx

Look at the file labels:

ls -Z /var/www/html/

Web content needs the httpd_sys_content_t type. Files copied from /home or /tmp carry the wrong label and are denied.

Fix it durably:

sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
sudo restorecon -Rv /srv/www

semanage records the rule and restorecon applies it. Using chcon alone appears to work but is undone by the next filesystem relabel, and the error returns weeks later with no obvious trigger.

If nginx also needs to write (uploads, cache), use httpd_sys_rw_content_t for those directories only.

Do not disable SELinux to fix a labelling problem. setenforce 0 will make the 403 vanish and confirm the diagnosis, but shipping with SELinux off trades a five-minute fix for a permanently weaker server.


Cause 4: access forbidden by rule — An Explicit Deny

Something in your config is refusing on purpose. Search for it:

sudo nginx -T | grep -n -E "deny|allow|internal"

Common culprits:

location / {
    deny all;                      # blanket block, often left from testing
}

location ~ /\. {
    deny all;                      # blocks dotfiles — usually intentional
}

location /admin/ {
    allow 192.0.2.0/24;
    deny all;                      # IP allowlist; you are not on the list
}

nginx -T dumps the fully resolved config with every include, which is the only reliable way to find a rule buried in a file you forgot about.


Cause 5: Wrong root, or root vs alias

If root points somewhere that exists but is not your site, you can get 403 instead of 404 — nginx finds a directory, has no index for it, and refuses.

sudo nginx -T | grep -n "root\|alias"

A classic trap is mixing up root and alias in a location block:

location /static/ {
    root  /var/www/app;      # serves /var/www/app/static/…
}

location /static/ {
    alias /var/www/app/;     # serves /var/www/app/…  (path replaced, not appended)
}

root appends the URI to the path; alias replaces the matched prefix. Choosing the wrong one silently points nginx at a directory that is not what you meant.


Verify the Fix

sudo nginx -t
sudo systemctl reload nginx
curl -I http://127.0.0.1/                # expect 200
sudo tail -f /var/log/nginx/error.log    # confirm no new 403 lines

Test as the worker user too — that is the check that actually reflects what nginx can do:

sudo -u www-data cat /var/www/html/index.html | head -1

Prevention

  • Standardise on /var/www or /srv rather than home directories, so permissions and SELinux labels are right by default.
  • Set 755/644 on deploy. Bake it into your deployment script so a git pull or rsync from a developer's laptop cannot bring hostile permissions with it.
  • Never reach for chmod 777. If 755 does not work, the problem is ownership, path traversal, or SELinux — and 777 will hide it rather than solve it.
  • Alert on 403 rates. A sudden burst usually means a deploy changed ownership, and the error log will have said so from the first request — see Where Are Linux System Logs Stored? for centralising it.

Frequently Asked Questions

Find answers to common questions

Nginx found the request but refused to serve it. There are four usual reasons: no index file in the directory, filesystem permissions that block the nginx worker user, an SELinux label denying access, or an explicit deny rule in the config. The error log line tells you which.

Nginx was asked for a directory, found no index file, and will not list the contents because autoindex is off by default. Add an index.html, add the filename to the index directive, or turn on autoindex if you genuinely want a browsable listing.

The nginx worker user cannot read the file. It needs read permission on the file itself and execute permission on every directory in the path above it. A single directory missing the execute bit anywhere in the chain blocks access to everything beneath it.

www-data on Debian and Ubuntu, nginx on RHEL, Rocky, and AlmaLinux. Confirm on your machine with 'ps -o user= -C nginx | sort -u'. The master process runs as root; it is the worker user that must be able to read your files.

Home directories are typically mode 700, so nginx cannot traverse into them at all. Either move the site under /var/www or /srv, or grant execute permission on the home directory. Loosening permissions on a home directory has privacy consequences, so moving the content is usually better.

Yes, on RHEL-family systems. Files need the httpd_sys_content_t label, and files copied from a home directory or /tmp usually carry the wrong one. Check with 'ls -Z' and look for denials with 'sudo ausearch -m avc -ts recent'.

Use semanage to record the correct context and restorecon to apply it: 'sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"' then 'sudo restorecon -Rv /srv/www'. Using chcon alone works until the next relabel, then the problem returns.

No. 777 makes every file writable by every user and process on the machine, so any compromised service can modify your site. Directories should be 755 and files 644, with ownership set so the nginx user can read them.

404 means nginx could not find the path at all, usually a wrong root or a typo. 403 means it found the path and refused. If you expected a file to exist and get 403, the file is there but unreadable; if you get 404, check the root directive first.

It exposes a browsable listing of everything in the directory, including files you may not have intended to publish such as backups, dotfiles, and archives. It is fine for a deliberate file-download directory and a mistake on a normal web root.