/인프라/nginx 403 Forbidden Error: 5-Minute Diagnosis and Fix Guide by 6 Root Causes
Infrastructurenginx 403403 Forbidden

nginx 403 Forbidden Error: 5-Minute Diagnosis and Fix Guide by 6 Root Causes

Classify nginx 403 Forbidden into six root causes—permissions, missing index, root/alias, SELinux, deny, and try_files—diagnose from error_log strings, and fix each case on the spot with copy-paste commands like ls, namei, and chcon.

nginx 403 Forbidden Error: 5-Minute Diagnosis and Fix Guide by 6 Root Causes

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.

Bash
# 가장 먼저 실행할 명령 — 실제 원인 문자열을 확인
sudo tail -n 30 /var/log/nginx/error.log

Get 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 stringLikely 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.

LOG
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

#Causeerror_log stringOne-line diagnostic commandCore fix
File/directory permissionsPermission denied (13)namei -l /var/www/html/index.htmlDirectories 755, files 644, matching owner
Missing indexdirectory index ... is forbiddenls -la /var/www/htmlPlace an index file or autoindex on
Misconfigured root/alias pathopen() ... No such file or 403nginx -T | grep -E 'root|alias'Check the path; fix alias trailing slashes
SELinux contextPermission denied (13)getenforce + ausearch -m avcchcon -t httpd_sys_content_t
deny ruleaccess forbidden by rulenginx -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 -TSort out try_files and location precedence

Copy-paste diagnostic and fix commands

Bash
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 uses nginx as the default worker user. Confirm the actual worker user with ps 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

Nginx
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

Nginx
location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}
# location / 가 정의 안 되어 정적 root를 찾다 403/404

If 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

Nginx
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

  1. Check the last 30 lines of error_log → branch on the string
  2. index/directory string → autoindex or place an index
  3. access forbidden by rule → inspect deny/allow order
  4. Permission denied → trace path permissions with namei -l
  5. Denied even though permissions look fine → check SELinux with getenforce + ausearch
  6. 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.