How to Fix Nginx 403 Forbidden Error: Complete Guide

A 403 Forbidden response from Nginx means the web server understood the request but deliberately refuses to fulfill it. Unlike a 404 Not Found, the resource usually exists — or at least Nginx knows where to look — but something in the access control, filesystem permissions, or security policy is blocking delivery. The page simply never reaches the visitor.

403 errors are among the most frequent Nginx problems on fresh deployments, after server migrations, or following a package update that resets SELinux contexts. The good news is that almost every 403 has a clear, discoverable cause, and Nginx records the reason in its error log. This guide walks through what a 403 means, the root causes you should check, and a six-step fix process that resolves the vast majority of cases.

What is Nginx 403 Forbidden?

A 403 Forbidden is an HTTP status code in the 4xx range. Nginx sends it when it has been configured — or forced by the operating system — to deny access to the requested resource, even though the request itself is well-formed. In Nginx's own words, the error log typically reads access forbidden by rule or directory index of "..." is forbidden.

The critical distinction from a 404 is that Nginx located the file or directory but is not permitted to serve it. This makes 403 a permissions or policy problem rather than a routing problem. The browser cannot fix it; the change must happen on the server. A 403 is therefore never random — it is always a deliberate refusal, and the configuration or filesystem holds the answer.

Common Causes

Before diving into commands, understand the eight root causes behind nearly every Nginx 403:

Step-by-Step Fix Guide

Work through these six steps in order. After each change, test the configuration and reload Nginx before checking the result in a browser.

Step 1: Verify File Permissions on the Document Root

The single most common cause. Nginx runs as a worker user (usually nginx on RHEL, www-data on Debian/Ubuntu) and must be able to read every file and search every directory from the root down to the requested file.

# List ownership and permissions in the web root
ls -la /var/www/html

# Trace every directory in the path for execute (search) permission
namei -l /var/www/html/index.html

# Fix ownership (use www-data on Debian/Ubuntu, nginx on RHEL)
chown -R nginx:nginx /var/www/html

# Directories need 755, files need 644
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;

If namei shows a directory without x permission for others, Nginx cannot traverse it and will return 403 regardless of the file's own permissions.

Step 2: Confirm the Document Root Path Is Correct

A typo or a path left over from a migration is enough to produce a 403. Open your server block and check the root directive, then confirm the directory actually contains your files.

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com/public;   # verify this path exists
    index index.html index.htm;
}
# Confirm the path exists and contains an index file
ls -la /var/www/example.com/public

If you recently moved files, remember that the root value is literal — trailing slashes and case sensitivity matter.

Step 3: Check the index Directive and autoindex

If a directory is requested without a filename, Nginx looks for a file listed in the index directive. If none exists and autoindex is off (the default), Nginx returns 403.

location / {
    index index.html index.htm index.php;
    # autoindex on;   # uncomment only to list directory contents
}

Either add a matching index file to the directory, or enable autoindex on; if you genuinely want a directory listing. Never enable autoindex on sensitive directories.

Step 4: Review allow and deny Access Rules

An explicit deny all; or deny of a specific IP range will produce a 403 even when permissions are perfect. Search the configuration for these directives.

# Find every allow/deny rule across the Nginx config tree
grep -RIn "allow\|deny" /etc/nginx/
# Example of a rule that blocks everyone except localhost
location /admin {
    allow 127.0.0.1;
    deny all;
}

Remove or tighten the rule so legitimate clients are permitted, then reload Nginx.

Step 5: Inspect IP and Geo Restrictions

Beyond simple allow/deny, Nginx supports geo and map blocks that can return 403 for entire regions. A WAF such as ModSecurity can do the same. Check for these blocks and the return 403 statements they may trigger.

geo $blocked {
    default 0;
    10.0.0.0/8 1;
}

server {
    if ($blocked) { return 403; }
}

Log the matched value temporarily with error_log at the debug level if you cannot tell which rule is firing.

Step 6: Resolve SELinux or AppArmor Blocks

On RHEL, CentOS, and Fedora, SELinux is enforcing by default and will block Nginx from reading files whose context is not httpd_sys_content_t, even when Unix permissions are 755. On Debian-family systems, AppArmor can do the equivalent.

# Is SELinux enforcing?
getenforce
sestatus

# Fix file contexts and allow Nginx to read user content
restorecon -Rv /var/www/html
setsebool -P httpd_read_user_content on

# Check AppArmor (Debian/Ubuntu)
sudo aa-status | grep nginx

After correcting contexts, reload Nginx and re-test. SELinux denials are also recorded in /var/log/audit/audit.log — see the diagnostic commands below.

Nginx Diagnostic Commands

When a 403 appears, these commands pinpoint the cause in seconds. Always start with the error log.

# 1. Read the exact reason Nginx refused the request
tail -n 50 /var/log/nginx/error.log

# 2. Test configuration syntax before reloading
nginx -t

# 3. Reload without dropping active connections
nginx -s reload

# 4. Inspect ownership and permissions
ls -la /var/www/html
namei -l /var/www/html/index.html

# 5. Fix ownership and permissions
chown -R nginx:nginx /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;

# 6. Check SELinux status and contexts
getenforce
ls -Z /var/www/html
restorecon -Rv /var/www/html

# 7. Search recent SELinux denials
ausearch -m AVC,USER_AVC -ts recent

# 8. Check AppArmor status and Nginx profile mode
sudo aa-status

# 9. See which user Nginx workers run as
ps -ef | grep nginx | grep -v grep

Quick Reference Table

Symptom / Log Message Root Cause Fix
access forbidden by rule allow/deny or geo block Remove or adjust the rule; verify the client IP is permitted
directory index of "..." is forbidden No index file and autoindex off Add a matching index file or set autoindex on;
Permission denied reading file Filesystem permissions chown/chmod so the Nginx user can read and search
403 only on RHEL/CentOS/Fedora SELinux context mismatch restorecon -Rv and setsebool -P httpd_read_user_content on
403 on a symlinked path disable_symlinks or bad symlink target Set disable_symlinks off; or fix the target ownership
403 right after a migration Document root path wrong Correct the root directive and reload Nginx
403 for specific IPs only IP-based deny or geo rule Adjust geo/allow/deny rules

FAQ

Why am I getting a 403 when the file definitely exists?

Because Nginx found the file but is not allowed to serve it. The three usual suspects are filesystem permissions (the Nginx worker user cannot read the file or search a parent directory), SELinux/AppArmor labels, or an explicit deny rule. Run tail /var/log/nginx/error.log first — it states the exact reason.

What is the difference between 403 Forbidden and 404 Not Found?

A 404 means Nginx could not locate the resource at all. A 403 means Nginx located it but access was denied. If you see a 403, the path is correct; focus on permissions and access control rather than routing.

How do I enable directory listing instead of seeing a 403?

Add autoindex on; inside the relevant location block and make sure no index file is present (otherwise the index file is served). Reload Nginx with nginx -s reload. Use directory listing only for non-sensitive content.

Does SELinux really cause 403 on CentOS and RHEL?

Yes. SELinux is enforcing by default on RHEL-family systems and will block Nginx from reading files whose context is not httpd_sys_content_t, even when Unix permissions are 755. Run getenforce; if it returns Enforcing, apply restorecon -Rv /var/www/html and setsebool -P httpd_read_user_content on.

Conclusion

A 403 Forbidden from Nginx is always a deliberate refusal, never a guessing game. Start with the error log — it tells you whether the block is a directory index issue, a permission denial, or an access rule. From there, work through filesystem permissions, the document root, the index directive, allow/deny rules, IP and geo restrictions, and finally SELinux or AppArmor. Once the underlying permission or policy problem is corrected and nginx -t && nginx -s reload succeeds, the 403 disappears and the resource is served exactly as intended.

Related Guides