Rocky Linux나 AlmaLinux 9로 서버를 새로 올리면 CentOS 7 시절과 달리 SELinux가 Enforcing으로 켜진 채 시작한다. 설정 파일도 방화벽도 다 맞는데 Nginx가 502를 뱉거나 Tomcat이 파일을 못 읽는 상황이 여기서 자주 나온다. 로그만 봐서는 원인이 안 보이고, 결국 SELinux를 통째로 끄고 넘어가는 경우를 현장에서 많이 본다. 이 글은 그 대신 무엇이 막혔는지 정확히 짚어 최소한으로 여는 방법을 정리한다.
정리하면, 애플리케이션 로그의 13 Permission denied나 Connection refused가 방화벽, 퍼미션과 무관해 보일 때는 /var/log/audit/audit.log에서 avc: denied 줄을 먼저 확인한다. 포트가 막혔으면 semanage port로 라벨을 붙이고, 파일 접근이 막혔으면 semanage fcontext 뒤에 restorecon을 돌린다. 표준 대응으로 안 풀리는 접근만 audit2allow로 커스텀 정책 모듈을 만들어 허용한다.
1. SELinux와 avc denied가 무엇인지
SELinux는 프로세스와 파일에 각각 타입 라벨(context)을 붙이고, 정책에 명시된 조합만 허용하는 강제 접근 제어다. Nginx 프로세스는 httpd_t 도메인으로 돌고, 웹 문서는 httpd_sys_content_t 타입이어야 읽을 수 있다. 라벨이 어긋나면 파일 퍼미션이 755여도 커널이 접근을 거부한다.
이 거부는 커널 AVC(Access Vector Cache)가 판정하며, 거부될 때마다 감사 로그에 avc: denied 한 줄을 남긴다. 애플리케이션 쪽에는 Permission denied(errno 13)나 소켓 연결 거부로 나타나기 때문에, 일반 퍼미션 문제와 헷갈리기 쉽다. 그래서 진단의 출발점은 항상 감사 로그다.
현재 모드부터 확인한다.
$ getenforce Enforcing $ sestatus SELinux status: enabled SELinuxfs mount: /sys/fs/selinux Current mode: enforcing Loaded policy name: targeted Policy MLS status: enabled
2. 도구 설치 - policycoreutils-python-utils, setroubleshoot, checkpolicy
미니멀 설치한 Rocky/AlmaLinux 9에는 진단 도구가 빠져 있다. semanage나 audit2allow를 치면 command not found가 난다. 이 둘은 policycoreutils-python-utils 패키지가 제공한다(과거 문서에 나오는 policycoreutils-devel이 아니다). 사람이 읽기 좋은 진단 메시지를 주는 sealert는 setroubleshoot-server에 들어 있다.
$ sudo dnf install -y policycoreutils-python-utils setroubleshoot-server
여기서 한 번 걸리는 지점이 있다. audit2allow -M으로 정책 모듈(.pp)을 컴파일하려면 내부적으로 checkmodule을 호출하는데, 이건 별도 패키지 checkpolicy에 있다. 안 깔면 -M 단계에서 checkmodule: command not found가 난다. 모듈까지 만들 계획이면 같이 설치한다.
$ sudo dnf install -y checkpolicy $ which semanage audit2allow /usr/sbin/semanage /usr/bin/audit2allow
3. 감사 로그에서 막힌 접근 찾기
거부 기록은 /var/log/audit/audit.log에 쌓인다. 원본을 직접 보기보다 ausearch로 AVC 이벤트만 걸러 최근 것부터 본다.
$ sudo ausearch -m avc -ts recent
----
time->Fri Aug 28 10:20:11 2026
type=AVC msg=audit(1756362011.482:512): avc: denied { name_connect }
for pid=1841 comm="nginx" dest=8080
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:http_cache_port_t:s0 tclass=tcp_socket permissive=0
줄을 읽는 요령은 세 군데다. scontext는 접근한 주체(여기서는 httpd_t, Nginx), tcontext는 대상의 라벨, tclass와 중괄호 안의 권한({ name_connect })은 무엇을 하려다 막혔는지다. 위 예시는 Nginx가 8080 포트로 연결하려다 막힌 것이다.
sealert를 쓰면 같은 사건을 사람이 읽기 쉬운 권고와 함께 보여준다. 다만 권고에 나오는 audit2allow 명령을 그대로 붙여넣기 전에 반드시 뜻을 확인한다. 넓게 허용하는 제안이 섞여 있을 수 있다.
$ sudo sealert -a /var/log/audit/audit.log
참고로 dontaudit 규칙 때문에 로그에 안 남는 거부도 있다. 뭔가 막히는데 ausearch에 안 잡히면 잠시 전체 감사를 켜고 재현한 뒤 되돌린다.
$ sudo semodule -DB # dontaudit 잠시 해제 # ... 문제 재현 후 로그 확인 ... $ sudo semodule -B # 원래대로 복구
4. 포트가 막힌 경우 - semanage port
SELinux는 어떤 도메인이 어떤 포트를 쓸 수 있는지도 라벨로 통제한다. Nginx/Apache 계열(httpd_t)이 바인딩하거나 연결할 수 있는 포트 타입은 http_port_t다. 현재 목록을 본다.
$ sudo semanage port -l | grep -w http_port_t http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
여기 8080이 없다. Tomcat 기본 포트 8080은 http_cache_port_t에 속해 있어서, Nginx를 리버스 프록시로 8080에 붙이면 3장에서 본 name_connect 거부가 난다. 상황에 따라 대응이 갈린다.
리버스 프록시로 백엔드에 연결하는 경우
Nginx가 upstream으로 Tomcat이나 다른 앱에 붙는 구성이라면, 개별 포트를 하나씩 여는 대신 아웃바운드 연결을 허용하는 불리언을 켜는 것이 낫다. -P는 재부팅 후에도 유지되게 한다.
$ sudo setsebool -P httpd_can_network_connect on $ getsebool httpd_can_network_connect httpd_can_network_connect --> on
비표준 포트로 직접 리슨하는 경우
Nginx나 Tomcat을 http_port_t에 없는 포트(예: 8888)로 직접 띄우려면 그 포트에 라벨을 추가한다. 추가는 -a, 이미 있는 항목의 타입 변경은 -m이다.
$ sudo semanage port -a -t http_port_t -p tcp 8888 $ sudo semanage port -l | grep -w http_port_t http_port_t tcp 8888, 80, 81, 443, 488, 8008, 8009, 8443, 9000
이미 다른 타입에 등록된 포트를 -a로 추가하려 하면 ValueError: Port tcp/8080 already defined가 난다. 이때는 -m으로 타입을 바꾸거나, 위처럼 불리언으로 접근을 여는 쪽을 택한다.
5. 파일 접근이 막힌 경우 - semanage fcontext와 restorecon
웹 문서 루트를 /var/www 밖으로 옮기면 거의 이 문제를 만난다. 표준 경로 밖 파일은 default_t나 user_home_t 같은 라벨이라 httpd_t가 못 읽는다. 로그에는 이렇게 남는다.
type=AVC msg=audit(1756362250.771:531): avc: denied { read }
for pid=1902 comm="nginx" name="index.html" dev="nvme0n1p2" ino=133
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
ls -Z로 현재 라벨을 확인한다.
$ ls -Zd /srv/www unconfined_u:object_r:default_t:s0 /srv/www
대응은 두 단계다. 먼저 semanage fcontext로 경로 규칙을 정책에 등록하고, 그다음 restorecon으로 실제 파일에 적용한다. 정규식 (/.*)?는 디렉터리 자신과 하위 전체를 뜻한다.
$ sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?" $ sudo restorecon -Rv /srv/www Relabeled /srv/www from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0 Relabeled /srv/www/index.html from ...default_t:s0 to ...httpd_sys_content_t:s0
chcon으로 바로 바꾸는 방법도 있지만 임시다. 파일시스템 relabel이 돌면 원래대로 돌아간다. 영구적으로 두려면 반드시 semanage fcontext로 규칙을 등록한 뒤 restorecon을 쓴다. 앱이 파일을 쓰기까지 해야 하면(업로드 등) httpd_sys_rw_content_t를 대신 지정한다.
6. audit2allow로 커스텀 정책 모듈 만들기
포트, 불리언, 파일 컨텍스트라는 표준 손잡이로 안 풀리는 접근이 남을 때가 있다. 이럴 때 audit2allow가 감사 로그의 거부 기록을 읽어 그 접근만 허용하는 정책 규칙을 생성한다. 먼저 무엇을 허용하게 될지 -M 없이 규칙 텍스트만 뽑아 눈으로 확인한다.
$ sudo ausearch -m avc -ts recent | audit2allow #============= httpd_t ============== allow httpd_t self:capability net_bind_service;
규칙이 의도한 범위인지 확인했으면 모듈로 컴파일한다. -M은 .te(소스)와 .pp(적재용 바이너리) 두 파일을 만든다. 여기서 앞서 설치한 checkpolicy가 쓰인다.
$ sudo ausearch -m avc -ts recent | audit2allow -M nginx_local ******************** IMPORTANT *********************** To make this policy package active, execute: semodule -i nginx_local.pp $ sudo semodule -i nginx_local.pp
적재된 커스텀 모듈은 목록에서 확인하고, 필요 없어지면 이름으로 제거한다.
$ sudo semodule -l | grep nginx_local nginx_local $ sudo semodule -r nginx_local # 롤백
주의할 점이 있다. audit2allow는 로그에 찍힌 거부만 보고 규칙을 만든다. 여러 거부가 섞여 들어오면 필요 이상으로 넓게 허용하는 규칙을 뱉을 수 있다. 그래서 .te 파일을 꼭 열어 보고, 가능하면 포트/파일 컨텍스트 같은 표준 방법으로 먼저 풀 수 있는지 따진 다음 마지막 수단으로 모듈을 쓴다.
7. 검증과 permissive를 이용한 디버깅
고친 뒤에는 실제로 거부가 사라졌는지 로그로 확인한다. 문제를 재현하고 다시 ausearch를 돌려 새 denied가 없으면 된다.
$ sudo systemctl restart nginx $ sudo ausearch -m avc -ts recent <no matches>
원인이 한 개가 아니라 여러 개가 얽혀 있을 때는, 특정 도메인만 강제를 잠시 풀어 거부를 전부 로그로 뽑는 방법이 유용하다. SELinux 전체를 끄지 않고 httpd_t만 permissive로 돌린다.
$ sudo semanage permissive -a httpd_t # ... 앱을 끝까지 돌려 막히는 접근을 모두 로그에 남긴 뒤 ... $ sudo semanage permissive -d httpd_t # enforcing으로 복귀
이 상태에서는 거부해도 통과시키되 로그는 남기므로, 필요한 규칙을 한 번에 모아 정리할 수 있다. 마무리하면 반드시 permissive를 걷어내 원래 강제 상태로 돌린다. setenforce 0으로 시스템 전체를 끄는 것과는 다르다.
8. 마무리
순서를 정리하면 이렇다. ausearch -m avc로 무엇이 막혔는지 읽고, 포트면 semanage port나 httpd_can_network_connect 불리언, 파일이면 semanage fcontext 뒤 restorecon으로 표준 대응을 먼저 시도한다. 그래도 남는 접근만 audit2allow로 규칙을 확인한 뒤 최소 범위 모듈을 만들어 적재한다. 이 방식이면 SELinux를 Disabled로 두지 않고도 Nginx, Tomcat을 정상 운영할 수 있고, 서버 보안 라인을 그대로 유지한다.