NGINX Forbidden 403 Error: A Practical Troubleshooting Guide
July 5, 2026

You push a deployment, refresh the site, and get 403 Forbidden. The file is there. The server is up. nginx -t passes. You change a permission bit, reload NGINX, refresh again, and nothing changes.
That's where most bad troubleshooting starts. People guess. They chmod -R 777 a web root, break ownership, and make the server less secure without fixing the root cause.
A proper Nginx forbidden 403 workflow starts with evidence. Read the error log first. Let the log tell you whether you're dealing with filesystem permissions, a bad root, missing index handling, an access rule, SELinux, or a backend that's returning its own 403. If you want a second plain-English reference on how 403 responses show up in day-to-day hosting environments, this 403 error guide for Australian businesses is a useful companion.
Table of Contents
- Deciphering the NGINX 403 Forbidden Error
- Start with the Logs Your First Step in Diagnosis
- The #1 Culprit Incorrect File and Directory Permissions
- Beyond Permissions Fixing Common NGINX Configuration Errors
- Advanced Scenarios SELinux Docker and Upstream Issues
- Your Go-To NGINX 403 Forbidden Checklist
Deciphering the NGINX 403 Forbidden Error
A 403 from NGINX means something specific. NGINX understood the request, found the target path or request context, and refused to serve it. This is not a 404. A 404 says the resource can't be found. A 403 says access is blocked.
That distinction matters because it changes how you debug. If the server can identify the location but won't return content, the failure is usually one of these classes:
- Filesystem access. NGINX can't traverse a directory or read a file.
- Configuration logic. A
root,alias,index,try_files, ordenyrule produces a forbidden outcome. - Host security controls. SELinux can block access even when UNIX permissions look correct.
- Application-side refusal. When NGINX is proxying, the 403 may come from upstream.
Practical rule: Don't treat every 403 as a permission problem. Treat it as an access decision, then identify which layer made that decision.
A common example is a static site deployed to /var/www/html after a CI run. The files exist, but the pipeline wrote them as a user NGINX doesn't run as. Another is a framework app with a clean server block but a subtle try_files mistake that makes directory requests fail. Another is a RHEL host where the permissions are correct but SELinux labels aren't.
The fastest path isn't trial and error. It's observation first, then one targeted fix.
Start with the Logs Your First Step in Diagnosis
Professionals don't start with chmod. They start with the error log.
On most systems, the first place to look is:
sudo tail -f /var/log/nginx/error.log
Then reproduce the request in a browser or with curl. Watching the log live is the easiest way to stop guessing.

What to look for in the log
These messages usually point you in the right direction fast:
open() "/var/www/html/index.html" failed (13: Permission denied)
That almost always means the worker process can't read the file or traverse part of the path.
You may also see:
directory index of "/var/www/html/" is forbidden
That usually points to a request hitting a directory, with no usable index file and no directory listing allowed.
Or, in reverse proxy setups:
connect() to unix:/run/app.sock failed (13: Permission denied)
That tells you the refusal may be happening at the socket or upstream boundary, not in the static file path.
A simple diagnostic routine
Use this sequence every time:
Watch the error log live
sudo tail -f /var/log/nginx/error.logReproduce the failing request
curl -I http://localhost/Filter for the relevant terms
sudo grep -Ei "forbidden|denied|permission" /var/log/nginx/error.logValidate the active configuration
sudo nginx -t sudo nginx -TCheck whether the 403 is local or upstream Look for
open(),directory index,deny,connect(), or upstream-related messages.
If the log says
Permission denied, believe it. If the log saysdirectory index ... is forbidden, don't waste time changing ownership first.
If you manage more than one host, centralizing these logs saves time. A practical reference for that is this blueprint for centralized log management. For teams building internal debugging utilities and operational tooling, the broader Apify tools catalog is also worth browsing.
Why this step matters
A 403 can come from several layers that look identical in the browser. The log separates them. Without that separation, people tend to apply the right fix to the wrong problem.
That's why log-first troubleshooting works. It narrows the problem before you touch the system.
The #1 Culprit Incorrect File and Directory Permissions
Most production Nginx forbidden 403 incidents still come down to Linux permissions. In 75 to 85% of enterprise deployments, the primary cause is incorrect file permissions, especially directories that lack the execute bit for other users. The standard fix is to set directories to 755 and files to 644 according to OneUptime's NGINX 403 analysis.
That “execute bit on directories” detail trips people up. On a file, execute usually means runnable. On a directory, it means traversable. NGINX needs to move through every parent directory in the path before it can read the file at the end.

The permission model that actually matters
For static content, these are the values you want:
| Item | Correct mode | Why |
|---|---|---|
| Directories | 755 |
NGINX can traverse and read directory metadata |
| Files | 644 |
NGINX can read file contents without granting write access |
Ownership matters too. The user running NGINX must own the files or at least have the required access. On Debian and Ubuntu, that user is often www-data.
The practical baseline from the field is straightforward: set ownership to the NGINX user, set directories to 755, and files to 644. Dash0's guidance also stresses that parent directories like /var, /var/www, and /var/www/html must be traversable or NGINX will refuse access with a 403, even when the target file itself looks fine in ls -l output, as described in this Nginx 403 Forbidden FAQ from Dash0.
Exact commands to run
If your web root is /var/www/html and your NGINX user is www-data, run:
cd /var/www/html
sudo chown -R www-data:www-data .
sudo find . -type d -exec chmod 755 {} \;
sudo find . -type f -exec chmod 644 {} \;
If you want to apply the fix using the full path:
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 {} \;
Then verify the path itself:
namei -l /var/www/html/index.html
That command is one of the best tools for this problem. It shows permissions on every path component, not just the final file.
Watch the whole path, not just the file. A readable
index.htmlis useless if/var/wwwisn't traversable by the NGINX worker.
If you suspect the wrong runtime user, confirm it:
ps aux | grep nginx
grep -E "^[[:space:]]*user[[:space:]]" /etc/nginx/nginx.conf
What works and what doesn't
What works:
- Correct ownership first. Fix the user mismatch before changing modes.
- Recursive
findfor dirs and files separately. That avoids making files executable. - Checking parent directories. This catches a lot of “looks correct but still 403” cases.
What doesn't:
chmod -R 777. It's sloppy, insecure, and often still wrong.- Changing only the web root leaf directory. The parent path can still block traversal.
- Assuming your deployment user and NGINX user are the same. They often aren't.
A short walkthrough can help if you want to compare your process with another operator's screen-level fix sequence:
After the permission fix, validate and reload:
sudo nginx -t
sudo systemctl reload nginx
If the log no longer shows permission errors but the site still returns 403, stop changing modes. The next likely source is the NGINX config itself.
Beyond Permissions Fixing Common NGINX Configuration Errors
When permissions are clean and the log still points to forbidden access, the problem usually lives in the server block. These aren't exotic mistakes. They're the kind that show up in rushed edits, copied snippets, and framework defaults.
Root and alias mistakes
root appends the request URI to a filesystem base path. alias replaces the matched location path with a different filesystem path. Mixing them up creates confusing file lookups.
Bad config:
location /static/ {
root /var/www/assets;
}
Requesting /static/app.css makes NGINX look under:
/var/www/assets/static/app.css
If your actual file is /var/www/assets/app.css, this isn't what you want.
Good config:
location /static/ {
alias /var/www/assets/;
}
Now /static/app.css maps to:
/var/www/assets/app.css
A fast way to inspect the loaded configuration is:
sudo nginx -T
Then search the exact server and location block handling the failing request.
Missing index handling
Directory requests often produce a 403 when NGINX reaches a valid directory but has nothing it's allowed to serve from it.
Bad config:
server {
listen 80;
root /var/www/html;
location / {
}
}
If the request hits / and there's no default index file NGINX can use, directory access may fail.
Good config:
server {
listen 80;
root /var/www/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
Check for real files, not assumptions:
ls -la /var/www/html/index.*
If you intentionally want directory listings for a path, configure that path explicitly:
location /files/ {
autoindex on;
}
If you don't want listings, add a valid index file instead.
A 403 on
/often means “directory exists, but I won't list it and I can't find an index file I'm allowed to serve.”
The try_files and autoindex trap
One recurring issue is a directory request combined with try_files and disabled directory indexing. Users often configure:
location / {
try_files $uri $uri/ =404;
}
That pattern is common, but it can become a 403 trap on directory access when autoindex is off and there's no valid index handling. The specific conflict of try_files $uri $uri/ with disabled directory indexing is a recurring scenario discussed in the Neos forum thread on NGINX 403 directory listing forbidden. The practical fix is to remove the trailing directory option from try_files or enable autoindex where directory listing is intended.
Bad config for a directory you don't want listed:
location /docs/ {
try_files $uri $uri/ =404;
autoindex off;
}
Better config if the directory should serve only files:
location /docs/ {
try_files $uri =404;
}
Better config if you want listing behavior:
location /docs/ {
autoindex on;
}
For broader writeups on debugging server behavior and application delivery patterns, the Apify Hub blog is a useful general reading source.
Deny rules that block legitimate traffic
Some 403s are self-inflicted by access control directives.
Bad config:
location / {
deny all;
}
Or a rule copied from a hardening guide and left too broad:
location /admin/ {
deny all;
}
Good config depends on intent. If the rule was accidental, remove it:
location / {
try_files $uri $uri/ =404;
}
If the restriction is deliberate, tighten the path and verify it only applies where expected.
Also inspect hidden-file blocks. This is useful:
location ~ /\. {
deny all;
return 404;
}
But if your app relies on a hidden path and you copied a generic snippet without understanding it, you can block valid requests.
A quick config review sequence
Run these in order:
sudo nginx -t
sudo nginx -T | less
grep -R "deny all" /etc/nginx
grep -R "alias " /etc/nginx
grep -R "try_files" /etc/nginx
grep -R "index " /etc/nginx
Review one request path end to end. Ask:
- Which
serverblock matched? - Which
locationmatched? - Does
rootoraliasmap to the file path you expect? - Is an
indexfile present where the request lands? - Does
try_filessend directory requests into a forbidden branch? - Is there a
denyrule on the final location?
That gets you much farther than skimming nginx.conf and hoping the mistake jumps out.
Advanced Scenarios SELinux Docker and Upstream Issues
If you've fixed ownership, verified 755 and 644, and cleaned up the server block, stubborn 403s usually come from the environment around NGINX.

SELinux on RHEL and CentOS
SELinux is the classic trap. Permissions look correct. Ownership looks correct. NGINX still returns 403.
A critical pitfall is that SELinux contexts cause about 15% of 403 failures despite correct UNIX permissions, and the fix is typically chcon -R httpd_sys_content_t or using the :Z suffix in Docker bind mounts, as noted in the earlier OneUptime source.
Check whether SELinux is enforcing:
getenforce
Inspect labels on the web root:
ls -Z /var/www/html
Apply a content label:
sudo chcon -R httpd_sys_content_t /var/www/html
If the audit log is active, look for AVC denials:
sudo grep nginx /var/log/audit/audit.log
Correct UNIX permissions don't override an incorrect SELinux context. The kernel enforces both.
Docker bind mounts and labels
Containers add another layer. You may have valid permissions on the host and still get blocked inside the container because of labeling or mount behavior.
When bind-mounting content into an NGINX container on SELinux-enabled hosts, use the :Z suffix where appropriate. Example:
-v /host/site:/usr/share/nginx/html:Z
Also inspect from inside the container:
docker exec -it <container_name> sh
ls -la /usr/share/nginx/html
If the files are visible but requests still fail, check the NGINX config inside the container, not just the host copy.
Upstream-generated 403 responses
Sometimes NGINX is innocent. It's just passing along a 403 from the backend.
Common signs:
- The error log mentions upstream connection issues rather than file reads.
- Static files work, but proxied routes fail.
- The application log shows authorization or path errors.
Inspect your proxy block:
location /app/ {
proxy_pass http://backend;
}
Then test the backend directly from the host or container where NGINX runs. If the upstream returns 403 on its own, don't keep editing NGINX filesystem permissions.
A short check sequence helps here:
curl -I http://localhost/
curl -I http://backend-service/
sudo tail -f /var/log/nginx/error.log
The key question is simple: who generated the 403? NGINX static file handling, NGINX access rules, SELinux, container isolation, or the upstream app. Once you know that, the fix usually becomes obvious.
Your Go-To NGINX 403 Forbidden Checklist
When production is on fire, you need a sequence you can trust.

Use this order:
- Read the error log first. Run
sudo tail -f /var/log/nginx/error.logand reproduce the request. - Confirm the NGINX runtime user. Check
ps aux | grep nginxandnginx.conf. - Fix ownership before modes. Set the web root to the NGINX user if needed.
- Apply sane permissions. Directories should be
755, files644. - Check the full path. Run
namei -lon the target file and inspect every parent directory. - Validate the active config. Use
sudo nginx -tandsudo nginx -T. - Review request mapping. Check
root,alias,index,try_files, and anydenyrules. - Inspect SELinux or container context. Especially on RHEL, CentOS, and bind-mounted Docker setups.
- Decide whether the 403 is upstream. Test the backend directly if NGINX is reverse proxying.
- Reload only after a verified fix. Don't reload after random edits.
If you maintain repeatable debugging notes or internal runbooks, keeping them organized in a searchable place helps. For that kind of reference workflow, the Apify Hub guides collection is one example of how structured knowledge resources can be presented clearly.
If you build data products or automation tools for the Apify ecosystem, Apify Hub is worth a look. It surfaces public actor rankings, category trends, pricing patterns, and revenue estimates in one place, which makes it useful for validating ideas before you build or reprice anything.