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:
- Filesystem permissions — the Nginx worker user lacks read access to the file, or lacks execute (search) access on a parent directory.
- Incorrect document root — the
rootdirective points to a path that is empty, wrong, or not the one you deployed to. - Missing index file — the
indexdirective listsindex.htmlbut onlyindex.phpexists, with no fallback configured. - autoindex off — directory listing is disabled and no index file is present, so Nginx returns 403 instead of listing files.
- deny directives — an
allow/denyrule explicitly blocks the client IP. - IP or geo restrictions —
geoormapblocks, or a WAF, reject the request. - SELinux or AppArmor — mandatory access control labels prevent Nginx from reading files even when Unix permissions look correct.
- Symlink failures —
disable_symlinksis on, or the symlink target is outside an allowed path.
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.