nginx 403 Forbidden: Diagnose and Fix the 6 Root Causes in 5 Minutes
"Why is a page that was working a minute ago returning 403?"
You just finished a deploy and the browser shows 403 Forbidden. A lot of people reflexively blast chmod 777 at this point. If you're lucky it unblocks, but you've only left a security hole—and the next time 403 shows up, you'll wander the same maze.
The key point first: nginx 403 is not just a permissions problem. There are at least six causes, and each leaves a different string in error_log. The right approach is not guessing at permissions—it's pinpointing the cause from a single log line.
Don't panic. Look at the logs first.
# 가장 먼저 실행할 명령 — 실제 원인 문자열을 확인
sudo tail -n 30 /var/log/nginx/error.logGet oriented in 5 seconds: branch on the error_log string
When you open the log, you'll usually see one of the three strings below. That's your diagnostic branch point.
| Representative error_log string | Likely cause |
|---|---|
directory index of "/var/www/html/" is forbidden | ② Missing index file (autoindex off) |
access forbidden by rule | ⑤ deny rule |
Permission denied (errno 13) | ① File/directory permissions or ④ SELinux |
Real log samples look like this.
2026/06/14 10:21:33 [error] 812#812: *5 directory index of "/var/www/html/" is forbidden, client: 10.0.0.5 ...
2026/06/14 10:24:01 [error] 812#812: *7 access forbidden by rule, client: 10.0.0.5 ...
2026/06/14 10:27:18 [error] 812#812: *9 open() "/var/www/html/index.html" failed (13: Permission denied) ...Seeing Permission denied but permissions look fine? Then it's almost certainly SELinux. (Especially common on RHEL/CentOS/Rocky families.)
Classification table: the 6 causes of 403
| # | Cause | error_log string | One-line diagnostic command | Core fix |
|---|---|---|---|---|
| ① | File/directory permissions | Permission denied (13) | namei -l /var/www/html/index.html | Directories 755, files 644, matching owner |
| ② | Missing index | directory index ... is forbidden | ls -la /var/www/html | Place an index file or autoindex on |
| ③ | Misconfigured root/alias path | open() ... No such file or 403 | nginx -T | grep -E 'root|alias' | Check the path; fix alias trailing slashes |
| ④ | SELinux context | Permission denied (13) | getenforce + ausearch -m avc | chcon -t httpd_sys_content_t |
| ⑤ | deny rule | access forbidden by rule | nginx -T | grep -E 'deny|allow' | Revisit allow/deny order and target IPs |
| ⑥ | try_files / location conflict | (403 with no explicit error) | Review location blocks with nginx -T | Sort out try_files and location precedence |
Copy-paste diagnostic and fix commands
ls -la /var/www/html # 파일/디렉터리 소유자·권한 한눈에 확인
namei -l /var/www/html/index.html # 경로 전체(상위 디렉터리 포함) 권한을 단계별 추적
sudo chown -R www-data:www-data /var/www/html # Debian/Ubuntu 소유자 일치 (RHEL은 nginx:nginx)
sudo chmod -R 755 /var/www/html # 디렉터리 진입권한 부여 (파일은 추후 644 권장)
getenforce # SELinux 모드 확인 (Enforcing이면 의심)
sudo chcon -R -t httpd_sys_content_t /var/www/html # nginx가 읽을 수 있는 컨텍스트 부여
sudo ausearch -m avc -ts recent # 최근 SELinux 거부(AVC) 로그 확인
nginx -t && systemctl reload nginx # 설정 문법 검증 후 무중단 리로드⚠️ Owner caveat: Debian/Ubuntu uses
www-data; RHEL/CentOS/Rocky usesnginxas the default worker user. Confirm the actual worker user withps aux | grep nginx.
Practical fixes by scenario
Even for the same 403, the likely causes differ by how you serve content.
Static sites — ② and ⑥ are common
server {
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404; # index 부재 시 403 대신 404로 명확히
}
}If try_files includes $uri/, nginx tries directory auto-lookup and returns 403 when there's no index. Explicitly using =404 makes the intent clear.
Reverse proxy — missing location match
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
# location / 가 정의 안 되어 정적 root를 찾다 403/404If you get a 403 on a reverse proxy, it may be a 403 returned by the backend (upstream), not nginx. If error_log has no nginx-specific string, suspect the upstream response.
PHP-FPM — try_files + fastcgi pattern
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}If you omit try_files $uri =404, a 403 can occur for PHP paths that don't exist.
A field note: these days, containers are the real trap
The #1 cause of the recent surge in 403 tickets in the field is, surprisingly, UID/GID mismatch on Docker/k8s volume mounts. Host files owned by 1000:1000 can't be read by nginx (UID 101) inside the container, so you get Permission denied. In that case, check the worker UID with id inside the container before you chmod on the host. And on RHEL-family systems, setenforce 0 because "just turn off SELinux" is only a stopgap. Fixing the context is the right answer, and if you codify permissions and context with Ansible, recurrence almost disappears.
5-minute diagnostic checklist
- Check the last 30 lines of
error_log→ branch on the string index/directorystring → autoindex or place an indexaccess forbidden by rule→ inspect deny/allow orderPermission denied→ trace path permissions withnamei -l- Denied even though permissions look fine → check SELinux with
getenforce+ausearch - Finish with
nginx -t && systemctl reload nginx
Related reading: if uploads are blocked, see 413 Request Entity Too Large; if the backend connection drops, see the 502/504 Gateway troubleshooting guide.
References: official docs
The primary source for the behavior, settings, and errors covered in this post is the official documentation below. Check it for version-specific options and exact behavior.
Frequently asked questions (FAQ)
Q. It was fixed with chmod 777 — why shouldn't I use that? A. Anyone can read, write, and execute, so the risk of webshell upload and tampering is high. The standard is 755 for directories and 644 for files; grant write only on directories that actually need it.
Q. File permissions are all correct, but I still get 403.
A. It's often blocked at a parent directory (e.g. a home directory with 700). Tracing the full path with namei -l /home/user/site/index.html shows in one line which hop is blocking. For the nginx worker to reach the end of the path, every parent directory needs execute (x) permission.
Q. Can't I just turn SELinux off?
A. setenforce 0 drops an entire security layer. On RHEL-family systems the recommended path is to stay Enforcing: fix only the context with chcon -t httpd_sys_content_t, and make it persistent with semanage fcontext.
Q. Why does a trailing slash on alias flip between 403 and 404?
A. alias substitutes the location path, so slash alignment matters. Match slashes on both sides, as in location /img/ { alias /data/img/; }. If only one side has the slash, the path goes wrong and you get 403/404.
Q. I'm on a reverse proxy and getting 403 — is it an nginx problem?
A. Not necessarily. nginx may have proxy_passed correctly and the backend (upstream) returned 403. If error_log has no nginx-specific string, check the backend application logs.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.