RHEL 중급 #1 SELinux 입문 — Enforcing/Permissive, 라벨, 트러블슈팅
기초 #7에서 SELinux를 한 줄로만 짚었습니다. “RHEL 9에서 SELinux는 기본 Enforcing이고, 끄지 마라.” 하지만 운영에 들어가면 그 한 줄이 가장 자주 발목을 잡습니다. 이번 글은 SELinux를 끄지 않고 문제를 푸는 법을 다룹니다. 모드를 바꾸는 법, 라벨을 확인하는 법, 막힌 요청을 푸는 법까지 한 번에 정리합니다. SELinux를 이해하면 RHEL 운영의 안정성이 한 단계 올라갑니다.
RHEL 중급 시리즈에서 이번 글의 위치:
- #1 SELinux 입문 — Enforcing/Permissive, 라벨, 트러블슈팅 ← 이번 글
- #2 LVM — PV/VG/LV, 스냅샷, 확장
- #3 스토리지 심화 — Stratis, NFS, Samba
- #4 네트워킹 — NetworkManager (nmcli), bonding, teaming
- #5 로그 관리 — journald, rsyslog, log rotation
- #6 작업 스케줄링 — cron, systemd timer, at
- #7 컨테이너 입문 — Podman/Buildah/Skopeo (Docker와의 차이)
SELinux란 무엇인가 #
리눅스의 기본 권한 모델은 DAC (Discretionary Access Control, 임의 접근 제어)입니다. 파일에 적힌 사용자/그룹/rwx로 결정되는, 기초 #5에서 다룬 그 모델입니다. 문제는 한 줄로 요약됩니다. 루트 권한을 잡은 프로세스는 모든 파일에 접근할 수 있습니다.
웹 서버가 한 번 뚫려서 httpd 프로세스를 공격자가 쥐었다고 합시다. 그 프로세스는 root거나 apache 사용자로 동작하는데, 어느 쪽이든 시스템 어디든 자기 권한 안에서 마음대로 돌아다닐 수 있습니다. /etc/shadow 읽기, /var/lib/mysql 접근, SSH 키 훔치기. DAC만으로는 막을 수가 없습니다.
SELinux (Security-Enhanced Linux)는 그 위에 한 층 더 두는 MAC (Mandatory Access Control, 강제 접근 제어)입니다. 모든 파일,프로세스,소켓,포트에 **라벨(context)**을 부여해두고, 커널이 “이 라벨의 프로세스가 저 라벨의 자원에 접근해도 되는지"를 정책에 따라 강제로 검사합니다. DAC가 통과해도 SELinux가 막을 수 있고, 루트도 SELinux를 우회할 수 없습니다.
요청: httpd 프로세스가 /etc/shadow를 읽으려 함
│
▼
┌─────────────────────────────────────┐
│ 1. DAC 검사 │
│ httpd의 사용자가 /etc/shadow의 │
│ 퍼미션(보통 600 root)을 통과하나? │
│ → 거의 항상 거절. 통과한다면 ↓ │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. SELinux (MAC) 검사 │
│ httpd_t 타입 프로세스가 │
│ shadow_t 타입 파일을 읽도록 │
│ 정책이 허용했나? │
│ → 정책에 없으면 거절 │
└─────────────────────────────────────┘
│
▼
접근 허가DAC가 한 번 뚫려도 SELinux가 잡아줍니다. 두 번째 잠금 이라고 생각하면 됩니다. RHEL 9의 보안 모델에서 가장 큰 차이가 나는 부분입니다.
세 가지 모드 — Enforcing / Permissive / Disabled #
SELinux의 동작 모드는 셋입니다.
| 모드 | 동작 | 권장 |
|---|---|---|
| Enforcing | 정책 위반을 차단 + 로그 남김 | ✅ 운영 표준 (RHEL 9 기본값) |
| Permissive | 정책 위반을 허용 + 로그만 남김 | 디버깅,정책 작성 시 임시 |
| Disabled | SELinux 자체가 안 동작 | ❌ 운영 절대 금지 |
확인:
$ getenforce
Enforcing
$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33sestatus가 더 풍부한 정보입니다. Loaded policy name이 targeted 인 게 일반적입니다 (대부분의 환경에서). MLS는 군/정부 등 특수 환경에서만 사용합니다.
일시적 전환 — setenforce
#
$ sudo setenforce 0 # Permissive로 (재부팅하면 원복)
$ sudo setenforce 1 # Enforcing으로
$ getenforce
Permissive디버깅 중에만 잠깐 Permissive로 내려두고, 끝나면 다시 Enforcing으로 올려요.
영구 전환 — /etc/selinux/config
#
SELINUX=enforcing
SELINUXTYPE=targetedSELINUX= 값을 permissive / enforcing / disabled로 바꾸고 재부팅하면 적용됩니다. 다만:
disabled는 절대 쓰지 마세요. 한 번 disabled로 부팅하면 모든 파일의 라벨이 풀려서, 다시 Enforcing으로 올릴 때 전체 파일시스템 relabel (수십 분~수 시간)이 필요합니다.- 잠깐 SELinux 검사를 끄고 싶으면 **항상
permissive**로. 디버깅,로그 수집은 Permissive에서도 동일하게 됩니다.
autorelabel — 라벨이 깨졌을 때 #
뭔가 잘못돼서 전체 파일시스템의 라벨을 다시 부여하고 싶을 때:
$ sudo touch /.autorelabel
$ sudo reboot부팅 시 모든 파일을 정책 기준으로 재라벨합니다. disabled → enforcing 전환의 마지막 단계에 등장하는 명령이고, 일반적으로는 평생 한 번 칠까 말까 합니다.
라벨 (context)의 모양 #
SELinux의 모든 결정은 라벨에 기반합니다. ls -Z와 ps -Z가 라벨을 보여주는 명령입니다.
$ ls -Z /var/www/html/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
$ ls -Z /etc/shadow
system_u:object_r:shadow_t:s0 /etc/shadow
$ ls -Z /home/curtis
unconfined_u:object_r:user_home_dir_t:s0 /home/curtis$ ps -eZ | grep httpd
system_u:system_r:httpd_t:s0 1234 ? 00:00:01 httpd
$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023라벨은 콜론으로 네 부분입니다.
system_u : object_r : httpd_sys_content_t : s0
────┬─── ───┬──── ─────────┬────────── ─┬─
│ │ │ │
user role type level (MLS)| 필드 | 의미 | 자주 보는 값 |
|---|---|---|
| user | SELinux 사용자 (리눅스 사용자와 다름) | system_u, unconfined_u, staff_u |
| role | 역할 (RBAC) | object_r (파일), system_r (시스템 프로세스), unconfined_r |
| type | 타입 — 실질적으로 정책의 키 | httpd_t, httpd_sys_content_t, shadow_t, ssh_port_t |
| level | MLS 레벨 (targeted 정책에선 거의 s0) | s0 |
targeted 정책에서는 사실상 type만 신경 쓰면 됩니다. 그래서 SELinux 작업의 90%는 “어떤 파일에 어떤 type이 부여돼 있나"를 살피는 일입니다. user와 role은 거의 항상 자동으로 맞고, level은 targeted에서 의미가 없습니다.
자주 만나는 type 들 #
| type | 의미 |
|---|---|
httpd_t | 웹 서버 (Apache/nginx) 프로세스 |
httpd_sys_content_t | 웹 서버가 읽기 가능한 정적 콘텐츠 |
httpd_sys_rw_content_t | 웹 서버가 읽고 쓸 수 있는 콘텐츠 |
shadow_t | /etc/shadow 같은 비밀번호 파일 |
ssh_home_t | ~/.ssh 안의 SSH 키 |
user_home_dir_t | 일반 사용자 홈 디렉터리 |
var_log_t | /var/log 안의 로그 파일 |
bin_t | /usr/bin 안의 실행 파일 |
unconfined_t | SELinux 제약을 거의 안 받는 프로세스 (사용자 셸 등) |
이 type 들과 정책의 조합으로 “이게 저기 접근해도 되는지” 가 결정됩니다. 예를 들면 정책이 httpd_t → httpd_sys_content_t 읽기는 허용하지만, httpd_t → shadow_t 읽기는 절대 허용하지 않습니다.
라벨 고치기 — chcon / restorecon
#
운영에서 가장 자주 만나는 작업입니다. 파일을 잘못된 경로에 두거나 잘못된 명령으로 복사하면 라벨이 잘못 부여되어 SELinux가 차단합니다. 두 명령이 답입니다.
chcon — 임시 변경
#
$ sudo chcon -t httpd_sys_content_t /var/www/html/new.html
$ ls -Z /var/www/html/new.html
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/new.html
$ sudo chcon -R -t httpd_sys_content_t /var/www/html/ # 재귀chcon은 수동으로 type을 바꿉니다. 빠르고 직관적이지만 함정이 있습니다. restorecon이나 relabel이 일어나면 원래 정책이 정한 type으로 되돌아갑니다. 임시 작업에만.
restorecon — 정책 기준으로 복구
#
$ sudo restorecon -v /var/www/html/new.html
Relabeled /var/www/html/new.html from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
$ sudo restorecon -Rv /var/www/html/ # 재귀restorecon은 정책이 그 경로에 어떤 type을 부여해야 한다고 정의해두었는지를 보고 거기로 되돌립니다. 그래서 “라벨이 망가졌다” 면 거의 항상 restorecon이 정답입니다. chcon보다 안전합니다.
일반적인 흐름 #
1. 파일을 새 경로에 복사 (cp / mv)
↓ 라벨이 원래 경로의 것을 따라옴 (잘못됨)
2. 웹 서버가 읽기 거절 (SELinux AVC denial)
3. ls -Z로 잘못된 라벨 확인
4. restorecon -Rv <경로> 로 정책 기준으로 복구
5. 다시 시도 → 통과함정 —
cp는 대체로 대상 경로의 정책 기준으로 새 라벨이 붙고,cp -a(archive)나mv처럼 메타데이터를 보존하는 작업은 원본 라벨을 유지할 수 있습니다. 그래서mv ~/page.html /var/www/html/같은 단순 이동이 라벨 사고의 1순위입니다. 이동 후에는restorecon을 한 번 실행하는 게 안전합니다.
영구 라벨 정책 변경 — semanage fcontext
#
/var/www/html 외에 다른 디렉터리를 웹 콘텐츠 경로로 쓰고 싶다면 어떻게 합니까? 거기에 매번 chcon을 적용해도 restorecon 한 번에 풀립니다. 정책 자체를 바꿔야 합니다.
$ sudo dnf install -y policycoreutils-python-utils
$ sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
$ sudo restorecon -Rv /srv/www세 줄의 의미:
policycoreutils-python-utils—semanage명령을 제공하는 패키지. RHEL 9 기본 설치에 안 들어 있는 경우가 있음semanage fcontext -a -t <type> "<regex>"— 그 정규식 경로 패턴에 그 type을 박으라고 정책에 등록restorecon -Rv— 등록한 정책을 실제 파일들에 적용
이러면 /srv/www 안의 어떤 파일도 항상 httpd_sys_content_t가 되고, 새로 만들어지는 파일도 자동으로 그 type을 받습니다. 이 방식이 운영의 표준 입니다.
$ sudo semanage fcontext -l | grep '/srv/www'
/srv/www(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
$ sudo semanage fcontext -d "/srv/www(/.*)?" # 등록 해제포트도 라벨이 있다 — semanage port
#
기초 #7의 SSH 포트 변경에서 잠깐 만난 명령입니다. 포트 번호도 SELinux 라벨이 있어서, “허용된 포트가 아닌 포트로 데몬이 listen 하려고 하면” 정책이 거절합니다.
$ sudo semanage port -l | grep ssh
ssh_port_t tcp 22
$ sudo semanage port -a -t ssh_port_t -p tcp 2222 # 추가
$ sudo semanage port -l | grep ssh
ssh_port_t tcp 2222, 22웹 서버 포트(http_port_t), DB 포트(postgresql_port_t) 등도 같은 모델입니다. 데몬이 표준이 아닌 포트로 구동될 때는 항상 이 단계를 거쳐야 합니다.
Booleans — 정책의 on/off 스위치 #
매번 정책을 새로 짜지 않아도 자주 쓰는 옵션은 boolean으로 미리 준비돼 있습니다. 이름 그대로 켜고/끄는 스위치입니다.
$ getsebool -a | head -10
abrt_anon_write --> off
abrt_handle_event --> off
...
$ getsebool -a | grep httpd | head -10
httpd_anon_write --> off
httpd_can_check_spam --> off
httpd_can_connect_ftp --> off
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
httpd_can_sendmail --> off
httpd_enable_cgi --> on
...자주 켜는 항목:
# httpd가 외부 네트워크로 나가게 (예: nginx → upstream)
$ sudo setsebool -P httpd_can_network_connect on
# httpd가 DB(postgres/mysql 등)로 연결
$ sudo setsebool -P httpd_can_network_connect_db on
# NFS 클라이언트로 원격 홈을 마운트해 SSH 키 인증
$ sudo setsebool -P use_nfs_home_dirs on-P가 영구 적용입니다. 안 붙이면 재부팅 시 원복됩니다. 운영에서는 거의 항상 -P를 함께 붙입니다.
$ getsebool httpd_can_network_connect
httpd_can_network_connect --> on
$ semanage boolean -l | grep httpd_can_network_connect
httpd_can_network_connect (on , on) Allow HTTPD scripts and modules to connect to the network using TCP.AVC denial — SELinux가 차단했다는 신호 #
SELinux가 무언가를 차단하면 AVC denial 로그가 남습니다. (Access Vector Cache.) 트러블슈팅의 출발점입니다.
$ sudo ausearch -m avc -ts recent
time->Wed Apr 16 10:23:12 2026
type=AVC msg=audit(1713248592.123:456):
avc: denied { read } for pid=1234 comm="httpd"
name="config.json" dev="vda2" ino=12345
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:user_home_t:s0
tclass=file permissive=0핵심 필드를 풀어보면:
denied { read }— 무엇을 거절했는지 (read / write / open / connectto 등)comm="httpd"— 어떤 명령이 시도했는지scontext— source context — 시도한 프로세스의 라벨 (httpd_t)tcontext— target context — 접근하려던 자원의 라벨 (user_home_t)tclass— 자원 종류 (file / dir / tcp_socket 등)
이 한 줄로 거의 모든 진단이 됩니다. 위 예에서는 httpd_t가 user_home_t의 파일을 읽으려다 거절 — 정책이 그 조합을 허용하지 않았다는 뜻입니다. 답은 거의 항상 “그 파일의 라벨을 httpd_sys_content_t로 바꿔라” 입니다.
journalctl로도 보기 #
$ sudo journalctl -t setroubleshoot
Apr 16 10:23:14 rhel9-lab setroubleshoot[5678]:
SELinux is preventing httpd from read access on the file config.json.
For complete SELinux messages run: sealert -l ...setroubleshoot 데몬이 AVC를 사람이 읽기 좋은 메시지로 변환해 줍니다. 친절한 안내문에 가깝습니다.
sealert — 사람을 위한 트러블슈팅 안내문
#
setroubleshoot-server 패키지가 설치되어 있으면 더 친절한 도구를 쓸 수 있습니다.
$ sudo dnf install -y setroubleshoot-server
$ sudo systemctl enable --now setroubleshootd$ sudo sealert -a /var/log/audit/audit.log
100% done
found 1 alerts in /var/log/audit/audit.log
--------------------------------------------------------------------------------
SELinux is preventing /usr/sbin/httpd from read access on the file config.json.
***** Plugin restorecon (99.5 confidence) suggests ************************
If you want to fix the label.
/var/www/html/config.json default label should be httpd_sys_content_t.
Then you can run restorecon. The access attempt may have been stopped due
to insufficient permissions to access a parent directory in which case
try this command:
# restorecon -Rv /var/www/html/config.json
...Plugin이 추천 솔루션을 자신감 점수와 함께 보여 줍니다. 99.5%면 거의 그대로 따라가면 됩니다. 운영에서 SELinux 트러블슈팅의 절반은 이 한 명령으로 해결됩니다.
정책 만들기 — audit2allow
#
sealert가 답을 못 줄 때, 또는 직접 정책을 만들고 싶을 때 쓰는 명령입니다. AVC denial을 입력으로 받아 그걸 허용하는 정책 모듈을 자동으로 만들어 줍니다.
# 1) Permissive로 내려서 모든 작업을 한 번 돌림 (denial이 아닌 would-deny 까지 다 로깅)
$ sudo setenforce 0
$ # ... 앱을 한 번 정상 동작시킴 ...
$ sudo setenforce 1
# 2) 누적된 AVC를 모아 정책 모듈로 변환
$ sudo ausearch -m avc -ts recent | sudo audit2allow -M myapp
$ ls
myapp.pp myapp.te
# 3) 모듈 적용
$ sudo semodule -i myapp.pp.te 파일이 사람이 읽을 수 있는 정책, .pp가 컴파일된 모듈입니다. .te를 한 번 열어 검토하고 적용하는 게 안전합니다 — audit2allow는 보이는 모든 denial을 허용으로 바꿔주기 때문에, 잘못하면 보안 구멍을 정책으로 굳혀버릴 수 있습니다.
$ sudo semodule -l | grep myapp # 적용된 모듈 목록
$ sudo semodule -r myapp # 제거운영 권장 순서 — (1)
restorecon으로 풀리는지 먼저, (2) 안 되면semanage fcontext/semanage port로 정책에 등록, (3) boolean으로 풀리는 케이스인지 확인, (4) 그래도 안 되면 마지막에audit2allow. 1~3으로 풀리는 케이스가 90%가 넘습니다.
자주 만나는 함정 다섯 #
“포트를 바꿨더니 데몬이 못 떠요” #
기초 #7에서도 다룬 케이스. 22 → 2222처럼 비표준 포트로 sshd를 띄우면 SELinux가 막습니다. semanage port -a -t ssh_port_t -p tcp 2222로 등록.
“홈 디렉터리에 둔 웹 콘텐츠가 안 읽혀요” #
~/public_html 같은 경로에 웹 콘텐츠를 두면 라벨이 user_home_t라 httpd_t가 못 읽습니다. 두 가지 방법:
# 1) boolean으로 — 사용자 홈 디렉터리에서 httpd가 읽도록
$ sudo setsebool -P httpd_enable_homedirs on
$ sudo setsebool -P httpd_read_user_content on
# 2) 콘텐츠를 /var/www/html 또는 등록한 경로로 이동 (운영 권장)“mv로 옮긴 파일이 안 읽혀요”
#
mv는 원본 라벨을 가져올 수 있습니다. 옮긴 후에 restorecon -Rv <경로>를 한 번 실행하는 것이 안전합니다.
“DB가 비표준 포트로 열려야 하는데 막혀요” #
$ sudo semanage port -a -t postgresql_port_t -p tcp 5433웹,DB,메시징 데몬은 거의 모두 자기만의 포트 type이 있습니다. semanage port -l | grep <서비스>로 확인.
“Container가 호스트 디렉터리를 마운트했는데 안 보입니다” #
Podman / 도커 컨테이너가 호스트 볼륨을 마운트했는데 SELinux가 막을 때. 마운트 옵션에 :Z (전용) 또는 :z (공유)를 붙이면 자동으로 라벨이 잡혀요.
$ podman run -v /host/path:/container/path:Z myimage자세한 컨테이너 + SELinux는 이 시리즈 #7에서.
setroubleshoot 알림이 데스크톱에 뜰 때
#
GUI 환경에서는 setroubleshoot가 데스크톱 알림으로 AVC denial을 띄워줍니다. 학습 환경에서 켜두면 SELinux가 어디서 무엇을 막는지 즉시 보여서 학습 속도가 빨라요. 운영에서는 알림이 너무 많아 끄는 경우가 많지만, 학습용 VM에서는 권장합니다.
AlmaLinux / Rocky 차이 #
이번 글의 모든 명령이 그대로 동작합니다. SELinux 정책은 RHEL의 selinux-policy 패키지를 그대로 가져온 거라 차이가 없습니다. semanage, restorecon, audit2allow, sealert 모두 동일합니다.
자주 쓰는 명령 한 표 #
| 명령 | 하는 일 |
|---|---|
getenforce / sestatus | 현재 모드 / 상세 상태 |
setenforce 0/1 | 런타임 모드 전환 |
ls -Z <file> / ps -eZ / id -Z | 파일,프로세스,내 라벨 |
chcon -t <type> <file> | 라벨 임시 변경 |
restorecon -Rv <path> | 정책 기준으로 라벨 복구 |
semanage fcontext -a -t <type> "<regex>" | 영구 라벨 정책 등록 |
semanage port -a -t <type> -p tcp <port> | 포트 라벨 등록 |
getsebool -a / setsebool -P <bool> on/off | boolean 설정 |
ausearch -m avc -ts recent | 최근 AVC denial |
sealert -a /var/log/audit/audit.log | 사람이 읽는 진단 |
audit2allow -M <name> | denial로그를 정책 모듈로 변환 |
semodule -l / -i / -r | 정책 모듈 관리 |
정리 #
이번 글에서 잡은 그림:
- SELinux는 DAC 위에 한 층 더 두는 MAC — 루트도 우회 못 하는 정책 검사.
- 모드는 Enforcing (기본) / Permissive (디버깅) / Disabled (절대 금지). 임시는
setenforce, 영구는/etc/selinux/config. - 모든 파일,프로세스,포트에 **라벨 (context)**이 부여돼 있고, targeted 정책에선 사실상 type만 신경 쓰면 됨.
- 라벨 고치기는
restorecon(정책 기준 복구)이 90%,chcon(임시)은 보조. 영구 정책 변경은semanage fcontext. - 비표준 포트는 **
semanage port**로 등록. - 자주 쓰는 정책 옵션은 boolean으로 켜고/끄는 스위치 —
setsebool -P가 표준. - 차단됐을 때 흐름: AVC denial로그 →
sealert로 진단 →restorecon/semanage/ boolean / 마지막에audit2allow.
다음 — LVM #
기초 #6에서 잠깐 본 LVM을 본격적으로 다룹니다. 운영 디스크 관리의 표준입니다.
#2 LVM — PV/VG/LV, 스냅샷, 확장에서는 PV / VG / LV 세 층의 관계를 직접 만들어 보고, 디스크가 가득 찼을 때 새 PV를 추가해 LV를 늘리는 흐름, 스냅샷으로 백업 직전 상태를 잡고 복구하는 패턴, 그리고 thin provisioning과 striping 같은 한 단계 깊은 옵션까지 잡습니다.