How to Fix Nginx 403 Forbidden

Fix Nginx 403 Forbidden errors by checking index files, permissions, allow/deny rules, SELinux labels, upstream apps, and CDN layers.

PublishedAuthorJoshua JamesRead time13 minGuide typeNginx

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.

SymptomLikely CauseFirst Check
Homepage path such as / returns 403No index file found and directory listing is disabledCheck the error log for directory index ... is forbidden and verify the index directive.
Specific file returns 403Nginx worker cannot read the file or traverse a parent directoryCheck permissions with namei -l and confirm the worker user.
Only certain IP addresses receive 403allow and deny rules, CDN IP handling, or a security gatewayInspect active allow/deny directives and the real client IP seen by Nginx.
Permissions look correct but 403 remains on Fedora, RHEL, Rocky Linux, or AlmaLinuxSELinux label blocks web-server accessCheck getenforce, ls -Z, and recent AVC denials.
Public request returns 403 but local Nginx request worksCDN, load balancer, web application firewall, or wrong originCompare the local origin request with the public request headers.
Reverse-proxied path returns 403The upstream application denies the path or Nginx forwards the wrong path/headerRequest 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.

DirectiveContextRole in 403 Troubleshooting
indexhttp, server, locationDefines which index files Nginx checks for requests ending in /.
autoindexhttp, server, locationControls directory listing output. The documented default is off, which can produce a 403 when no index file exists.
roothttp, server, location, if in locationBuilds file paths by appending the URI to the configured root.
aliaslocationReplaces the matching location prefix with a filesystem path. Trailing slash mistakes can make Nginx open the wrong path.
try_filesserver, locationChecks files or directories in order. A $uri/ check can trigger directory index handling.
allow and denyhttp, server, location, limit_exceptPermit or block requests by address. Nginx checks these rules in sequence until the first match.
disable_symlinkshttp, server, locationCan 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 off for 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 777 to 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.

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.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show our tutorials more often in Top Stories and mark them as preferred in AI Mode and AI Overviews when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Add to the discussion

Questions, fixes, command output, and version notes help keep this guide current.

Verify before posting: