홈/보안/SELinux Permission denied 해결법 — chmod 정상인데 막힐 때(avc denied)
보안SELinuxavc denied

SELinux Permission denied 해결법 — chmod 정상인데 막힐 때(avc denied)

chmod·chown은 정상인데 nginx·httpd가 Permission denied를 낸다면 SELinux가 범인입니다. getenforce·ausearch·audit2allow·restorecon·setsebool로 avc denied를 분기별 진단하고 복붙 명령으로 바로 푸는 실전 가이드.

SELinux Permission denied 해결법 — chmod 정상인데 막힐 때(avc denied)
목차
  1. chmod 777을 줘도 안 풀리는 'Permission denied'의 정체
  2. 1단계 — 범인이 SELinux인지 30초 만에 확인하기
  3. 2단계 — 원인 분기별 정밀 해결 (핵심 섹션)
  4. 3단계 — 'setenforce 0' 임시 우회의 함정과 올바른 사용법
  5. 결론: 복붙 가능한 명령 치트시트
  6. 참고: 공식 문서
  7. 자주 묻는 질문 (FAQ)

SELinux 'Permission denied' 완벽 진단 가이드 — chmod는 정상인데 접근이 막힐 때

chmod 777을 줘도 안 풀리는 'Permission denied'의 정체

분명히 chmod 777을 주고 chown nginx:nginx까지 했는데도 nginx가 502를 뱉거나, 웹 페이지가 403을 던지거나, 애플리케이션 로그에 Permission denied가 찍힙니다. ls -l로 봐도 권한은 멀쩡합니다. 이럴 때 가장 먼저 의심할 대상은 SELinux입니다.

우리가 익숙한 chmod/chown은 DAC(임의적 접근 제어)입니다. 그런데 RHEL·Rocky·AlmaLinux 9/10 계열은 기본적으로 SELinux(MAC, 강제적 접근 제어)가 Enforcing 상태로 켜져 있습니다. DAC를 통과해도 MAC 라벨(컨텍스트)이 맞지 않으면 커널이 한 번 더 막습니다. 그래서 권한이 정상이어도 거부가 납니다.

이 글은 SELinux 개념 강의가 아닙니다. 지금 막힌 접근을 당장 푸는 명령을 분기별로 정리한 실전 가이드입니다. 급하면 1단계로 범인을 확정하고, 2단계에서 본인 증상에 맞는 블록만 복붙하세요.

1단계 — 범인이 SELinux인지 30초 만에 확인하기

먼저 SELinux가 켜져 있는지 한 줄로 봅니다.

Bash
getenforce
# Enforcing  → SELinux가 막고 있을 가능성 높음
# Permissive → 로그만 남기고 막지 않음 (다른 원인 의심)
# Disabled   → SELinux는 무죄

Enforcing이면 차단 로그를 잡습니다. 실시간으로 보려면:

Bash
tail -f /var/log/audit/audit.log | grep denied
# 다른 터미널에서 막힌 동작(웹 새로고침 등)을 다시 시도

이미 발생한 로그는 ausearch로 잡습니다.

Bash
ausearch -m avc -ts recent      # 최근 10분
ausearch -m avc -ts today       # 오늘 전체

그리고 왜 막혔는지 사람이 읽을 수 있게 번역해 줍니다.

Bash
ausearch -m avc -ts recent | audit2why

setroubleshoot가 설치돼 있다면 sealert가 친절한 해결책까지 제안합니다.

Bash
dnf install -y setroubleshoot-server
sealert -a /var/log/audit/audit.log

증상별로 어디를 볼지 먼저 정리하면 다음과 같습니다.

증상로그에서 보이는 것판정
파일 읽기 Permission denied / 403tclass=file, tcontext가 default_t·user_home_t 등파일 컨텍스트(①)
서비스가 포트를 못 엶{ name_bind }, tclass=tcp_socket포트 라벨(②)
리버스 프록시 502, 원격 DB 연결 실패{ name_connect }boolean 또는 포트 타입(③)
컨테이너가 마운트한 볼륨을 못 읽음tcontext가 container_file_t가 아님볼륨 라벨(Podman :Z/:z)

avc: denied 로그 한 줄 해부

실제 로그는 이렇게 생겼습니다. 어디를 봐야 하는지 화살표로 짚어 보겠습니다.

CODE
type=AVC msg=audit(1718440000.123:456): avc:  denied  { read } for
  pid=2314 comm="nginx"                ← ① 누가 (프로세스 이름)
  name="index.html" dev="vda1" ino=12345
  scontext=system_u:system_r:httpd_t:s0          ← ② 출발지 라벨(프로세스)
  tcontext=unconfined_u:object_r:user_home_t:s0  ← ③ 도착지 라벨(파일) ← 범인!
  tclass=file permissive=0              ← ④ 무엇에 대한 접근인지(file/dir/tcp_socket…)

해석은 단순합니다. httpd_t(웹서버)가 user_home_t(홈 디렉터리 라벨이 붙은 파일)를 read하려다 막혔다는 뜻입니다. 웹 콘텐츠는 httpd_sys_content_t여야 하는데 라벨이 틀려서 거부된 전형적인 케이스죠. 즉 tcontext가 잘못된 컨텍스트라는 게 핵심 단서입니다.

2단계 — 원인 분기별 정밀 해결 (핵심 섹션)

로그의 tclass와 tcontext를 보면 어느 분기인지 거의 판별됩니다.

① 파일 컨텍스트가 틀린 경우 (가장 흔함)

tclass=file/dir이고 라벨이 default_t, user_home_t, var_t 등 엉뚱하게 붙어 있으면 컨텍스트 문제입니다. 먼저 확인:

Bash
ls -Z /var/www/html
# unconfined_u:object_r:default_t:s0  index.html   ← 잘못됨

표준 경로라면 정책 기본값으로 복원하면 끝입니다.

Bash
restorecon -Rv /var/www/html

정책이 기대하는 라벨과 실제 라벨을 비교하면 확실합니다.

Bash
matchpathcon /var/www/html/index.html   # 정책상 기대 라벨 (libselinux-utils)
restorecon -Rnv /var/www/html           # -n: 바꾸지 않고 무엇이 바뀔지만 출력

chcon으로 라벨을 바꾸면 당장은 풀리지만 정책 규칙과 다르면 다음 restorecon이나 전체 재라벨링 때 되돌아갑니다. 영구 해결은 semanage fcontext로 규칙을 등록하고 restorecon으로 적용하는 것입니다.

웹루트를 /data/www 같은 비표준 경로로 옮겼다면, 먼저 그 경로의 기본 컨텍스트 규칙을 등록한 뒤 복원합니다.

Bash
semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
restorecon -Rv /data/www

② 비표준 포트·디렉터리 정책이 필요한 경우

nginx를 정책에 없는 포트로 띄우면 tclass=tcp_socket, name_bind 거부가 납니다. 먼저 그 포트에 이미 타입이 있는지 확인하고 포트 라벨을 붙이세요.

Bash
semanage port -l | grep -w 8090        # 출력이 없으면 아직 정의되지 않은 포트
semanage port -a -t http_port_t -p tcp 8090

# 8080처럼 이미 다른 타입(http_cache_port_t)으로 정의된 포트는 -a가
# "Port tcp/8080 already defined"로 실패하므로 -m으로 바꿈
semanage port -m -t http_port_t -p tcp 8080
semanage port -l -C                    # 로컬에서 바꾼 포트 정의 확인

③ boolean이 꺼져 있는 경우

가장 놓치기 쉬운 케이스입니다. 예를 들어 PHP가 외부 API나 DB에 붙어야 하는데 httpd_can_network_connect가 꺼져 있으면 네트워크 연결이 막힙니다. 현재 값 확인 후 켭니다.

Bash
getsebool -a | grep httpd
setsebool -P httpd_can_network_connect on

자주 막히는 boolean 비교표:

Boolean용도켜는 명령
httpd_can_network_connect웹서버가 외부 호스트/API로 아웃바운드 연결setsebool -P httpd_can_network_connect on
httpd_can_network_connect_db웹서버가 원격 DB(MySQL/PostgreSQL)에 접속setsebool -P httpd_can_network_connect_db on
httpd_use_nfsNFS 마운트 경로를 웹 콘텐츠로 사용setsebool -P httpd_use_nfs on
httpd_enable_homedirs~user/public_html 제공setsebool -P httpd_enable_homedirs on
nis_enabledNIS/외부 인증 연동 시 광범위 네트워크 허용setsebool -P nis_enabled on

-P 옵션이 생명입니다. -P 없이 setsebool을 실행하면 메모리에만 적용돼 재부팅하면 원래대로 돌아갑니다. "분명히 켰는데 서버 재시작 후 또 막혔다"면 먼저 -P 누락을 의심하세요. 영구 적용은 반드시 -P.

컨테이너(Podman) 볼륨: 컨테이너가 마운트한 디렉터리를 못 읽으면 podman run -v /data:/data:Z처럼 :Z를 붙여 그 컨테이너 전용 라벨을 씌웁니다. 여러 컨테이너가 함께 쓰는 디렉터리는 :z(공유 라벨)를 씁니다. /home이나 /usr 같은 시스템 디렉터리에는 쓰지 마세요. 호스트의 라벨이 바뀝니다.

④ 진짜 커스텀 정책이 필요한 경우

위 셋 어디에도 해당하지 않는 정상 동작인데 막힌다면, 차단 로그로 전용 정책 모듈을 만듭니다.

Bash
# 1) 무엇을 허용하게 될지 먼저 읽기
ausearch -c 'nginx' --raw | audit2allow
# 2) nginx가 발생시킨 거부만 모아 모듈 생성 (nginxlocal.te / nginxlocal.pp)
ausearch -c 'nginx' --raw | audit2allow -M nginxlocal
cat nginxlocal.te            # 허용 규칙을 눈으로 검토
# 3) 검토가 끝난 뒤에만 설치
semodule -i nginxlocal.pp

설치 후 동작을 재시도하고 더 이상 새 avc: denied가 안 뜨는지 검증하세요. 모듈 목록은 semodule -l | grep nginxlocal로 확인합니다.

3단계 — 'setenforce 0' 임시 우회의 함정과 올바른 사용법

급한 마음에 setenforce 0을 치면 거짓말처럼 풀립니다. Permissive 모드는 거부를 차단하지 않고 로그만 남기기 때문이죠. 하지만 운영 서버를 그대로 Permissive로 두는 건 보안 통제를 통째로 내려놓는 것과 같습니다. 침해 시 SELinux가 막아 줬을 측면 이동·파일 접근이 전부 열립니다.

올바른 워크플로우는 "끄기"가 아니라 "진단용 잠깐 열기"입니다.

시스템 전체를 Permissive로 바꾸는 대신 문제 도메인 하나만 permissive로 두면 다른 서비스의 보호는 그대로 유지됩니다.

Bash
semanage permissive -a httpd_t     # httpd_t(nginx·Apache)만 permissive
# 막혔던 동작을 재현해 거부 로그 수집 → 위 ①~④로 해결
semanage permissive -d httpd_t     # 진단이 끝나면 반드시 해제
semanage permissive -l             # permissive 도메인 목록 확인

도메인을 특정하기 어렵다면 전체를 잠깐 Permissive로 바꾸는 방법도 있습니다.

Bash
setenforce 0                 # 진단용으로 잠깐 Permissive
# 막혔던 동작을 전부 한 번씩 재현 → 모든 거부 로그 수집
ausearch -m avc -ts recent | audit2allow -M myapp
semodule -i myapp.pp         # 수집한 정책을 정식 적용
setenforce 1                 # 다시 Enforcing으로 복귀

/etc/selinux/config에서 SELINUX=disabled로 영구 비활성화하는 것은 최후의 수단입니다. 한 번 disabled로 부팅하면 파일 라벨이 갱신되지 않아, 나중에 다시 켤 때 전체 파일시스템 재라벨링(touch /.autorelabel 후 재부팅)이 필요해 운영 부담이 큽니다. 클라우드 이미지·컨테이너 호스트에서 SELinux 활성 비중이 늘어나는 요즘 흐름을 봐도, "끄는 게 아니라 다루는 것"이 정석입니다.

자주 나오는 사례: 배포할 때마다 간헐적으로 403이 뜨는 경우, CI가 rsync로 파일을 올리면서 컨텍스트가 default_t로 덮이는 것이 흔한 원인입니다. 배포 스크립트 마지막 줄에 restorecon -Rv /var/www/html 한 줄을 넣자 재발이 사라졌습니다. SELinux 문제의 상당수는 "거창한 정책"이 아니라 컨텍스트 복원 한 줄로 끝납니다.

설정을 서버에서 손으로 바꿨다면 Ansible의 community.general.sefcontext, ansible.posix.seboolean, community.general.seport 모듈로 코드에 남겨 두세요. 서버를 다시 만들어도 같은 문제가 반복되지 않습니다.

결론: 복붙 가능한 명령 치트시트

권한은 정상인데 거부가 날 때, 아래 순서대로만 가면 됩니다.

증상진단해결
403 / Permission denied (권한 정상)getenforce → Enforcing?아래로 진행
어떤 동작이 막혔는지 모름ausearch -m avc -ts recent | audit2why원인 분기 판별
파일 라벨이 틀림(ls -Z)tcontext가 default_t 등restorecon -Rv /var/www/html
비표준 웹루트라벨 규칙 없음semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" → restorecon -Rv
비표준 포트tclass=tcp_socket, name_bind미정의 포트는 semanage port -a -t http_port_t -p tcp 8090, 이미 정의된 8080은 -m
외부 연결/DB 막힘getsebool -a | grep httpdsetsebool -P httpd_can_network_connect on
위 어디에도 없음정상 동작인데 거부ausearch -c 'nginx' --raw | audit2allow -M nginxlocal && semodule -i nginxlocal.pp

마무리 체크리스트: ① getenforce로 범인 확정 → ② ausearch로 로그 확보 → ③ 분기에 맞는 명령 적용 → ④ setsebool엔 항상 -P → ⑤ 진단이 끝나면 setenforce 1로 복귀.

참고: 공식 문서

이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.

자주 묻는 질문 (FAQ)

Q. audit2allow가 만들어 준 정책을 그냥 다 allow 해도 되나요? A. 위험합니다. audit2allow는 "막힌 동작을 전부 허용"하는 규칙을 생성할 뿐, 그 동작이 정당한지는 판단하지 않습니다. 반드시 | audit2allow로 어떤 권한을 여는지 먼저 사람이 읽고, 정상 동작에 필요한 것만 모듈로 만드세요. 침해로 발생한 거부까지 통째로 허용하면 보안 구멍이 됩니다.

Q. setsebool로 켰는데 재부팅하면 또 막혀요. A. -P 옵션을 빠뜨렸을 확률이 가장 높습니다. -P 없이 실행하면 런타임 메모리에만 적용되어 재부팅 시 초기화됩니다. setsebool -P ...로 영구 적용하세요.

Q. 그냥 SELinux를 꺼버리면 안 되나요? A. 당장은 편하지만 권장하지 않습니다. RHEL 9·Rocky·AlmaLinux 9/10은 기본 Enforcing이며 클라우드·컨테이너 환경에서 SELinux 활용이 늘고 있습니다. 진단이 필요하면 잠깐 setenforce 0으로 로그를 수집하고, 정책을 적용한 뒤 setenforce 1로 돌아오는 워크플로우를 쓰는 게 안전합니다.

확인 정보
✦ ✦ ✦
출처·검증 안내

글마다 출처와 확인 범위가 다릅니다. 아래에 확인일·확인 범위·출처가 기록된 글만 그 범위에서 확인된 것입니다. 기록이 없는 글은 적용 전 사용 중인 버전의 공식 문서로 직접 확인하고, 오류는 이메일로 알려주세요. 편집 정책 · 이메일로 제보

이 글에는 사람이 확인한 날짜·범위 기록이 아직 없습니다.

발행 · ·업데이트 ·
주제 관련 공식 문서(자동 연결 · 문장별 근거 아님)NGINX 공식 문서 ↗GNU/Linux man 페이지 ↗OWASP 공식 문서 ↗

댓글

첫 번째 댓글을 남겨보세요.