본문 바로가기
Operating System

Rocky/AlmaLinux 9 SELinux가 Nginx, Tomcat를 막을 때 - avc denied 읽고 audit2allow로 정책 모듈 만들기

나크나로·2026년 8월 30일·조회 1

Rocky Linux나 AlmaLinux 9로 서버를 새로 올리면 CentOS 7 시절과 달리 SELinux가 Enforcing으로 켜진 채 시작한다. 설정 파일도 방화벽도 다 맞는데 Nginx가 502를 뱉거나 Tomcat이 파일을 못 읽는 상황이 여기서 자주 나온다. 로그만 봐서는 원인이 안 보이고, 결국 SELinux를 통째로 끄고 넘어가는 경우를 현장에서 많이 본다. 이 글은 그 대신 무엇이 막혔는지 정확히 짚어 최소한으로 여는 방법을 정리한다.

정리하면, 애플리케이션 로그의 13 Permission deniedConnection 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에는 진단 도구가 빠져 있다. semanageaudit2allow를 치면 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_tuser_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 porthttpd_can_network_connect 불리언, 파일이면 semanage fcontextrestorecon으로 표준 대응을 먼저 시도한다. 그래도 남는 접근만 audit2allow로 규칙을 확인한 뒤 최소 범위 모듈을 만들어 적재한다. 이 방식이면 SELinux를 Disabled로 두지 않고도 Nginx, Tomcat을 정상 운영할 수 있고, 서버 보안 라인을 그대로 유지한다.

자주 묻는 질문

Rocky/AlmaLinux 9에서 semanage, audit2allow가 command not found인데 어떤 패키지를 설치해야 하나?

policycoreutils-python-utils 패키지가 semanage와 audit2allow, audit2why를 제공한다. sudo dnf install -y policycoreutils-python-utils로 설치한다. 과거 문서에 나오는 policycoreutils-devel이 아니다. 사람이 읽기 좋은 진단 메시지를 주는 sealert는 setroubleshoot-server에, audit2allow -M로 .pp를 컴파일할 때 필요한 checkmodule은 checkpolicy 패키지에 들어 있다.

Nginx를 8080 같은 비표준 포트에 붙이면 왜 막히나?

SELinux는 httpd_t 도메인이 쓸 수 있는 포트를 http_port_t 타입으로 제한한다. 기본 목록에는 80, 81, 443, 488, 8008, 8009, 8443, 9000만 있고 8080은 http_cache_port_t에 속해 있어 제외된다. 직접 리슨할 포트라면 semanage port -a -t http_port_t -p tcp 8080으로 라벨을 추가하고, 리버스 프록시로 백엔드에 연결하는 경우라면 setsebool -P httpd_can_network_connect on으로 아웃바운드 연결을 허용한다.

웹 문서를 /var/www 밖으로 옮겼더니 Permission denied가 난다. 어떻게 고치나?

표준 경로 밖 파일은 default_t 같은 라벨이라 httpd_t가 읽지 못한다. semanage fcontext -a -t httpd_sys_content_t "/경로(/.*)?"로 규칙을 등록한 뒤 restorecon -Rv /경로로 실제 파일에 적용한다. 앱이 파일을 쓰기까지 해야 하면 httpd_sys_rw_content_t를 지정한다. chcon은 임시라 relabel이 돌면 되돌아가므로 영구 설정에는 쓰지 않는다.

audit2allow가 만든 규칙을 그대로 적용해도 되나?

권장하지 않는다. audit2allow는 로그에 찍힌 거부만 보고 규칙을 만들어, 여러 거부가 섞이면 필요 이상으로 넓게 허용할 수 있다. -M 없이 먼저 규칙 텍스트를 출력해 확인하고, .te 파일 내용을 검토한 다음 semodule -i로 적재한다. 포트나 파일 컨텍스트 같은 표준 방법으로 풀 수 있으면 그쪽을 우선하고, audit2allow 모듈은 마지막 수단으로 쓴다.

문제가 여러 개 얽혀 있을 때 SELinux를 끄지 않고 진단하려면?

특정 도메인만 permissive로 돌린다. semanage permissive -a httpd_t를 하면 httpd_t의 거부를 통과시키되 로그는 남기므로, 앱을 끝까지 돌려 막히는 접근을 한 번에 모을 수 있다. 정리가 끝나면 semanage permissive -d httpd_t로 다시 enforcing으로 복귀한다. setenforce 0으로 시스템 전체를 끄는 것과 달리 나머지 도메인의 보호는 유지된다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.