배경
Nginx에 새 conf 파일을 추가하고 nginx -t, nginx -s reload를 했는데 요청이 server_name에 매칭되지 않았습니다.
설정 문법에는 문제가 없었고, nginx -t도 통과했습니다.
그런데 막상 요청을 보내면 가상 호스트가 잡히지 않고 default server로 떨어졌습니다.
원인 추적 중 /var/log/audit/audit.log에서 SELinux AVC deny를 발견했습니다. 파헤쳐보니 문제는 두 개였습니다.
첫 번째는 WAF 에이전트 로그 파일(/usr/local/deepfinder/log/filter.log)의 SELinux 컨텍스트가 잘못되어 있었고, 두 번째는 더 근본적인 원인으로 /etc/nginx/conf.d/ 디렉토리 전체가 user_home_t 컨텍스트를 갖고 있어 Nginx master가 설정 파일 자체를 읽지 못하고 있었습니다.
server_name 매칭이 실패한 직접적인 원인은 두 번째였습니다.
이 글은 그 트러블슈팅 과정을 정리하고, 함께 공부한 SELinux 개념을 기록해둔 것입니다.
1. 발견한 에러
/var/log/audit/audit.log에서 발견한 실제 AVC 메시지입니다. 같은 패턴이 수 ms 간격으로 연속 기록되어 있었습니다.
type=AVC msg=audit(1779157493.851:550): avc: denied { append } for pid=6021 comm="nginx" name="filter.log" dev="dm-0" ino=1576580 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:usr_t:s0 tclass=file permissive=0
type=SYSCALL msg=audit(1779157493.851:550): arch=c000003e syscall=257 success=no exit=-13 ... ppid=848 pid=6021 ... comm="nginx" exe="/usr/sbin/nginx" subj=system_u:system_r:httpd_t:s0
type=PROCTITLE msg=audit(1779157493.851:550): proctitle=6E67696E783A206D61737465722070726F636573732...
로그 해석
필드값의미
| 필드 | 값 | 의미 |
| { append } | append | 파일에 내용을 추가(쓰기)하려 함 |
| scontext | system_r:httpd_t:s0 | Nginx 프로세스의 SELinux 타입 = httpd_t |
| tcontext | object_r:usr_t:s0 | 대상 파일(filter.log)의 SELinux 타입 = usr_t |
| syscall=257 | openat | 실제 시스템 콜은 파일 open 시도 |
| exit=-13 | EACCES | Permission denied로 실패 |
| permissive=0 | 0 | enforcing 모드 — 실제로 차단됨 |
| ppid=848 | systemd | reload로 새로 뜬 master의 부모가 systemd |
PROCTITLE 필드의 hex 값을 디코딩하면 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf입니다.
worker가 아니라 master process 자체가 차단된 것입니다.
이벤트가 수 ms 안에 연속 발생한 것은 reload 직후 여러 server block이 동시에 filter.log 열기를 시도했기 때문입니다.
SELinux deny가 감지되면 setroubleshootd 분석 데몬이 자동으로 기동되는 것도 이후 로그에서 확인됩니다.
filter.log는 Nginx에 모듈 형태로 로드된 내부 WAF 에이전트(deepfinder)가 쓰는 파일입니다.
WAF 에이전트는 Nginx 프로세스 공간에서 실행되기 때문에 SELinux 컨텍스트를 httpd_t로 그대로 상속받습니다.
그 결과 comm="nginx"로 찍히지만 실제로는 WAF 에이전트의 쓰기 시도가 차단된 것입니다.
2. SELinux 동작 원리
SELinux를 처음 접하면 "왜 root인데 막히지?" 라는 의문이 생깁니다.
기존 Linux 권한 체계(DAC)와 SELinux(MAC)의 차이를 이해하면 납득이 됩니다.
DAC (기존 방식) — chmod, chown으로 파일 소유자가 권한을 결정합니다. root는 모든 것을 할 수 있습니다.
MAC (SELinux) — 정책이 모든 접근을 결정합니다. root여도 SELinux 정책에 허용 규칙이 없으면 거부됩니다.
두 체계는 독립적으로 동작합니다. DAC에서 허용되더라도 MAC에서 거부되면 접근이 막힙니다.
Nginx가 파일 소유자이거나 권한이 있어도, SELinux가 해당 타입에 대한 접근을 허용하지 않으면 차단됩니다.
SELinux의 접근 판단 흐름은 아래와 같습니다.
프로세스가 파일에 접근 시도
↓
DAC 확인 (chmod 권한)
↓
MAC 확인 (SELinux 정책)
├── allow 규칙 있음 → 허용
└── allow 규칙 없음 → 거부 + AVC 로그 기록
3. 컨텍스트 구조 이해하기
SELinux는 모든 프로세스와 파일에 레이블을 붙입니다. 이 레이블을 컨텍스트(Context) 라고 합니다.
system_u : system_r : httpd_t : s0
① ② ③ ④
| 번호 | 이름 | 설명 |
| ① | user | SELinux 사용자. system_u, unconfined_u 등 |
| ② | role | 역할. 프로세스는 system_r, 파일은 object_r |
| ③ | type | 가장 중요. 프로세스의 type을 특별히 도메인이라 부름 |
| ④ | level | MLS 보안 레벨. targeted 정책에서는 s0로 고정 |
실제로 정책은 대부분 ③ type을 기준으로 동작합니다. allow httpd_t httpd_log_t:file { append write }; 같은 형태로, 어떤 도메인이 어떤 타입의 파일에 어떤 작업을 할 수 있는지를 정의합니다.
컨텍스트를 확인하는 방법입니다.
# 파일 컨텍스트 확인
ls -Z /etc/nginx/conf.d/
# 프로세스 컨텍스트 확인
ps auZ | grep nginx
# system_u:system_r:httpd_t:s0 nginx: master process
Nginx와 관련된 주요 타입입니다.
| 타입 | 용도 |
| httpd_t | Nginx/Apache 프로세스 도메인 |
| httpd_config_t | /etc/nginx/** 설정 파일 |
| httpd_log_t | /var/log/nginx/** 로그 파일 |
| httpd_sys_content_t | 웹 콘텐츠 (읽기 전용) |
| httpd_sys_rw_content_t | 웹 콘텐츠 (읽기/쓰기) |
| http_port_t | HTTP 계열 서비스가 bind할 수 있는 포트 타입. 허용 포트는 배포판과 정책 버전에 따라 다르며 semanage port -l | grep http_port_t로 확인 |
4. SELinux 모드
SELinux는 세 가지 모드로 동작합니다.
| 모드 | 동작 |
| enforcing | 정책 위반 시 실제 차단 + 감사 로그 기록. 기본값 |
| permissive | 차단하지 않고 로그만 기록. 디버깅 시 활용 |
| disabled | SELinux 완전 비활성화. 운영 환경에서는 사용하지 않음 |
getenforce # Enforcing / Permissive / Disabled
sestatus # 상세 상태
트러블슈팅 중 전체를 permissive로 내리고 싶을 수 있습니다.
하지만 그보다는 특정 도메인만 선택적으로 permissive로 설정하는 방법이 더 안전합니다.
# httpd_t 도메인만 permissive, 나머지는 enforcing 유지
semanage permissive -a httpd_t
# 확인
semanage permissive -l
# 원복
semanage permissive -d httpd_t
5. 문제 해결 과정
1단계 — 모드 확인
getenforce
# Enforcing
2단계 — 문제 1: WAF 에이전트 로그 파일 컨텍스트 수정
inode로 파일 경로를 찾아 컨텍스트를 확인했습니다.
ls -Z /usr/local/deepfinder/log/filter.log
# system_u:object_r:usr_t:s0 /usr/local/deepfinder/log/filter.log
usr_t가 붙어 있습니다. httpd_t 도메인이 쓸 수 있는 타입은 httpd_log_t입니다.
semanage fcontext -a -t httpd_log_t "/usr/local/deepfinder/log/filter.log"
restorecon -Rv /usr/local/deepfinder/
# Relabeled /usr/local/deepfinder/log/filter.log
# from system_u:object_r:usr_t:s0
# to system_u:object_r:httpd_log_t:s0
3단계 — reload 후 재확인
nginx -s reload
ausearch -m AVC -c nginx -ts recent
이 시점에서 filter.log 관련 deny는 사라졌지만, 새로운 AVC deny가 등장했습니다.
avc: denied { read } for pid=848 comm="nginx" name="conf.d"
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=dir
avc: denied { read } for pid=848 comm="nginx" name="server"
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=dir
avc: denied { read } for pid=848 comm="nginx" name="upstream"
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=dir
avc: denied { execmem } for pid=11944 comm="nginx"
scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:system_r:httpd_t:s0
conf.d, server, upstream, frontend 디렉토리 전체가 user_home_t로 되어 있어 Nginx master(pid=848)가 설정 파일을 읽지 못하고 있었습니다.
이것이 server_name 매칭이 실패한 직접적인 원인이었습니다.
execmem deny는 WAF 에이전트 모듈이 실행 가능 메모리를 요구하는 것으로, 별도 boolean으로 처리합니다.
4단계 — 문제 2: conf.d 컨텍스트 수정
ls -Z /etc/nginx/
# unconfined_u:object_r:user_home_t:s0 conf.d ← 잘못됨
# system_u:object_r:httpd_config_t:s0 nginx.conf
conf.d가 user_home_t인 것을 확인. 타입명을 잘못 입력하면 아래처럼 에러가 납니다.
semanage fcontext -a -t http_config_t "/etc/nginx/conf.d"
# ValueError: Type http_config_t is invalid, must be a file or device type
올바른 타입은 httpd_config_t입니다.
semanage fcontext -a -t httpd_config_t "/etc/nginx/conf.d"
restorecon -Rv /etc/nginx/conf.d/
# Relabeled /etc/nginx/conf.d user_home_t → httpd_config_t
# Relabeled /etc/nginx/conf.d/upstream user_home_t → httpd_config_t
# Relabeled /etc/nginx/conf.d/server user_home_t → httpd_config_t
# Relabeled /etc/nginx/conf.d/server/frontend/bo-hanpassplus.conf
# user_home_t → httpd_config_t
# ... (하위 파일 전체)
5단계 — execmem boolean 적용
WAF 에이전트가 실행 가능 메모리를 사용하므로 boolean을 활성화합니다.
setsebool -P httpd_execmem 1
6단계 — 최종 reload 및 검증
nginx -s reload
ausearch -m AVC -c nginx -ts recent
# 출력 없음 → 정상
6. 배운 것
Jenkins 배포와 SELinux 컨텍스트
/etc/nginx/conf.d/가 user_home_t로 되어 있었던 직접적인 원인은 Jenkins를 통한 파일 배포였습니다.
Jenkins 워크스페이스는 /var/lib/jenkins/workspace/ 하위에 위치합니다.
이 경로는 SELinux 상 user_home_t로 분류됩니다. Jenkins가 파이프라인에서 conf 파일을 /etc/nginx/conf.d/로 복사할 때, 파일의 SELinux 컨텍스트가 워크스페이스의 user_home_t를 그대로 달고 목적지에 배포됩니다.
Jenkins workspace (/var/lib/jenkins/workspace/)
→ user_home_t 컨텍스트를 갖는 파일들
↓ 배포 (cp / scp / rsync)
/etc/nginx/conf.d/
→ user_home_t 컨텍스트가 그대로 따라옴
↓ nginx reload
→ Nginx master가 conf 파일을 읽지 못함 → AVC denied { read }
따라서 Jenkins 파이프라인에 restorecon 단계를 추가하는 것이 근본적인 해결책입니다.
# Jenkinsfile 또는 배포 스크립트에 추가
restorecon -Rv /etc/nginx/conf.d/
nginx -s reload
cp -a나 SELinux 속성을 보존하는 방식으로 복사하면 원본 컨텍스트가 그대로 유지됩니다.
다른 경로로 파일을 복사한 뒤에는 항상 restorecon으로 컨텍스트를 재적용하는 것이 안전합니다.
mv는 흔히 "목적지 디렉토리 컨텍스트를 상속한다"고 오해하기 쉽지만, 실제로는 반대입니다.
같은 파일시스템 내 mv는 inode가 유지되므로 SELinux 컨텍스트도 그대로 유지됩니다.
즉 잘못된 컨텍스트 상태로 이동될 수 있으며, 이동 후에도 restorecon이 필요할 수 있습니다.
touch, vim으로 직접 생성한 경우에는 부모 디렉토리 정책을 따르므로 비교적 안전합니다.
nginx -t는 런타임 동작을 보장하지 않는다
nginx -t는 설정 문법과 일부 파일 접근을 검증하지만, SELinux 정책 위반은 감지하지 못합니다.
실제 worker process가 reload되는 과정에서 SELinux deny가 발생할 수 있으며, 이 경우 nginx -t와 reload 모두 성공으로 보여도 런타임에서 일부 server block이 정상 동작하지 않을 수 있습니다.
이번 이슈가 정확히 그 경우였습니다.
nginx -t 통과 → reload 성공 → 그런데 요청은 default server로 폴백.
reload 후 예상치 못한 동작이 있다면 audit.log를 먼저 확인하는 습관이 중요합니다.
chcon은 임시, semanage fcontext는 영구
| 명령 | 특징 |
| chcon -t httpd_log_t 파일 | 즉시 변경. restorecon 실행 또는 재레이블링 시 원복됨 |
| semanage fcontext -a -t httpd_log_t 경로 + restorecon | SELinux 정책 DB에 영구 등록. 재부팅 후에도 유지 |
운영 환경에서는 반드시 semanage fcontext + restorecon 조합을 사용합니다.
chcon은 동작을 빠르게 확인하는 용도로만 씁니다.
Boolean으로 기능 단위 허용이 가능하다
파일 컨텍스트 문제 외에 Nginx에서 자주 만나는 SELinux 이슈와 해결책입니다.
# upstream(proxy_pass) TCP 연결 거부 시 — 대부분의 경우 이걸로 해결됨
setsebool -P httpd_can_network_connect 1
# reverse proxy/gateway 계열 동작 허용 (relay 역할)
setsebool -P httpd_can_network_relay 1
# DB 연결 허용 시
setsebool -P httpd_can_network_connect_db 1
# WAF 등 모듈이 실행 가능 메모리를 사용하는 경우
setsebool -P httpd_execmem 1
httpd_can_network_connect는 upstream으로의 TCP 연결 전반을 허용하며, proxy_pass 문제의 대부분은 이 boolean으로 해결됩니다. httpd_can_network_relay는 reverse proxy나 gateway 역할에 가까운 추가 정책입니다.
-P 옵션이 있으면 재부팅 후에도 유지됩니다.
7. 명령어 정리
# 상태 확인
sestatus
getenforce
# 컨텍스트 확인
ls -Z 파일또는디렉토리
ps auZ | grep nginx
# 문제 분석
ausearch -m AVC -c nginx -ts recent
ausearch -m AVC -c nginx -ts today
grep nginx /var/log/audit/audit.log | audit2why
# 컨텍스트 영구 수정 — 로그 파일
semanage fcontext -a -t httpd_log_t "/path/to/log(/.*)?"
restorecon -Rv /path/to/log/
# 컨텍스트 영구 수정 — 설정 파일
semanage fcontext -a -t httpd_config_t "/etc/nginx/conf.d(/.*)?"
restorecon -Rv /etc/nginx/conf.d/
# Boolean 설정
setsebool -P httpd_can_network_connect 1
setsebool -P httpd_can_network_relay 1
setsebool -P httpd_execmem 1
# 포트 추가
semanage port -a -t http_port_t -p tcp 8082
semanage port -l | grep http_port_t
# 도메인 permissive 설정
semanage permissive -a httpd_t
semanage permissive -d httpd_t
마치며
SELinux를 처음 마주쳤을 때는 "그냥 끄면 안 되나"라는 생각이 먼저 들었습니다.
그런데 구조를 이해하고 나면 생각보다 체계적이고, 문제가 생겼을 때 audit 로그가 원인을 꽤 명확하게 알려준다는 걸 알게 됐습니다.
이번 이슈에서 가장 인상 깊었던 점은 nginx -t가 통과했음에도 실제 동작이 달랐다는 것입니다.
SELinux는 nginx -t 시점이 아닌 실제 worker가 파일에 접근하는 시점에 개입합니다.
conf 파일을 배포할 때마다 restorecon을 실행하거나, ausearch -m AVC -ts recent로 reload 후 deny가 없는지 확인하는 습관이 필요합니다.
참고
'system' 카테고리의 다른 글
| 도와줘! strace (0) | 2026.06.04 |
|---|---|
| 인프라 모니터링: 무엇을, 왜, 어떻게 볼 것인가 (0) | 2026.06.04 |
| 시스템 성능 분석: CPU, 메모리, 디스크, 네트워크 (0) | 2026.06.04 |
| 스토리지 종류와 구성 방식 정리 (0) | 2026.06.04 |
| OpenVPN Access Server로 Split Tunneling 구현하기 (0) | 2026.04.21 |