nginx 403 오류 해결: SELinux 권한 문제 확인 및 수정 방법
파일 권한이 정상인데도 nginx에서 403 오류가 발생한다면 SELinux 레이블 문제일 가능성이 높습니다. audit2why로 거부 원인을 분석하고 semanage fcontext와 restorecon 명령어를 사용하여 올바른 보안 컨텍스트를 적용하는 방법을 단계별로 설명합니다.
파일 권한이 정상인데도 nginx가 403 오류를 반환하는 이유
파일의 권한 비트가 올바른데도 nginx가 403 오류를 반환한다면, 거의 항상 SELinux(Security-Enhanced Linux)가 읽기를 거부하는 상황입니다. SELinux는 일반적인 권한 검사를 통과한 후 두 번째 규칙 집합을 확인하며, 웹 서버는 웹 콘텐츠 레이블이 지정된 파일만 읽을 수 있습니다. 해당 파일에 다른 레이블이 지정되어 있으면 열기 작업이 실패하고 nginx는 전송할 데이터를 찾지 못합니다.
권한 모드뿐만 아니라 레이블도 확인하십시오.
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmldrwxr-xr-x 뒤에 출력된 점(dot)은 해당 파일에 SELinux 레이블이 지정되어 있음을 의미합니다. default_t는 정책에 정의되지 않은 경로에 부여되는 레이블이며, 웹 서버 규칙상 해당 유형의 파일은 읽을 수 없습니다. 오류 로그에는 일반적인 Unix 오류가 표시되므로 권한 문제로 오인하기 쉽습니다.
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"커널은 일반적인 권한 거부와 SELinux에 의한 거부 모두에 대해 13: Permission denied을 반환합니다. 따라서 첫 번째 작업은 어느 계층에서 거부했는지 확인하는 것입니다. setenforce 0부터 시작하지 마십시오.
모델의 핵심 구성 요소
SELinux는 강제적 접근 제어(Mandatory Access Control, MAC)를 수행합니다. 모든 프로세스는 웹 서버를 위한 httpd_t와 같은 도메인 내에서 실행됩니다. 모든 파일과 네트워크 포트에는 httpd_sys_content_t과 같은 타입이 지정됩니다. 정책은 도메인, 타입, 작업의 허용 조합을 나열한 목록이며, 이 목록에 없는 모든 작업은 거부됩니다. SELinux는 기존의 Unix 권한 검사 이후에 작동하므로, drwxr-xr-x와 같은 권한 비트가 먼저 접근을 허용해야 합니다. 두 계층 모두에서 승인되어야 접근이 가능합니다.
전체 컨텍스트는 system_u:system_r:httpd_t:s0과 같이 콜론으로 구분된 4개의 필드(SELinux 사용자, 역할, 타입, 레벨)로 구성됩니다. 서버 관리 시에는 세 번째 필드인 타입에 대부분의 시간을 할애하게 됩니다. 다음 명령어를 통해 실시간 값을 확인할 수 있습니다.
ps -eZ | grep nginx
id -ZNginx 워커 프로세스는 httpd_t로 끝나는 컨텍스트를 보여줍니다. 사용자의 로그인 셸은 unconfined_u:unconfined_r:unconfined_t:s0를 보여주는데, 이는 기본 targeted 정책이 서비스는 제한하고 대화형 사용자는 그대로 두기 때문입니다. SELinux는 최소 권한 사용자로 서비스 실행하기를 대체하지 않는다는 점을 기억해야 합니다. SELinux는 누군가 서비스에 침입했을 때 해당 서비스가 접근할 수 있는 범위를 제한하는 역할을 합니다.
세 가지 모드와 SELinux가 포함된 이미지
sestatus
getenforceEnforcing 모드는 접근을 차단하고 기록합니다. Permissive 모드는 모든 접근을 허용하되 차단했을 작업을 기록합니다. Disabled 모드는 정책을 전혀 로드하지 않습니다. getenforce은 현재 모드를 출력합니다. sestatus는 /etc/selinux/config에 설정된 모드도 함께 출력하며, 이 설정은 재부팅 후에도 유지됩니다.
Rocky Linux, AlmaLinux, Fedora 및 RHEL은 SELinux를 Enforcing 모드로 설정하고 targeted 정책을 기본으로 제공합니다. 반면 Ubuntu와 Debian은 동일한 역할을 수행하지만 메커니즘이 다른 AppArmor를 사용합니다(마지막 섹션에서 다룹니다). 따라서 동일한 애플리케이션이라도 서버 환경에 따라 정상적으로 설치되거나 403 오류를 반환할 수 있습니다.
필요하기 전에 도구를 설치하십시오
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server최소 설치 이미지에서 semanage: command not found를 실행할 때 policycoreutils-python-utils이 없다는 것은 해당 패키지가 semanage 및 audit2allow을 포함하고 있기 때문입니다. setroubleshoot-server를 설치하면 sealert이 추가되며, 각 거부 사례에 대한 평이한 요약 정보가 저널에 기록됩니다. 서버를 새로 구축할 때 이 두 가지를 모두 설치하십시오. 도구가 필요한 시점은 이미 무언가가 고장 난 상황이기 때문입니다.
SELinux 거부 로그를 읽는 방법
각 거부 사례는 audit daemon에 의해 AVC(access vector cache) 메시지로 기록됩니다.
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0네 개의 필드가 전체 상황을 설명합니다. comm은 차단된 프로그램입니다. scontext는 소스 컨텍스트로, 프로세스가 실행 중이던 도메인입니다. tcontext은 타겟 컨텍스트로, 프로세스가 접근하려던 대상에 붙은 라벨입니다. tclass는 객체의 종류이며, 여기서는 파일입니다. 이를 종합하면 httpd_t의 프로세스가 default_t 라벨이 붙은 파일에 접근하려 했으며, permissive=0은 해당 요청이 단순히 기록된 것이 아니라 실제로 차단되었음을 나타냅니다.
ausearch이 아무것도 출력하지 않는다면 audit daemon이 실행 중이지 않을 수 있습니다. 이 경우 거부 기록은 커널 링 버퍼에 남습니다.
sudo journalctl -k | grep -i avc이제 기록을 문장으로 해석해 봅니다.
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why는 동일한 기록을 읽어 인식 가능한 원인을 알려줍니다. 꺼져 있는 불리언, 정책과 일치하지 않는 라벨, 또는 규칙 부재 등이 원인이 될 수 있습니다. sealert은 로그 전체를 분석하여 거부 사례마다 권장 명령어를 출력합니다. 이 제안은 힌트로만 활용하십시오. 릴리스마다 문구가 달라지며, sealert은 한 줄의 라벨 수정으로 해결될 문제를 때때로 사용자 정의 정책 모듈로 제안하기도 합니다.
한 가지 더 알아둘 점이 있습니다. 정책에는 무해하다고 판단되는 거부를 숨기는 dontaudit 규칙이 포함되어 있어, 프로그램이 오작동하더라도 로그가 비어 있을 수 있습니다. 테스트를 진행하는 동안만 이 규칙을 해제하십시오.
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -Bsemanage fcontext와 restorecon으로 잘못 지정된 경로 수정하기
두 명령어를 사용해야 하며, 순서가 중요합니다. semanage fcontext -a은 특정 경로가 가져야 할 레이블을 기록합니다. restorecon는 기록된 기본값을 디스크의 파일에 적용합니다.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html경로는 정규 표현식으로 지정합니다. (/.*)?는 디렉터리 자체와 그 하위의 모든 항목을 포함하며, 이는 문서 루트(document root)에 필요한 설정입니다. 변경하기 전에 어떤 내용이 바뀔지 확인하십시오. sudo restorecon -Rvn /data/www은 -n 옵션을 사용하므로 실제 변경 없이 계획된 재레이블링(relabel) 내역만 출력합니다. 실제 restorecon을 실행한 후에는 레이블이 httpd_sys_content_t로 변경되며, 서비스 재시작 없이 403 오류가 해결됩니다.
chcon은 테스트 용도로만 사용하십시오. chcon -t httpd_sys_content_t index.html은 레이블을 직접 설정하지만, 다음 restorecon 실행, 패키지 업데이트 또는 전체 재레이블링 시 정책에 정의된 기본값으로 초기화됩니다. semanage fcontext을 사용해야 설정이 유지됩니다. 기록된 목록은 sudo semanage fcontext -l | grep '^/data'로 확인할 수 있습니다.
서비스가 데이터를 기록해야 하는 경로에는 다른 타입이 필요합니다. 업로드 디렉터리나 캐시에는 httpd_sys_rw_content_t를 사용하고, 해당 경로에만 제한적으로 적용하십시오. 읽기 전용 사이트에 쓰기 가능한 타입을 설정하면 애플리케이션이 필요한 것보다 더 많은 접근 권한을 허용하게 됩니다.
왜 레이블이 잘못 지정되었을까요? 거의 대부분 파일이 이동하거나 복사된 방식 때문입니다. mv은 파일의 기존 레이블을 유지하므로, /root에서 이동한 사이트는 admin_home_t 레이블을 그대로 유지합니다. 일반적인 cp는 대상 디렉터리의 기본 레이블을 새 파일에 부여하므로 보통 의도한 대로 동작하지만, cp -a 및 rsync -X은 원본의 레이블을 파일과 함께 복사합니다. 새로운 최상위 디렉터리로 git clone를 수행하면 default_t이 발생합니다. /usr/share/nginx/html에서 페이지가 정상적으로 로드되는데 사용자가 생성한 디렉터리에서 실패한다면, 바로 이 이유 때문입니다.
부울 값을 사용하여 동작 문제 해결하기
일부 오류는 레이블 설정 문제가 아닙니다. 새로 설치한 Rocky 또는 AlmaLinux 서버에서 리버스 프록시가 502 오류를 반환하며, 오류 로그에 다음 내용이 기록됩니다.
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstream업스트림 설정은 정상입니다. httpd_t 도메인은 기본적으로 아웃바운드 네트워크 연결을 허용하지 않으므로, connect() 호출이 루프백 인터페이스에 도달하기 전에 거부됩니다. 이 동작 전체를 제어하는 스위치는 다음과 같습니다.
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P은 중요한 플래그입니다. 이 플래그는 값을 디스크에 기록합니다. -P을 사용하지 않으면 재부팅 시 변경 사항이 사라지며, 결과적으로 서버를 재시작하기 전까지만 서비스가 정상 작동하게 됩니다. 실행 중인 값과 저장된 값을 함께 출력하는 semanage boolean -l | grep httpd_can_network_connect를 사용하여 확인하십시오.
직접 작성한 규칙보다는 부울 값을 사용하는 것이 좋습니다. 부울 값은 배포판 정책과 함께 제공되므로 유지 관리 및 문서화가 잘 되어 있으며, 다음 관리자가 쉽게 찾을 수 있습니다. getsebool -a은 시스템의 모든 부울 값을 나열합니다.
서비스가 비표준 포트에서 수신 대기하도록 설정하기
포트에도 레이블이 지정되어 있습니다. nginx를 8081 포트로 옮기면 시작을 거부합니다.
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t은 http_port_t 레이블이 지정된 포트만 바인딩할 수 있으며, 8081은 여기에 포함되지 않습니다. 해당 포트를 추가하십시오.
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081먼저 목록을 확인하십시오. 8008 및 8443을 포함하여 이미 여러 높은 포트 번호가 허용되어 있으며, 동일한 포트를 두 번 추가하면 ValueError: Port tcp/8081 already defined 오류가 발생합니다. 만약 해당 포트가 이미 다른 유형에 속해 있다면, 추가하는 대신 semanage port -m -t http_port_t -p tcp 8081를 사용하여 변경하십시오.
동일한 명령을 사용하여 변경된 SSH 포트가 작동하도록 설정할 수 있습니다. journalctl -u sshd 내의 Bind to port 2222 on 0.0.0.0 failed: Permission denied는 2222 포트가 ssh_port_t에 누락되었음을 의미하므로, 데몬을 재시작하고 세션을 종료하기 전에 sudo semanage port -a -t ssh_port_t -p tcp 2222을 실행하십시오. 이는 Red Hat 계열 이미지에서 VPS의 SSH 강화에 관한 일반적인 가이드를 따를 때 사람들이 자주 건너뛰는 단계입니다. SELinux는 방화벽이 아니므로 포트는 여전히 개방되어 있어야 합니다. sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload를 사용하거나 Debian 또는 Ubuntu 이미지의 ufw를 설정하십시오.
부울 값이나 레이블을 변경할 수 없는 경우
일반적인 서버 환경에서는 드문 경우이며, 사용자가 설정을 잘못 건드려 시스템에 손상을 입히는 상황이기도 합니다. audit2allow은 로그에 기록된 거부(denial) 기록을 바탕으로 정책 모듈을 생성할 수 있습니다.
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.pp설치하기 전에 nginx_local.te을 읽어보십시오. 다음 두 가지 습관을 지키면 안전하게 작업할 수 있습니다. 첫째, -c를 사용하여 수정하려는 특정 프로그램의 로그만 입력으로 필터링하십시오. 일주일 치의 무관한 거부 기록을 모두 audit2allow으로 넘기면 그 모든 권한이 한꺼번에 허용되기 때문입니다. 둘째, 이유를 설명할 수 없는 거부 기록으로 생성된 모듈은 절대 설치하지 마십시오. httpd_t가 서버의 모든 파일을 읽도록 허용하는 규칙은 생성하기는 쉽지만, 몇 달 뒤에 찾아내기는 매우 어렵습니다. 모듈을 제거할 때는 sudo semodule -r nginx_local를 사용하십시오.
Permissive 모드는 진단용이며 해결책이 아닙니다
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Permissive 모드는 접근을 허용하고 해당 기록을 로그에 남깁니다. 이 모드의 진정한 가치는 완전성에 있습니다. Enforcing 모드에서는 서비스가 첫 번째 거부 지점에서 중단되므로, 하나를 수정하고 재시작하면 다시 두 번째 거부를 만나게 됩니다. Permissive 모드에서는 실행이 계속되므로 로그가 한 번의 실행으로 모든 거부 사례를 수집하며, 이후 모드를 전환하여 한꺼번에 수정할 수 있습니다.
setenforce은 /etc/selinux/config을 건드리지 않으므로, 재부팅하면 시스템은 다시 Enforcing 모드로 돌아갑니다. 이는 안전장치이며, setenforce 0을 사용하여 수행한 "수정"이 최악의 순간에 다시 문제가 되는 이유이기도 합니다. 특정 서비스를 작업하는 동안 여유가 필요하다면, 시스템 전체가 아닌 해당 도메인만 설정하십시오. sudo semanage permissive -a httpd_t는 다른 모든 영역을 Enforcing 상태로 유지하며, sudo semanage permissive -d httpd_t은 이를 되돌립니다.
SELinux를 비활성화하는 것이 레이블을 수정하는 것보다 더 큰 비용을 치르는 이유
/etc/selinux/config에서 SELINUX=disabled을 설정하는 것은 한 줄의 레이블 수정으로 해결할 일을 서버의 보안을 영구적으로 약화시키는 결과로 바꿉니다. 이러한 차이는 웹 애플리케이션이 침해당하는 날 극명하게 드러납니다. enforcing 모드에서는 공격자의 코드가 httpd_t 내에서 실행되므로 웹 콘텐츠를 읽을 수는 있어도, Unix 사용자 권한이 허용하더라도 정책상 /etc/shadow를 읽거나 systemd 유닛을 수정하는 행위는 거부됩니다. 정책이 로드되지 않은 상태라면, 해당 코드는 서비스 계정이 가진 모든 권한을 그대로 탈취합니다.
비활성화에는 나중에 치러야 할 대가도 따릅니다. 정책이 로드되지 않은 동안 생성된 새 파일에는 레이블이 지정되지 않으므로, 파일 시스템이 정책과 어긋나게 됩니다. 이후 SELinux를 다시 켜면 전체 레이블 재지정(relabel)이 필요하며, 그렇지 않으면 다수의 서비스가 한꺼번에 실패합니다.
sudo fixfiles -F onboot
sudo reboot이 명령은 /.autorelabel를 기록하고 다음 부팅 시 모든 파일 시스템의 레이블을 다시 지정합니다. 디스크 용량이 크면 시간이 오래 걸리고 콘솔이 멈춘 것처럼 보일 수 있으므로, 대기 시간이 충분할 때 실행하십시오. Rocky Linux 및 AlmaLinux 9에서는 설정 파일만으로는 커널 수준의 기능을 완전히 끌 수 없으며, 공식적으로 안내되는 완전한 비활성화 방법은 커널 인자(sudo grubby --update-kernel ALL --args selinux=0)를 사용하는 것입니다. 이 명령을 알고 있으면 타인이 관리하던 서버를 인계받았을 때 유용합니다. 하지만 이는 403 오류에 대한 해결책이 아닙니다.
컨테이너에 레이블 하나 더 추가하기
Red Hat 계열 호스트에서 컨테이너 프로세스는 container_t 내에서 실행되며 container_file_t 레이블이 지정된 파일만 읽을 수 있습니다. 호스트에서 바인드 마운트를 수행하면 컨테이너 내부에서는 Permission denied 오류가 발생하지만, 호스트의 ls -l에서는 정상적으로 보입니다. :Z 접미사는 런타임에 마운트의 레이블을 다시 지정하도록 지시합니다.
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z는 해당 컨테이너 전용으로 디렉터리에 레이블을 지정합니다. :z은 컨테이너 간 공유를 위해 레이블을 지정합니다. :Z를 다른 서비스가 사용하는 디렉터리로 지정하면 해당 디렉터리의 레이블을 재귀적으로 다시 지정하게 되어 기존 서비스가 중단되므로, 컨테이너마다 고유한 경로를 부여해야 합니다. 설정의 나머지 부분은 다른 이미지와 동일하며, VPS에서 Docker 실행하기에서 다룹니다.
Ubuntu와 Debian은 AppArmor를 제공합니다
동일한 작업을 수행하지만 설계 방식이 다릅니다. AppArmor는 디스크상의 파일에 레이블을 지정하는 대신 /etc/apparmor.d/ 아래의 프로필을 사용하여 실행 파일의 경로를 기준으로 프로그램을 제한합니다. 따라서 레이블을 다시 지정할 필요가 없으며 restorecon도 존재하지 않습니다. 다음 명령으로 시작하십시오:
sudo aa-status
sudo journalctl -k | grep -i apparmor거부 사례는 apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r"로 나타납니다. 작업 흐름은 동일합니다. 거부 로그를 읽고, 프로필을 찾은 뒤, 규칙을 변경하십시오. sudo apt install apparmor-utils을 사용하면 aa-complain(특정 프로필에 대해 허용 모드 적용) 및 aa-enforce(원래 상태로 복구)을 수행할 수 있습니다. Ubuntu는 패키지화된 서비스 중 일부만 제한하고 나머지는 제한하지 않은 상태로 두므로, 추측하기보다 aa-status을 읽어 실제로 활성화된 항목을 확인하십시오.
두 시스템 모두에서 공통으로 적용되는 습관이 하나 있습니다. 서비스가 올바르게 보이는 항목에 대해 Permission denied를 보고할 경우, 권한을 수정하기 전에 보안 로그를 먼저 확인하십시오. 권한 비트가 문제인 경우는 드뭅니다.
FAQ
파일 권한이 올바른데도 Nginx가 403 오류를 반환하는 이유는 무엇입니까?
파일 권한 비트가 아니라 SELinux가 읽기를 거부했기 때문입니다. 웹 서버는 httpd_t 도메인에서 실행되며 웹 콘텐츠로 레이블이 지정된 파일만 읽을 수 있습니다. 따라서 default_t 또는 admin_home_t로 레이블이 지정된 파일은 접근이 거부되며 Nginx는 제공할 콘텐츠를 찾지 못합니다. sudo ausearch -m AVC -ts recent 명령으로 이를 확인하십시오. 이 명령은 httpd_t로 끝나는 scontext과 잘못된 유형을 가진 tcontext를 보여줍니다. 그 후 올바른 레이블을 기록하고 적용하십시오: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"을 실행한 뒤 sudo restorecon -Rv /data/www을 수행합니다.
서비스를 작동시키기 위해 setenforce 0을 실행해도 안전합니까?
setenforce 0는 진단 단계일 뿐 해결책이 아닙니다. 로그가 모든 거부 사례를 한 번에 수집할 수 있도록 문제를 재현하는 용도로만 사용하고, sudo ausearch -m AVC -ts recent으로 로그를 읽은 뒤 sudo setenforce 1를 실행하여 원인을 수정하십시오. 서버를 permissive 모드로 두면 모든 거부 사례가 기록되지만 차단은 이루어지지 않으므로, 로그만 쌓이고 보호 기능은 상실됩니다. 작업 중에 특정 서비스에만 예외가 필요하다면 sudo semanage permissive -a httpd_t를 실행하여 나머지 시스템은 enforcing 상태를 유지하게 하십시오.
SELinux가 enforcing 상태일 때 비표준 포트에서 서비스를 실행하려면 어떻게 해야 합니까?
해당 서비스가 바인딩할 수 있는 유형에 포트를 추가하십시오. 8081 포트에서 웹 서버를 실행하려면 sudo semanage port -a -t http_port_t -p tcp 8081을 사용합니다. 2222 포트에서 SSH를 실행하려면 sudo semanage port -a -t ssh_port_t -p tcp 2222을 사용합니다. 이미 등록된 포트를 추가하면 ValueError: Port tcp/8081 already defined 오류가 발생하므로, 먼저 sudo semanage port -l | grep -w http_port_t로 현재 목록을 확인하십시오. 이 단계를 거치지 않으면 다른 프로세스가 포트를 점유하고 있지 않더라도 데몬이 시작 시 bind() ... Permission denied 오류와 함께 종료됩니다.
Ubuntu에도 SELinux가 있습니까?
아니요. Ubuntu와 Debian은 파일 레이블이 아닌 실행 파일 경로에 연결된 프로파일을 강제하는 AppArmor를 사용합니다. sudo aa-status로 이를 확인하고 sudo journalctl -k에서 apparmor="DENIED" 줄을 찾으십시오. Ubuntu는 선택된 패키지 서비스만 제한하므로, 많은 프로그램이 기본적으로 제한 없이 실행됩니다. SELinux가 기본적으로 enforcing 상태인 환경은 Fedora, RHEL을 비롯하여 Rocky Linux와 AlmaLinux입니다.