An Nginx 403 Forbidden error means the request reached the server, but Nginx or an upstream layer refused to serve the requested resource. The most common causes are a missing index file, directory listing being disabled, filesystem permissions, access-control rules, SELinux labels, symlink restrictions, or a backend application returning its own 403.
Start with diagnostics that separate an origin Nginx 403 from PHP-FPM, a proxied backend, a CDN, or the operating system. The fixes stay focused on static sites, application front controllers, access rules, SELinux systems, and hidden-file or ACME challenge paths, while avoiding broad permission changes such as chmod 777.
Understand Nginx 403 Forbidden Causes
A browser only shows the final HTTP status. The useful clue usually appears in the Nginx error log, where messages such as directory index ... is forbidden, permission denied, or access forbidden by rule point to different fixes.
| Symptom | Likely Cause | First Check |
|---|---|---|
Homepage path such as / returns 403 | No index file found and directory listing is disabled | Check the error log for directory index ... is forbidden and verify the index directive. |
| Specific file returns 403 | Nginx worker cannot read the file or traverse a parent directory | Check permissions with namei -l and confirm the worker user. |
| Only certain IP addresses receive 403 | allow and deny rules, CDN IP handling, or a security gateway | Inspect active allow/deny directives and the real client IP seen by Nginx. |
| Permissions look correct but 403 remains on Fedora, RHEL, Rocky Linux, or AlmaLinux | SELinux label blocks web-server access | Check getenforce, ls -Z, and recent AVC denials. |
| Public request returns 403 but local Nginx request works | CDN, load balancer, web application firewall, or wrong origin | Compare the local origin request with the public request headers. |
| Reverse-proxied path returns 403 | The upstream application denies the path or Nginx forwards the wrong path/header | Request the upstream directly from the Nginx host. |
These Nginx directives are the usual 403 decision points. Check them before changing ownership or permissions across a live document root.
| Directive | Context | Role in 403 Troubleshooting |
|---|---|---|
index | http, server, location | Defines which index files Nginx checks for requests ending in /. |
autoindex | http, server, location | Controls directory listing output. The documented default is off, which can produce a 403 when no index file exists. |
root | http, server, location, if in location | Builds file paths by appending the URI to the configured root. |
alias | location | Replaces the matching location prefix with a filesystem path. Trailing slash mistakes can make Nginx open the wrong path. |
try_files | server, location | Checks files or directories in order. A $uri/ check can trigger directory index handling. |
allow and deny | http, server, location, limit_except | Permit or block requests by address. Nginx checks these rules in sequence until the first match. |
disable_symlinks | http, server, location | Can deny access when a requested path passes through symbolic links. |
Diagnose an Nginx 403 Forbidden Error
Do not start by restarting the service or changing permissions recursively. First, reproduce the request locally, read the log line, and identify the active server block. That keeps a simple missing index problem from turning into a risky ownership change.
Check the Nginx 403 Response Locally
Send a local request from the Nginx host with the same hostname and path that fails publicly. The curl command in Linux guide is useful background if you want to review header-only requests such as -I.
curl -I -H "Host: example.com" http://127.0.0.1/
A local 403 confirms that the origin Nginx stack can reproduce the issue without DNS, browser cache, or a CDN in the first pass:
HTTP/1.1 403 Forbidden Server: nginx
If the local request works but the public request still returns 403, do not edit Nginx yet. Check the CDN, load balancer, web application firewall, hosting panel, or public origin mapping first.
Read the Nginx Error Log for the 403 Cause
The error log usually names the exact reason. Most packaged installs write to /var/log/nginx/error.log; if your package or source build uses another path, inspect the active error_log directive with sudo nginx -T.
sudo tail -n 80 /var/log/nginx/error.log
Relevant log lines often look like one of these patterns:
directory index of "/var/www/example.com/html/" is forbidden open() "/var/www/example.com/html/index.html" failed (13: Permission denied) access forbidden by rule
These messages point to different fixes. A directory-index error needs an index file, adjusted try_files, or intentional directory listing. A permission-denied error needs filesystem or SELinux checks. An access-rule error needs allow and deny review.
Check the Nginx Access Log for the Failing URI
The access log confirms the requested path and final status. Use a narrow search when the log is busy; the related grep command in Linux and tail command in Linux references cover deeper log-filtering patterns.
sudo grep ' 403 ' /var/log/nginx/access.log | tail -n 20
Relevant entries show which URI Nginx refused:
"GET / HTTP/1.1" 403 "GET /private/report.pdf HTTP/1.1" 403 "GET /.well-known/acme-challenge/token HTTP/1.1" 403
If only one path fails, focus on the matching location block. If every path fails for the hostname, focus on the server block, document root, permissions, or access rules.
Find the Active Nginx 403 Configuration
Dump the active Nginx configuration and inspect only routing, file-lookup, and access-control directives:
sudo nginx -T 2>&1 | grep -E 'server_name|listen|root|alias|index|try_files|location|allow|deny|autoindex|disable_symlinks|auth_request|fastcgi_param|proxy_pass'
Relevant output might expose a missing index directive, a broad deny rule, or a document root that differs from the deployed path:
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
location /private/ {
deny all;
}
Use the active configuration, not a file that happens to exist on disk. Debian and Ubuntu packages often use sites-available and sites-enabled, while Fedora, RHEL, Rocky Linux, AlmaLinux, and nginx.org packages commonly use /etc/nginx/conf.d/*.conf.
Identify the Nginx Worker User
Permission checks only make sense if you know which user reads the files. The master process may run as root, but the worker processes usually run as www-data, nginx, or another configured service user.
ps -o user,group,comm -C nginx
Relevant output includes the worker user and group:
USER GROUP COMMAND root root nginx www-data www-data nginx
On many RPM-family systems, the worker rows use nginx nginx instead. Use the user you actually see when checking file and directory access.
Fix Nginx 403 from Missing Index Files
The most recognizable Nginx 403 message is directory index of ... is forbidden. It means the request maps to a directory, Nginx did not find an index file it is configured to use, and directory listing is disabled.
Check whether the expected homepage file exists in the configured document root:
sudo test -f /var/www/example.com/html/index.html && echo "index file exists"
Expected output when the file exists:
index file exists
If the command prints nothing, deploy the missing file or point root to the directory that actually contains the site output. Static site generators often publish into public, dist, or build, not the project root.
Set the Correct Nginx index Directive
For a static site, include the index filenames your deployment actually creates:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
For PHP applications that use a front controller, include index.php and make sure PHP requests are passed to PHP-FPM in a separate location:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
Adjust the PHP-FPM socket to match your system. The important part for a 403 check is that root points to the public application directory and SCRIPT_FILENAME resolves to the real PHP file.
Fix Nginx try_files Directory Checks
A try_files rule that includes $uri/ tells Nginx to check whether the request maps to a directory. If that directory exists but has no matching index file, Nginx can return a directory-index 403.
For a single-page application, avoid forcing a directory check unless you need directory-specific index behavior. Use an index.html fallback instead:
location / {
try_files $uri /index.html;
}
For a normal static website that really has per-directory index files, keep $uri/ and make sure each public directory has the correct index file or should return 403 by design. If Nginx cannot match the file, directory, route, or fallback at all, compare the symptom with fixing Nginx 404 Not Found errors.
Use Nginx autoindex Only When Directory Listings Are Intended
Do not enable directory listings to hide a missing homepage. Use autoindex on; only for directories that are meant to show file listings, such as a private package mirror or controlled download directory.
location /downloads/ {
root /var/www/example.com/html;
autoindex on;
}
Directory listings expose filenames and paths. Keep
autoindex offfor normal websites unless the listing is the feature you want visitors to use.
Fix Nginx 403 from File Permissions
A permission denied log line means Nginx tried to open the file but the operating system refused access. The file needs read permission, and every parent directory in the path needs execute permission for the Nginx worker user.
Check Nginx Path Permissions with namei
Use namei -l to see each directory in the path. Replace the path with the file or directory named in the error log:
namei -l /var/www/example.com/html/index.html
Relevant output should show executable directories and a readable file:
f: /var/www/example.com/html/index.html drwxr-xr-x root root / drwxr-xr-x root root var drwxr-xr-x root root www drwxr-xr-x root root example.com drwxr-xr-x root root html -rw-r--r-- root root index.html
If one parent directory shows restrictive permissions such as drwx------, Nginx cannot traverse it even if the final file is world-readable. This is common when a public site is served from a user home directory.
Apply Safe Nginx Web Root Permissions
For a normal static web root, directories commonly need 755 and files commonly need 644. These modes let the Nginx worker read public files without giving everyone write access. If you need to review mode bits or the recursive pattern first, see the chmod command in Linux and find -exec command option in Linux references before adapting the paths.
The recursive commands in this example target
/var/www/example.com. Replace that path carefully, and do not run them against uploads, cache directories, private application data, or shared project trees until you know the required write permissions.
sudo find /var/www/example.com -type d -exec chmod 755 {} +
sudo find /var/www/example.com -type f -exec chmod 644 {} +
Then confirm the target file again:
namei -l /var/www/example.com/html/index.html
Do not use
chmod -R 777to fix an Nginx 403. It can expose write access to files that should remain read-only and usually hides the real cause instead of solving it.
Keep Nginx Public Files Outside Private Home Directories
If the site lives under a private home directory such as /home/alex/site, the home directory may be 700. That blocks Nginx from traversing the path even when the site files are readable.
The clean fix is to move public web content to a dedicated path such as /var/www/example.com, then point the server block at that directory:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
}
Use ACLs only when you intentionally serve from a nonstandard path and understand the security tradeoff. The setfacl command in Linux ACL examples can help with that model, but a dedicated web-root path is easier to audit and easier to label correctly with SELinux.
Fix Nginx 403 from allow and deny Rules
Nginx can return 403 because the access module denied the client address. The official access module checks rules in sequence until the first match, so rule order matters.
Search the active configuration for address-based rules:
sudo nginx -T 2>&1 | grep -E 'allow |deny '
A protected admin area might intentionally allow a private subnet and deny everyone else:
location /admin/ {
allow 192.0.2.0/24;
deny all;
}
If legitimate users receive 403, confirm the IP Nginx actually sees. Behind a CDN, reverse proxy, or load balancer, Nginx may see the proxy IP unless the real-client-IP layer is configured correctly.
sudo tail -n 20 /var/log/nginx/access.log
If the logged client address is a CDN or load-balancer address, either adjust the allowlist to the correct layer or configure real IP handling in the edge stack. Do not remove a deliberate deny all from an admin location unless that path is meant to be public.
Fix Nginx 403 from SELinux Labels
On SELinux-enabled systems, Linux permissions can look correct while access still fails. Fedora, RHEL, Rocky Linux, AlmaLinux, and similar systems may require the web content to be labeled with a type that the web server domain can read, such as httpd_sys_content_t for static content.
Check whether SELinux is enforcing:
getenforce
Expected output on an enforcing system:
Enforcing
Check the current context of the web root and a target file:
ls -Zd /var/www/example.com /var/www/example.com/html /var/www/example.com/html/index.html
Relevant output for static web content should include httpd_sys_content_t:
system_u:object_r:httpd_sys_content_t:s0 /var/www/example.com/html system_u:object_r:httpd_sys_content_t:s0 /var/www/example.com/html/index.html
If the files are labeled with a type such as default_t or user_home_t, create a persistent file-context rule and apply it:
sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/example.com(/.*)?'
sudo restorecon -Rv /var/www/example.com
When labels change, relevant output shows the restored type for the affected files or directories:
Relabeled /var/www/example.com from unconfined_u:object_r:default_t:s0 to system_u:object_r:httpd_sys_content_t:s0 Relabeled /var/www/example.com/html/index.html from unconfined_u:object_r:default_t:s0 to system_u:object_r:httpd_sys_content_t:s0
For application upload, cache, or media directories that the web server must write to, use a narrower writeable label only for those directories:
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/example.com/public/uploads(/.*)?'
sudo restorecon -Rv /var/www/example.com/public/uploads
If semanage is not installed, add the SELinux policy management package for your distribution and rerun the commands. On RHEL-family systems, that package is commonly policycoreutils-python-utils. Avoid using setenforce 0 as a production fix; it disables enforcement instead of correcting the file context.
Fix Nginx 403 from root, alias, and Symlink Issues
A path can return 403 when the active location maps to a directory you did not intend, when an alias path has the wrong trailing slash behavior, or when symlink restrictions block the resolved file.
Correct Nginx alias Paths
Use alias when the URI prefix and filesystem path differ. Keep the trailing slash pattern aligned between the location and the alias path.
location /media/ {
alias /srv/app/shared/media/;
try_files $uri =404;
}
With this configuration, a request for /media/logo.png maps to /srv/app/shared/media/logo.png. If you use root instead, Nginx appends the full URI under the root path, which is a different lookup model.
Check Nginx Symlink Restrictions
If the error log mentions a symlink or a path that resolves outside the expected web root, check whether disable_symlinks is active:
sudo nginx -T 2>&1 | grep disable_symlinks
Relevant output might show symlink checks enabled:
disable_symlinks if_not_owner from=/var/www/example.com;
Fix the symlink ownership or move the content into the web root rather than disabling symlink protection blindly. Symlink restrictions often exist to prevent serving files outside the intended tree.
Fix Nginx 403 from Hidden File and ACME Rules
Many Nginx configurations block dotfiles to protect files such as .git, .env, and editor backups. That is usually correct, but an overly broad rule can also block /.well-known/acme-challenge/ and break certificate validation.
A safe dotfile block should allow the ACME challenge path before denying other hidden files:
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com/html;
allow all;
}
location ~ /\. {
deny all;
}
Test the challenge path with a temporary file only when you control the domain and document root. Remove the temporary file after testing so the directory does not collect stale challenge data.
Fix Nginx 403 Returned by an Upstream Application
Not every 403 that appears through Nginx is generated by Nginx. A PHP application, Node.js service, Python app, API gateway, or upstream web server can return 403, and Nginx may simply pass that response to the client.
Compare Nginx and Direct Upstream Responses
Request the backend directly from the Nginx host. Replace the address and path with your upstream service. For the broader proxy setup path, see how to create a reverse proxy in Nginx.
curl -I http://127.0.0.1:3000/admin/
If the upstream also returns 403, the application owns the denial:
HTTP/1.1 403 Forbidden
In that case, check application authentication, route guards, CSRF rules, allowed-host settings, IP allowlists, or upstream web-server configuration. Editing the Nginx document root will not fix a backend authorization decision. If Nginx cannot reach the upstream or receives an invalid upstream response instead, compare the symptom with fixing Nginx 502 Bad Gateway errors.
Check PHP-FPM Access Denials
PHP-FPM can return its own access-denied response when the script path is wrong or the PHP pool refuses the file. Keep the Nginx PHP location conservative: check that the PHP file exists, pass the correct script filename, and avoid sending arbitrary non-PHP paths to PHP-FPM.
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
If the Nginx error log is quiet but the browser still shows 403, inspect the PHP-FPM pool log and application log. The denial may be happening after Nginx has already handed the request to PHP.
Test and Reload Nginx After Fixing 403 Forbidden
After changing Nginx configuration, always test syntax before reloading the service:
sudo nginx -t
A successful test prints these lines:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
On systemd-based Linux servers, reload Nginx after the syntax test passes:
sudo systemctl reload nginx
The reload command normally returns no output on success. Verify the failing path again with the same hostname and path:
curl -I -H "Host: example.com" http://127.0.0.1/
A successful response depends on the site, but it should no longer be 403 Forbidden:
HTTP/1.1 200 OK Server: nginx
If the site should redirect to HTTPS, a 301 or 308 response can also be correct. The important part is that the same path no longer returns 403 from the origin layer.
Troubleshoot Common Nginx 403 Forbidden Errors
After you have a log clue or repeatable request, match it to the narrow fix. These examples avoid broad permission changes and focus on the smallest change that resolves the specific 403.
Nginx Log Shows directory index is forbidden
This error means Nginx reached a directory but did not find a usable index file, and autoindex is off. Confirm that the log entry points at the same URI and filesystem directory before changing the server block:
sudo grep 'directory index' /var/log/nginx/error.log | tail -n 5
For a static site, add the missing index file or make the index directive match the file that should load first:
root /var/www/example.com/html;
index index.html index.htm;
For a single-page app, remove the directory probe when the app should fall back to one front controller instead of testing $uri/:
location / {
try_files $uri /index.html;
}
Nginx Log Shows Permission denied
Run namei -l on the exact path from the log, not a similar file in the same directory. The final file needs read permission, and every parent directory needs execute permission for the worker user:
namei -l /var/www/example.com/html/index.html
If the problem is one public path, prefer a targeted permission fix before applying recursive commands to a whole site tree:
sudo chmod 755 /var/www/example.com /var/www/example.com/html
sudo chmod 644 /var/www/example.com/html/index.html
On SELinux systems, also check the label because normal permissions are not the only access-control layer:
ls -Z /var/www/example.com/html/index.html
Nginx Log Shows access forbidden by rule
Inspect allow and deny directives in the active configuration, then match them to the location that handled the request:
sudo nginx -T 2>&1 | grep -nE '^[[:space:]]*(allow|deny) '
For a private admin path, keep the allowed address or subnet before deny all. For a public path, remove the access rule from that location instead of widening access globally:
location /admin/ {
allow 203.0.113.10;
deny all;
}
If the server sits behind a CDN or reverse proxy, confirm whether Nginx is matching the real client IP or the proxy IP. Rule order matters because Nginx stops at the first match.
Nginx 403 Appears Only for Let’s Encrypt Challenges
Check whether a dotfile-blocking regex captures /.well-known/acme-challenge/. Test a temporary challenge file on the origin before changing certificate automation:
curl -I -H "Host: example.com" http://127.0.0.1/.well-known/acme-challenge/test-token
Add a dedicated ACME location before the broad hidden-file deny rule, and point it at the directory where the challenge files are written:
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com/html;
allow all;
}
location ~ /\. {
deny all;
}
Nginx 403 Appears Only After Moving the Site
A move can change the active path mapping, directory traversal permissions, and SELinux labels. Confirm the path Nginx is using, then inspect that exact filesystem path:
sudo nginx -T 2>&1 | grep -E '^[[:space:]]*(root|alias) '
namei -l /srv/example.com/html/index.html
If the site moved from /var/www to /srv, a home directory, or a mounted volume, fix the server block path and path permissions together. On SELinux-enabled systems, also relabel the new web root instead of repeatedly changing mode bits.
Nginx 403 Disappears Locally but Remains Publicly
Compare the origin response with the public response for the same hostname and path:
curl -I -H "Host: example.com" http://127.0.0.1/
curl -I https://example.com/
If the origin request works but the public request still fails, the public 403 is probably not from the origin Nginx server block you just edited. Check CDN firewall rules, edge redirects, origin selection, cached error responses, and any hosting panel or proxy manager in front of Nginx.
Conclusion
An Nginx 403 fix is done when the same request reaches the intended file, index handler, access rule, or upstream instead of being refused. Keep the workflow narrow: reproduce the request locally, follow the error-log clue, run sudo nginx -t, reload on systemd-based Linux servers, and retest the exact path before changing broader permissions or removing protections.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><a href="https://example.com">link</a><blockquote>quote</blockquote>