LSM / Seccomp 심화
LSM과 seccomp를 리눅스 권한 통제의 핵심 경계 계층으로 심층 분석합니다. LSM hook 체계와 보안 객체 모델, SELinux/AppArmor/SMACK/Landlock 정책 특성 비교, seccomp-bpf 필터 설계와 우회 방지, capabilities 최소권한 적용, BPF LSM 기반 동적 정책 확장, 커스텀 LSM 모듈 작성 시 ABI·순서 의존성 주의점, 감사 로그와 운영 디버깅 절차, 컨테이너 환경에서의 정책 충돌 해결까지 실전 보안 운영 포인트를 다룹니다.
핵심 요약
- 전제 결합 — 보안, 성능, 아키텍처 지식을 함께 적용합니다.
- 경계 명확화 — API 경계와 ABI 영향 범위를 먼저 확인합니다.
- 위험 관리 — UAF, race, side-effect 가능성을 우선 점검합니다.
- 계측 기반 판단 — 추측 대신 데이터로 개선 여부를 판단합니다.
- 점진 적용 — 실험 범위를 작게 시작해 단계적으로 확장합니다.
단계별 이해
- 가설 수립
문제와 개선 목표를 수치로 정의합니다. - 제약 분석
호환성, 안정성, 보안 제약을 먼저 확인합니다. - 실험 적용
최소 변경으로 효과와 부작용을 측정합니다. - 정식 반영
검증된 변경만 문서화해 반영합니다.
개념 예시가 표시된 블록은 구조 이해 목적이며, 실습 예제가 표시된 블록은 사용자 공간에서 컴파일/실행을 고려한 형태입니다. 배포판별 기본 정책/활성 상태는 참고자료 — FIPS 지원/인증 상태 및 벤더 보안 공지를 기준으로 확인하세요.
리눅스 보안 모델 개요
Linux 커널의 보안은 단일 메커니즘이 아닌 다층 방어(Defense in Depth) 전략으로 구성됩니다. 각 계층이 독립적으로 동작하며, 하나의 계층이 우회되더라도 나머지 계층이 보호를 제공합니다.
DAC vs MAC
| 특성 | DAC (Discretionary) | MAC (Mandatory) |
|---|---|---|
| 정책 결정 주체 | 자원 소유자 (사용자) | 시스템 관리자 (정책) |
| 핵심 메커니즘 | uid/gid, rwx 퍼미션, ACL | 보안 레이블, 정책 규칙 |
| root 우회 | root는 모든 제한 우회 가능 | root도 정책에 종속 |
| 유연성 | 높음 (사용자 재량) | 낮음 (정책 고정) |
| 보안 수준 | 중간 — 사용자 실수에 취약 | 높음 — 정책 기반 강제 |
| 대표 구현 | UNIX 퍼미션, POSIX ACL | SELinux, AppArmor, SMACK |
보안 계층 구조
시스템 콜이 커널에 진입하면 다음 순서로 보안 검사를 통과해야 합니다:
- Seccomp-BPF — 시스템 콜 자체의 허용/거부 (가장 먼저 평가)
- DAC — 전통적 UNIX 퍼미션 검사 (uid/gid/mode)
- Capabilities — 세분화된 권한 비트 검사
- LSM 훅 — MAC 정책 기반 최종 접근 제어
LSM 프레임워크
LSM(Linux Security Modules)은 커널 시스템 콜 경로의 핵심 지점에 보안 훅(security hook)을 배치하여, 접근 제어 결정을 보안 모듈에 위임하는 프레임워크입니다. 2003년 커널 2.6에 도입된 이후, 현재 약 230개의 훅 포인트를 제공합니다.
security_hook_heads 구조
모든 LSM 훅은 security_hook_heads 구조체에 집중 관리됩니다. 각 필드는 해당 훅에 등록된 모든 LSM 콜백의 연결 리스트 헤드입니다.
/* 개념 예시: include/linux/lsm_hooks.h — 훅 헤드 구조체 (발췌) */
struct security_hook_heads {
struct hlist_head bprm_creds_for_exec;
struct hlist_head bprm_creds_from_file;
struct hlist_head bprm_check_security;
struct hlist_head task_alloc;
struct hlist_head task_free;
struct hlist_head task_fix_setuid;
struct hlist_head inode_alloc_security;
struct hlist_head inode_free_security;
struct hlist_head inode_permission;
struct hlist_head inode_create;
struct hlist_head file_permission;
struct hlist_head file_open;
struct hlist_head mmap_file;
struct hlist_head socket_create;
struct hlist_head socket_connect;
struct hlist_head socket_bind;
/* ... 약 230개의 훅 포인트 ... */
} __randomize_layout;
/* security/security.c — 글로벌 훅 헤드 인스턴스 */
struct security_hook_heads security_hook_heads __ro_after_init;
security_hook_list와 훅 등록
각 LSM 모듈은 security_hook_list 구조체 배열을 통해 자신의 콜백을 등록합니다. LSM_HOOK_INIT 매크로가 이를 간소화합니다.
/* 개념 예시: include/linux/lsm_hooks.h */
struct security_hook_list {
struct hlist_node list; /* 해시 리스트 연결 */
struct hlist_head *head; /* 소속 훅 헤드 포인터 */
union security_list_options hook; /* 실제 콜백 함수 */
const struct lsm_id *lsmid; /* LSM 식별자 */
};
/* 훅 등록 매크로 */
#define LSM_HOOK_INIT(HEAD, HOOK) \
{ .head = &security_hook_heads.HEAD, \
.hook = { .HEAD = HOOK } }
/* 예: SELinux 훅 등록 배열 (security/selinux/hooks.c) */
static struct security_hook_list selinux_hooks[] __ro_after_init = {
LSM_HOOK_INIT(bprm_creds_for_exec, selinux_bprm_creds_for_exec),
LSM_HOOK_INIT(inode_permission, selinux_inode_permission),
LSM_HOOK_INIT(file_open, selinux_file_open),
LSM_HOOK_INIT(socket_create, selinux_socket_create),
LSM_HOOK_INIT(task_alloc, selinux_task_alloc),
/* ... 수백 개의 훅 등록 ... */
};
훅 디스패치 메커니즘
커널 내부에서 LSM 훅이 호출되는 과정을 살펴봅니다. security_inode_permission()을 예로 들면:
/* 개념 예시: security/security.c — 훅 호출 래퍼 함수 */
int security_inode_permission(struct inode *inode, int mask)
{
/* DAC 검사 이후에 호출됨 */
if (unlikely(IS_PRIVATE(inode)))
return 0;
return call_int_hook(inode_permission, inode, mask);
}
/* call_int_hook 매크로 — 모든 등록된 LSM 순회 */
#define call_int_hook(FUNC, ...) ({ \
int RC = 0; \
struct security_hook_list *P; \
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != 0) \
break; \
} \
RC; \
})
call_int_hook은 등록된 모든 LSM 훅을 순차 호출합니다. 하나라도 비-0(거부)을 반환하면 즉시 중단하고 거부를 반환합니다. 모든 LSM이 0(허용)을 반환해야 최종 허용됩니다.
LSM 스태킹
최근 커널에서는 여러 major LSM(SELinux, AppArmor, SMACK) 동시 활성화 지원이 확대되었습니다. minor LSM(capability, yama, landlock, lockdown)은 major LSM과 함께 동작하며, 실제 활성 조합은 커널 버전/배포판 설정에 따라 달라집니다.
# 현재 활성 LSM 확인
cat /sys/kernel/security/lsm
# 출력 예: lockdown,capability,landlock,yama,apparmor
# 부트 커맨드라인으로 LSM 순서 지정
# GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,selinux"
# Kconfig에서 기본 LSM 순서 설정
# CONFIG_LSM="lockdown,capability,landlock,yama,apparmor,selinux"
# LSM 관련 커널 설정 확인
grep CONFIG_LSM /boot/config-$(uname -r)
grep CONFIG_SECURITY /boot/config-$(uname -r)
주요 LSM 훅 카테고리
| 카테고리 | 주요 훅 | 호출 시점 |
|---|---|---|
| 프로세스 (bprm) | bprm_creds_for_exec, bprm_check_security | execve() 시 실행 파일 검증 |
| 태스크 (task) | task_alloc, task_kill, task_setscheduler | 프로세스 생성, 시그널, 스케줄링 |
| 파일 (file) | file_permission, file_open, mmap_file | 파일 접근, mmap 시 |
| inode | inode_permission, inode_create, inode_unlink | inode 생성/삭제/접근 |
| 소켓 (socket) | socket_create, socket_connect, socket_bind | 네트워크 소켓 연산 |
| 키 (key) | key_alloc, key_permission | 키링 접근 |
| IPC | msg_queue_msgsnd, shm_shmat | SysV IPC 접근 |
| 마운트 | sb_mount, sb_umount, sb_kern_mount | 파일시스템 마운트 |
SELinux
SELinux(Security-Enhanced Linux)는 NSA가 개발한 MAC 시스템으로, Type Enforcement(TE), RBAC(Role-Based Access Control), MLS/MCS(Multi-Level/Multi-Category Security) 모델을 결합한 가장 포괄적인 리눅스 보안 프레임워크입니다. 배포판별 기본 활성화 정책은 참고자료에서 최신 상태를 확인하세요.
Type Enforcement (TE)
TE는 SELinux의 핵심입니다. 모든 프로세스(subject)와 자원(object)에 보안 컨텍스트를 부여하고, 정책 규칙에 따라 접근을 허용/거부합니다.
# 보안 컨텍스트 확인
ls -Z /etc/passwd
# system_u:object_r:passwd_file_t:s0 /etc/passwd
ps -eZ | grep sshd
# system_u:system_r:sshd_t:s0-s0:c0.c1023 1234 ? sshd
# 컨텍스트 형식: user:role:type:level
# user — SELinux 사용자 (system_u, unconfined_u)
# role — 역할 (system_r, object_r)
# type — 타입/도메인 (TE 정책의 핵심 단위)
# level — MLS/MCS 레벨 (s0, s0:c0.c1023)
# TE 정책 규칙 예시
# allow source_domain target_type : object_class { permissions };
allow httpd_t httpd_config_t : file { read getattr open };
allow httpd_t httpd_log_t : file { append create write };
allow httpd_t http_port_t : tcp_socket { name_bind };
# 거부 규칙 (명시적 차단)
neverallow httpd_t shadow_t : file { read write };
# 타입 전이 (type_transition) — 프로세스 도메인 전환
# init_t가 httpd_exec_t를 실행하면 httpd_t 도메인으로 전이
type_transition init_t httpd_exec_t : process httpd_t;
RBAC 및 MLS/MCS
RBAC은 사용자가 어떤 역할(role)을 수행할 수 있는지, 각 역할이 어떤 도메인에 진입할 수 있는지를 제어합니다. MLS(Multi-Level Security)는 군사 등급(s0~s15)과 카테고리(c0~c1023)를 사용하여 정보 흐름을 통제합니다.
# RBAC — 역할 정의 및 사용자 매핑
role system_r types { httpd_t sshd_t crond_t };
role webadmin_r types { httpd_t httpd_config_t };
# 사용자→역할 매핑
user staff_u roles { staff_r webadmin_r };
# MLS — 레벨 기반 접근 제어
# s0 = unclassified, s1 = confidential, s2 = secret, s3 = top secret
# 읽기: subject 레벨 >= object 레벨 (no read up)
# 쓰기: subject 레벨 <= object 레벨 (no write down)
mlsconstrain file { read } (h1 dom h2);
mlsconstrain file { write } (l1 domby l2);
SELinux 관리 명령어
# SELinux 상태 확인
getenforce # Enforcing / Permissive / Disabled
sestatus # 상세 상태 출력
# 모드 전환 (재부팅 없이)
setenforce 0 # Permissive (로깅만, 차단 안 함)
setenforce 1 # Enforcing (실제 차단)
# 정책 모듈 관리
semodule -l # 설치된 정책 모듈 목록
semodule -i mypolicy.pp # 정책 모듈 설치
# audit2allow — 거부 로그에서 정책 자동 생성
ausearch -m avc -ts recent | audit2allow -M mypolicy
semodule -i mypolicy.pp
# semanage — 정책 맞춤 설정
semanage port -a -t http_port_t -p tcp 8080 # 포트 레이블 추가
semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"
restorecon -Rv /srv/web # 컨텍스트 복원
# Boolean — 런타임 정책 조정
getsebool -a | grep httpd
setsebool -P httpd_can_network_connect on
SELinux 커널 내부 구조
/* 개념 예시: security/selinux/hooks.c — inode_permission 훅 구현 */
static int selinux_inode_permission(struct inode *inode, int mask)
{
const struct cred *cred = current_cred();
struct common_audit_data ad;
u32 perms;
int rc;
/* DAC 통과 후 호출됨 — MAC 추가 검사 */
if (!mask)
return 0;
/* 요청된 마스크를 SELinux AV 퍼미션으로 변환 */
perms = file_mask_to_av(inode->i_mode, mask);
/* AVC(Access Vector Cache)에서 결정 조회 또는 정책 엔진에 질의 */
rc = inode_has_perm(cred, inode, perms, &ad);
return rc;
}
/* AVC — 접근 결정 캐시 (성능 핵심) */
/* security/selinux/avc.c */
struct avc_entry {
u32 ssid; /* source SID */
u32 tsid; /* target SID */
u16 tclass; /* target object class */
struct av_decision avd; /* 캐시된 접근 결정 */
};
Android SELinux 정책
Android는 SELinux Enforcing 모드를 필수로 요구한다 (Android 5.0+). Binder IPC 전용 LSM 훅을 통해 프로세스 간 통신을 세밀하게 제어하며, neverallow 규칙으로 앱이 HAL이나 시스템 서비스에 직접 접근하는 것을 차단한다.
/* Binder 관련 SELinux LSM 훅 (security/selinux/hooks.c) */
static int selinux_binder_transaction(
const struct cred *from,
const struct cred *to)
{
return avc_has_perm(from_sid, to_sid,
SECCLASS_BINDER, BINDER__CALL, NULL);
}
/* Android SELinux 정책 규칙 예시 */
# allow system_server surfaceflinger:binder { call transfer };
# neverallow untrusted_app hal_camera_server:binder call;
Android SELinux의 컨텍스트 파일(file_contexts, service_contexts, hwservice_contexts 등)과 부팅 시 Enforcing 설정 과정은 Android 커널 — SELinux 정책 섹션을 참고하라.
SELinux 정책 컴파일 파이프라인
SELinux 정책은 소스 파일에서 커널이 사용하는 바이너리 형태까지 다단계 컴파일 과정을 거칩니다.
AppArmor
AppArmor는 경로 기반(path-based) 접근 제어를 사용하는 MAC 시스템입니다. SELinux의 레이블 기반 모델에 비해 프로파일 작성과 관리가 상대적으로 간단합니다. 배포판별 기본 활성화 정책은 참고자료를 확인하세요.
프로파일 기반 접근 제어
# /etc/apparmor.d/usr.sbin.nginx — Nginx 프로파일 예시
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
#include <abstractions/nameservice>
# 실행 파일
/usr/sbin/nginx mr,
# 설정 파일 읽기
/etc/nginx/** r,
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
# 로그 쓰기
/var/log/nginx/*.log w,
# 웹 콘텐츠 읽기
/var/www/** r,
/srv/www/** r,
# PID 파일
/run/nginx.pid rw,
# 네트워크 접근
network inet stream,
network inet6 stream,
# 자식 프로세스 제한
/usr/sbin/nginx Cx -> worker,
profile worker {
#include <abstractions/base>
/var/www/** r,
/var/log/nginx/*.log w,
network inet stream,
deny /etc/shadow r,
}
}
AppArmor 모드와 관리
# AppArmor 상태 확인
aa-status # 전체 프로파일 상태
aa-enabled # AppArmor 활성화 여부
# 프로파일 모드
aa-enforce /etc/apparmor.d/usr.sbin.nginx # enforce 모드 (실제 차단)
aa-complain /etc/apparmor.d/usr.sbin.nginx # complain 모드 (로깅만)
aa-disable /etc/apparmor.d/usr.sbin.nginx # 프로파일 비활성화
# 프로파일 자동 생성
aa-genprof /usr/sbin/nginx # 대화형 프로파일 생성기
aa-logprof # 로그 기반 프로파일 개선
# 프로파일 적용
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx # 리로드
apparmor_parser -a /etc/apparmor.d/usr.sbin.nginx # 추가
경로 기반 vs 레이블 기반
| 특성 | AppArmor (경로 기반) | SELinux (레이블 기반) |
|---|---|---|
| 식별 방식 | 파일 시스템 경로 | inode에 부착된 보안 레이블 |
| 하드링크 처리 | 경로별로 다른 정책 가능 (우회 위험) | inode 레이블이므로 하드링크 무관 |
| 파일 이동 | 새 경로에 맞는 정책 자동 적용 | 레이블 유지 (restorecon 필요할 수 있음) |
| 정책 작성 난이도 | 상대적으로 쉬움 (경로 기반 직관적) | 복잡함 (타입, 역할, 전이 규칙) |
| 보안 강도 | 경로 우회 가능성 있음 | inode 레이블이므로 우회 어려움 |
| 배포판 기본 정책 | 배포판/버전별 상이 (최신 상태는 참고자료 참조) | 배포판/버전별 상이 (최신 상태는 참고자료 참조) |
AppArmor 커널 내부 구조
AppArmor의 커널 측 자료구조는 aa_label, aa_profile, aa_ns 세 가지 핵심 구조체를 중심으로 구성됩니다.
/* 개념 예시: security/apparmor/include/label.h */
struct aa_label {
struct kref count; /* 참조 카운트 */
int size; /* 프로파일 수 */
struct aa_profile *vec[]; /* 프로파일 벡터 (스택 지원) */
};
/* security/apparmor/include/policy.h */
struct aa_profile {
struct aa_policy base; /* 이름, 참조 카운트 */
struct aa_ns *ns; /* 소속 네임스페이스 */
const char *rename; /* 프로파일 별칭 */
enum audit_mode audit; /* 감사 모드 */
long mode; /* enforce / complain / kill */
struct aa_policydb file; /* 파일 접근 DFA/HFA */
struct aa_policydb policy; /* 정책 관리 규칙 */
struct aa_rlimit rlimits; /* 자원 제한 */
struct aa_net_compat *net_compat; /* 네트워크 규칙 */
};
/* security/apparmor/include/policy_ns.h */
struct aa_ns {
struct aa_policy base; /* 이름, 참조 카운트 */
struct aa_ns *parent; /* 부모 네임스페이스 */
struct list_head sub_ns; /* 자식 네임스페이스 목록 */
struct aa_labelset labels; /* 네임스페이스 내 레이블 */
struct dentry *dents[AAFS_NS_SIZEOF]; /* apparmorfs 엔트리 */
};
프로파일 컴파일 과정은 사용자 공간에서 시작하여 커널에서 언패킹됩니다:
# 프로파일 컴파일 및 로드 과정
# 1. 텍스트 프로파일 작성
cat /etc/apparmor.d/usr.sbin.nginx
# 2. apparmor_parser가 DFA/HFA 바이너리로 컴파일
apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx
# 텍스트 프로파일 → 정규식 파싱 → DFA 구성 → HFA 최적화 → 바이너리
# 3. 바이너리 정책을 /sys/kernel/security/apparmor/.load에 기록
# → 커널 aa_unpack()이 바이너리를 파싱하여 aa_profile 구조체 생성
# 4. 프로파일 캐시 확인 (재부팅 시 빠른 로드)
ls /etc/apparmor.d/cache.d/
/* 개념 예시: aa_file_perm 내부 흐름 (security/apparmor/file.c) */
int aa_file_perm(const char *op, struct aa_label *label,
struct file *file, u32 request)
{
struct aa_profile *profile;
char *buffer = aa_get_buffer(false);
const char *name;
int error = 0;
/* 1. 파일 경로 해석 */
name = d_path(&file->f_path, buffer, aa_g_path_max);
/* 2. 레이블 내 각 프로파일에 대해 검사 (스택 지원) */
for_each_profile(profile, label) {
struct aa_perms perms;
/* 3. DFA/HFA 매칭 — 경로를 상태 머신에 입력 */
aa_state_t state = aa_dfa_match(
profile->file.dfa,
profile->file.start, name);
/* 4. 상태에서 허용된 퍼미션 조회 */
aa_compute_fperms(profile->file.perms,
state, &perms);
/* 5. 요청 vs 허용 비교 */
error = aa_check_perms(profile, &perms,
request, &sa);
if (error)
break;
}
aa_put_buffer(buffer);
return error;
}
SMACK
SMACK(Simplified Mandatory Access Control Kernel)은 임베디드 환경과 IoT 디바이스를 위한 경량 MAC 시스템입니다. SELinux보다 훨씬 단순한 레이블 기반 모델로, 최소한의 관리 오버헤드로 MAC를 제공합니다. Tizen OS, 차량용 Linux(AGL) 등에서 사용됩니다.
SMACK 레이블과 규칙
# SMACK 레이블 확인
attr -S -g SMACK64 /etc/passwd
# 출력: System
# 프로세스 레이블 확인
cat /proc/self/attr/current
# 출력: User
# SMACK 접근 규칙 형식: subject_label object_label access
# access: r(read) w(write) x(execute) a(append) t(transmute) l(lock)
# 규칙 설정
echo "Web Data rx" > /sys/fs/smackfs/load2
echo "Web Log rwa" > /sys/fs/smackfs/load2
# 특수 레이블
# _ — 바닥 레이블 (모든 레이블이 읽기 가능)
# * — 스타 레이블 (모든 접근 허용)
# ^ — 햇 레이블 (모든 레이블이 읽기/실행 가능)
# 레이블 설정
attr -S -s SMACK64 -V "Web" /var/www/html/index.html
SMACK 커널 구현
/* 개념 예시: security/smack/smack_lsm.c — 핵심 접근 검사 */
static int smk_access(struct smack_known *subject,
struct smack_known *object,
int request, struct smk_audit_info *a)
{
/* 특수 레이블 검사 */
if (subject == &smack_known_star) /* * 레이블: 모두 허용 */
return 0;
if (subject == object) /* 같은 레이블: 허용 */
return 0;
if (object == &smack_known_floor && /* _ 레이블 읽기: 허용 */
(request & (MAY_READ | MAY_EXEC)))
return 0;
/* 규칙 테이블에서 접근 허용 여부 조회 */
return smk_check_access(subject, object, request);
}
Landlock
Landlock는 커널 5.13+에 도입된 비특권 샌드박싱 메커니즘입니다. root 권한 없이도 프로세스가 자발적으로 자신의 접근 권한을 제한할 수 있어, 애플리케이션 수준의 최소 권한 원칙을 구현합니다. 파일시스템(5.13+)과 네트워크(6.4+) 접근 제한을 지원합니다.
Landlock API
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <unistd.h>
/* 실습 예제: 1단계 Ruleset 생성 — 제한할 접근 유형 선언 */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_EXECUTE,
/* 커널 6.4+ 네트워크 제한 */
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
};
int ruleset_fd = syscall(SYS_landlock_create_ruleset,
&ruleset_attr, sizeof(ruleset_attr), 0);
/* 2단계: 규칙 추가 — 허용할 경로 지정 */
struct landlock_path_beneath_attr path_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
syscall(SYS_landlock_add_rule, ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0);
close(path_attr.parent_fd);
/* 네트워크 규칙 (6.4+) */
struct landlock_net_port_attr net_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.port = 8080,
};
syscall(SYS_landlock_add_rule, ruleset_fd,
LANDLOCK_RULE_NET_PORT, &net_attr, 0);
/* 3단계: 자발적 제한 적용 (되돌릴 수 없음!) */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
syscall(SYS_landlock_restrict_self, ruleset_fd, 0);
close(ruleset_fd);
/* 이후 이 프로세스는 /usr 읽기와 TCP 8080 바인딩만 가능 */
landlock_restrict_self()로 적용된 제한은 되돌릴 수 없습니다. 프로세스와 모든 자식 프로세스에 영속적으로 적용됩니다. PR_SET_NO_NEW_PRIVS로 권한 상승도 방지됩니다.
Best-effort 호환성 패턴
Landlock ABI는 커널 버전마다 지원 범위가 다릅니다. 세부 버전 매핑은 참고자료 — 보안 & 암호화를 기준으로 관리하고, 여기서는 best-effort 호환 패턴에 집중합니다.
/* 실습 예제: Landlock ABI 버전 확인 */
int abi = syscall(SYS_landlock_create_ruleset, NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
/* ABI별 상세 기능 매핑은 references.html 기준으로 확인 */
/* 커널이 지원하지 않는 플래그 제거 */
switch (abi) {
case 1:
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
/* fall through */
case 2:
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
/* fall through */
case 3:
ruleset_attr.handled_access_net = 0;
/* fall through */
}
Capabilities
POSIX Capabilities는 전통적인 root/non-root 이분법을 세분화하여, 프로세스에 필요한 최소한의 특권만 부여합니다. 커널은 약 40개 이상의 capability 비트를 정의하며, task_struct → cred → cap_* 필드에 저장됩니다.
Capability 세트
| 세트 | 필드 | 설명 |
|---|---|---|
| Effective (e) | cap_effective | 현재 커널이 실제 권한 검사에 사용하는 세트 |
| Permitted (p) | cap_permitted | 프로세스가 effective에 추가할 수 있는 상한선 |
| Inheritable (i) | cap_inheritable | execve() 시 자식에게 전달 가능한 세트 |
| Bounding | cap_bnd | execve() 시 permitted에 추가 가능한 상한선 |
| Ambient | cap_ambient | 비특권 프로그램 실행 시에도 유지되는 세트 (커널 4.3+) |
커널 내부 구조
/* 개념 예시: include/linux/cred.h */
struct cred {
atomic_long_t usage;
kuid_t uid, euid, suid, fsuid;
kgid_t gid, egid, sgid, fsgid;
struct group_info *group_info;
/* Capability 세트 */
kernel_cap_t cap_inheritable;
kernel_cap_t cap_permitted;
kernel_cap_t cap_effective;
kernel_cap_t cap_bset; /* bounding set */
kernel_cap_t cap_ambient;
/* LSM 보안 데이터 */
void *security; /* LSM blob */
/* ... */
};
/* include/uapi/linux/capability.h — 주요 capability 상수 */
#define CAP_CHOWN 0 /* 파일 소유자 변경 */
#define CAP_NET_BIND_SERVICE 10 /* 1024 미만 포트 바인딩 */
#define CAP_NET_ADMIN 12 /* 네트워크 설정 변경 */
#define CAP_NET_RAW 13 /* RAW 소켓 사용 */
#define CAP_SYS_ADMIN 21 /* 다양한 관리 권한 (가장 강력) */
#define CAP_SYS_PTRACE 19 /* ptrace 사용 */
#define CAP_SYS_MODULE 16 /* 커널 모듈 로드/언로드 */
#define CAP_BPF 39 /* BPF 프로그램 로드 (커널 5.8+) */
커널 내 Capability 검사
/* 개념 예시: kernel/capability.c — 권한 검사 핵심 함수 */
bool ns_capable(struct user_namespace *ns, int cap)
{
return ns_capable_common(ns, cap, CAP_OPT_NONE);
}
static bool ns_capable_common(struct user_namespace *ns,
int cap, unsigned int opts)
{
struct cred *cred = current_cred();
/* effective 세트에 해당 capability가 있는지 확인 */
if (!cap_raised(cred->cap_effective, cap))
return false;
/* user namespace 검사 — 네임스페이스 내에서만 유효 */
if (!in_userns(cred->user_ns, ns))
return false;
/* LSM 훅 호출 — LSM이 추가로 거부할 수 있음 */
return security_capable(cred, ns, cap, opts) == 0;
}
/* 사용 예: raw socket 생성 시 */
/* net/ipv4/af_inet.c */
if (sock->type == SOCK_RAW && !capable(CAP_NET_RAW))
return -EPERM;
Capability 관리 CLI
# 프로세스 capability 확인
getpcaps $$
cat /proc/$$/status | grep Cap
# 파일에 capability 부여 (setcap)
setcap cap_net_bind_service=+ep /usr/bin/myserver
# e=effective, p=permitted, i=inheritable
# 파일 capability 확인
getcap /usr/bin/myserver
# 특정 capability만으로 프로그램 실행 (capsh)
capsh --caps="cap_net_bind_service+eip" -- -c "/usr/bin/myserver"
# capability 디코딩 (proc 출력은 16진수 비트마스크)
capsh --decode=00000000a80425fb
P(ambient)는 setuid 바이너리나 file capabilities가 설정된 파일 실행 시 자동으로 0으로 초기화됩니다. 이는 권한 상승 우회를 방지하기 위한 설계입니다. 비특권 프로그램에서만 ambient가 유지됩니다.
Seccomp
Seccomp(Secure Computing)은 프로세스가 사용할 수 있는 시스템 콜을 제한하는 커널 메커니즘입니다. 두 가지 모드가 있으며, 특히 filter mode(seccomp-bpf)가 컨테이너, 브라우저, 샌드박스에서 핵심 보안 계층으로 사용됩니다.
Seccomp 모드
| 모드 | 설명 | 사용 시나리오 |
|---|---|---|
| Strict (모드 1) | read, write, _exit, sigreturn만 허용 | 극도로 제한된 계산 전용 프로세스 |
| Filter (모드 2) | BPF 프로그램으로 시스템 콜별 허용/거부/수정 | 컨테이너, 브라우저, 일반 샌드박싱 |
Strict Mode
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
/* 실습 예제: strict mode 활성화 — read/write/_exit/sigreturn만 허용 */
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
/* 또는 */
syscall(SYS_seccomp, SECCOMP_SET_MODE_STRICT, 0, NULL);
/* 이후 다른 시스템 콜 호출 시 SIGKILL로 종료 */
Filter Mode (Seccomp-BPF)
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <asm/unistd.h>
#include <stddef.h>
#include <errno.h>
/* 실습 예제: BPF 필터로 execve() 시스템 콜 차단 */
struct sock_filter filter[] = {
/* seccomp_data 구조체에서 아키텍처 로드 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, arch)),
/* x86_64인지 확인 */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
/* 시스템 콜 번호 로드 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
/* execve(59) 차단 */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
/* 나머지 허용 */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
/* 필터 적용 */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
SECCOMP_RET_* 반환 값
| 반환 값 | 우선순위 | 동작 |
|---|---|---|
SECCOMP_RET_KILL_PROCESS | 최고 | 프로세스 전체 종료 (SIGSYS) |
SECCOMP_RET_KILL_THREAD | 높음 | 해당 스레드만 종료 |
SECCOMP_RET_TRAP | 중간 | SIGSYS 시그널 전달 (핸들링 가능) |
SECCOMP_RET_ERRNO | 중간 | 지정된 errno 반환 |
SECCOMP_RET_USER_NOTIF | 중간 | 사용자 공간 핸들러에 알림 (supervisor) |
SECCOMP_RET_TRACE | 낮음 | ptrace 트레이서에 통지 |
SECCOMP_RET_LOG | 낮음 | 허용하되 로그 기록 |
SECCOMP_RET_ALLOW | 최저 | 시스템 콜 허용 |
커널 내부 Seccomp 처리 경로
/* 개념 예시: kernel/seccomp.c — 시스템 콜 진입 시 seccomp 필터 실행 */
int __secure_computing(const struct seccomp_data *sd)
{
int this_syscall;
struct seccomp_filter *f;
this_syscall = sd->nr;
switch (current->seccomp.mode) {
case SECCOMP_MODE_STRICT:
return __seccomp_filter(this_syscall, sd, true);
case SECCOMP_MODE_FILTER:
return __seccomp_filter(this_syscall, sd, false);
default:
BUG();
}
}
/* seccomp_data — BPF 필터에 전달되는 데이터 */
struct seccomp_data {
int nr; /* 시스템 콜 번호 */
__u32 arch; /* AUDIT_ARCH_* */
__u64 instruction_pointer;
__u64 args[6]; /* 시스템 콜 인자 */
};
Seccomp-BPF 실전
실무에서는 raw BPF 명령어 대신 libseccomp 라이브러리를 사용하여 가독성 높은 필터를 작성합니다.
libseccomp 사용
#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
/* 실습 예제: libseccomp 기반 최소 허용 정책 */
int main(void)
{
scmp_filter_ctx ctx;
/* 기본 동작: 허용되지 않은 시스템 콜은 EPERM 반환 */
ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
if (!ctx) return 1;
/* 화이트리스트: 필요한 시스템 콜만 허용 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* 인자 조건부 필터링: write()는 stdout/stderr만 허용 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO));
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, STDERR_FILENO));
/* 필터 적용 */
seccomp_load(ctx);
seccomp_release(ctx);
/* 이제 허용된 시스템 콜만 사용 가능 */
printf("Sandbox active\\n");
return 0;
}
# 빌드
gcc -o sandbox sandbox.c -lseccomp
# 실행 — 허용되지 않은 시스콜 시도 시 EPERM
./sandbox
Docker/Container Seccomp 프로파일
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{ "architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] }
],
"syscalls": [
{
"names": ["read", "write", "open", "openat", "close",
"fstat", "lstat", "poll", "lseek", "mmap",
"mprotect", "munmap", "brk", "ioctl",
"access", "pipe", "select", "sched_yield",
"dup", "dup2", "socket", "connect", "accept",
"bind", "listen", "clone", "fork", "execve",
"exit", "exit_group", "wait4", "kill",
"getpid", "getuid", "getgid"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["ptrace"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1
},
{
"names": ["keyctl", "add_key", "request_key"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1
}
]
}
# Docker에서 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=my-profile.json nginx
# seccomp 완전 비활성화 (보안 위험!)
docker run --security-opt seccomp=unconfined nginx
# 기본 프로파일 확인 (Docker는 약 300개+ 시스콜 허용)
docker info --format '{{ .SecurityOptions }}'
# seccomp-bpf 필터 확인
grep Seccomp /proc/$(pgrep nginx)/status
# Seccomp: 2 (mode 2 = filter)
# Seccomp_filters: 1 (필터 체인 수)
Seccomp User Notification (Supervisor 모드)
커널 5.0+에서 SECCOMP_RET_USER_NOTIF를 사용하면, 특정 시스템 콜을 사용자 공간의 supervisor 프로세스가 대신 처리할 수 있습니다. 이는 컨테이너 런타임(runc, crun)에서 권한이 필요한 시스콜을 안전하게 에뮬레이션하는 데 사용됩니다.
/* 개념 예시: Supervisor 측 seccomp 알림 수신 및 처리 */
int notifyfd = seccomp(SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
/* 알림 대기 */
struct seccomp_notif *req;
struct seccomp_notif_resp *resp;
seccomp_notify_alloc(&req, &resp);
while (1) {
ioctl(notifyfd, SECCOMP_IOCTL_NOTIF_RECV, req);
/* 요청된 시스콜 분석 */
printf("PID %d called syscall %d\\n", req->pid, req->data.nr);
/* supervisor가 대신 처리 후 결과 반환 */
resp->id = req->id;
resp->val = 0; /* 성공 반환값 */
resp->error = 0;
resp->flags = 0;
ioctl(notifyfd, SECCOMP_IOCTL_NOTIF_SEND, resp);
}
LSM BPF (BPF LSM)
BPF LSM(커널 5.7+)은 BPF 프로그램을 LSM 훅에 동적으로 attach하여, 재부팅 없이 커스텀 보안 정책을 적용할 수 있는 메커니즘입니다. CONFIG_BPF_LSM=y와 부트 파라미터 lsm=...,bpf로 활성화합니다.
BPF LSM 프로그램 작성
/* 실습 예제: bpf_lsm_deny_unlink.bpf.c — 특정 파일 삭제 방지 */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
/* LSM 훅 inode_unlink에 attach */
SEC("lsm/inode_unlink")
int BPF_PROG(deny_unlink, struct inode *dir,
struct dentry *dentry)
{
const char target[] = "protected_file";
char name[256];
/* dentry 이름 읽기 */
bpf_probe_read_kernel_str(name, sizeof(name),
dentry->d_name.name);
/* "protected_file" 삭제 시도를 차단 */
for (int i = 0; i < sizeof(target) - 1; i++) {
if (name[i] != target[i])
return 0; /* 이름 불일치: 허용 */
}
/* 일치: EPERM 반환 (삭제 거부) */
return -EPERM;
}
char LICENSE[] SEC("license") = "GPL";
# BPF LSM 프로그램 빌드 및 로드
clang -O2 -target bpf -g -c bpf_lsm_deny_unlink.bpf.c -o deny_unlink.bpf.o
bpftool prog load deny_unlink.bpf.o /sys/fs/bpf/deny_unlink
# 현재 LSM에 bpf가 포함되어 있는지 확인
cat /sys/kernel/security/lsm
# lockdown,capability,landlock,yama,apparmor,bpf
# attach된 BPF LSM 프로그램 확인
bpftool prog list | grep lsm
BPF LSM Attach 포인트
BPF LSM은 security_hook_heads에 정의된 모든 훅 포인트에 attach할 수 있습니다. 주요 attach 포인트:
| SEC() 이름 | 훅 포인트 | 용도 |
|---|---|---|
lsm/file_open | security_file_open | 파일 열기 제어 |
lsm/inode_permission | security_inode_permission | inode 접근 제어 |
lsm/inode_unlink | security_inode_unlink | 파일 삭제 제어 |
lsm/bprm_check_security | security_bprm_check | 프로그램 실행 제어 |
lsm/socket_connect | security_socket_connect | 네트워크 연결 제어 |
lsm/task_alloc | security_task_alloc | 프로세스 생성 제어 |
CAP_BPF + CAP_SYS_ADMIN이 필요한 관리자용 도구입니다. Landlock는 비특권 사용자도 사용할 수 있는 자발적 샌드박싱입니다. 둘은 상호 보완적이며 동시 사용이 가능합니다.
Credentials
Linux 커널에서 모든 보안 결정의 기반이 되는 것은 struct cred입니다. 프로세스의 UID/GID, capability 세트, LSM 보안 데이터가 모두 이 구조체에 집중됩니다.
struct cred 상세
/* 개념 예시: include/linux/cred.h — 프로세스 자격 증명 */
struct cred {
atomic_long_t usage; /* 참조 카운트 */
/* POSIX UID/GID — DAC의 기반 */
kuid_t uid; /* 실제 UID */
kuid_t euid; /* 유효 UID (권한 검사용) */
kuid_t suid; /* 저장된 UID (setuid 복원용) */
kuid_t fsuid; /* 파일시스템 UID */
kgid_t gid, egid, sgid, fsgid;
/* 보조 그룹 */
struct group_info *group_info;
/* Capability 세트 (5종) */
kernel_cap_t cap_inheritable;
kernel_cap_t cap_permitted;
kernel_cap_t cap_effective;
kernel_cap_t cap_bset;
kernel_cap_t cap_ambient;
/* User Namespace */
struct user_namespace *user_ns;
/* 키링 */
struct key *session_keyring;
struct key *process_keyring;
struct key *thread_keyring;
/* LSM 보안 blob — SELinux, AppArmor 등의 보안 데이터 */
void *security;
struct rcu_head rcu; /* RCU 해제용 */
};
Credential 수명 주기
/* 개념 예시: credential 교체 패턴 (COW: Copy-On-Write) */
struct cred *new_cred;
/* 1. 현재 cred 복사본 생성 */
new_cred = prepare_creds();
if (!new_cred)
return -ENOMEM;
/* 2. 복사본 수정 */
new_cred->euid = KUIDT_INIT(0); /* euid를 root로 변경 */
cap_raise(new_cred->cap_effective, CAP_NET_ADMIN);
/* 3. 원자적으로 교체 (RCU 기반) */
commit_creds(new_cred);
/* 참고: 읽기 시에는 RCU로 안전하게 접근 */
const struct cred *cred;
rcu_read_lock();
cred = rcu_dereference(current->cred);
uid_t uid = from_kuid(&init_user_ns, cred->uid);
rcu_read_unlock();
/* task_struct에서의 cred 접근 */
struct task_struct {
/* ... */
const struct cred __rcu *real_cred; /* 객관적 cred (서버 측) */
const struct cred __rcu *cred; /* 주관적 cred (클라이언트 측) */
/* ... */
};
real_cred는 태스크가 "대상"으로서 평가될 때(예: 시그널 수신 시) 사용되고, cred는 태스크가 "주체"로서 행동할 때(예: 파일 열기) 사용됩니다. 대부분의 경우 두 포인터는 같은 struct cred를 가리킵니다.
LSM 모듈 작성
커스텀 LSM 모듈을 커널에 내장하는 방법을 살펴봅니다. LSM은 로드 가능 모듈(LKM)이 아닌, 커널 빌드 시 정적 링크되어야 합니다.
커스텀 LSM 예제: 실행 파일 경로 제한
/* 개념 예시: security/myguard/myguard_lsm.c */
#include <linux/lsm_hooks.h>
#include <linux/security.h>
#include <linux/binfmts.h>
#include <linux/dcache.h>
/* LSM 식별자 */
static const struct lsm_id myguard_lsmid = {
.name = "myguard",
.id = LSM_ID_UNDEF,
};
/* bprm_check_security 훅 — execve() 검사 */
static int myguard_bprm_check(struct linux_binprm *bprm)
{
const char *path;
char buf[256];
/* 실행 파일 경로 획득 */
path = d_path(&bprm->file->f_path, buf, sizeof(buf));
if (IS_ERR(path))
return 0;
/* /tmp 아래 실행 파일 차단 */
if (strncmp(path, "/tmp/", 5) == 0) {
pr_warn("myguard: blocked exec from /tmp: %s (pid=%d)\\n",
path, current->pid);
return -EACCES;
}
return 0;
}
/* file_open 훅 — 민감 파일 접근 감사 */
static int myguard_file_open(struct file *file)
{
char buf[256];
const char *path = d_path(&file->f_path, buf, sizeof(buf));
if (!IS_ERR(path) && strncmp(path, "/etc/shadow", 11) == 0) {
pr_notice("myguard: /etc/shadow accessed by pid=%d uid=%d\\n",
current->pid,
from_kuid(&init_user_ns, current_cred()->uid));
}
return 0; /* 감사만, 차단하지 않음 */
}
/* 훅 등록 배열 */
static struct security_hook_list myguard_hooks[] __ro_after_init = {
LSM_HOOK_INIT(bprm_check_security, myguard_bprm_check),
LSM_HOOK_INIT(file_open, myguard_file_open),
};
/* LSM 초기화 */
static int __init myguard_init(void)
{
pr_info("myguard: initializing\\n");
security_add_hooks(myguard_hooks,
ARRAY_SIZE(myguard_hooks),
&myguard_lsmid);
return 0;
}
/* LSM 등록 매크로 */
DEFINE_LSM(myguard) = {
.name = "myguard",
.init = myguard_init,
};
Kconfig 및 Makefile
# security/myguard/Kconfig
config SECURITY_MYGUARD
bool "MyGuard LSM"
depends on SECURITY
default n
help
Custom LSM that blocks execution from /tmp
and audits access to sensitive files.
# security/myguard/Makefile
obj-$(CONFIG_SECURITY_MYGUARD) += myguard_lsm.o
# security/Makefile 에 추가
subdir-$(CONFIG_SECURITY_MYGUARD) += myguard
# security/Kconfig 에 추가
source "security/myguard/Kconfig"
# 커널 빌드 설정
make menuconfig
# Security options → MyGuard LSM [*]
# 부트 파라미터에 추가
# lsm=lockdown,capability,landlock,yama,myguard,apparmor
# 빌드 후 확인
cat /sys/kernel/security/lsm
# lockdown,capability,landlock,yama,myguard,apparmor
# 로그 확인
dmesg | grep myguard
보안 모듈 비교
SELinux vs AppArmor vs SMACK vs Landlock
| 특성 | SELinux | AppArmor | SMACK | Landlock |
|---|---|---|---|---|
| 접근 제어 모델 | TE + RBAC + MLS | 경로 기반 프로파일 | 레이블 기반 규칙 | 비특권 샌드박싱 |
| 식별 방식 | inode 레이블 (xattr) | 파일시스템 경로 | 객체 레이블 (xattr) | 파일 디스크립터 기반 |
| 정책 복잡도 | 매우 높음 | 중간 | 낮음 | 낮음 |
| root 권한 필요 | 관리에 필요 | 관리에 필요 | 관리에 필요 | 불필요 (비특권) |
| 배포판 기본 정책 | 배포판/버전별 상이 (최신 상태는 참고자료 참조) | |||
| LSM 분류 | Major | Major | Major | Minor (스택 가능) |
| 네트워크 제어 | 포트/소켓 레이블 | 네트워크 규칙 | IP 레이블 (CIPSO) | TCP 포트 (6.4+) |
| 컨테이너 적합성 | 높음 (MCS 기반 격리) | 높음 (프로파일 기반) | 중간 | 높음 (자체 샌드박스) |
| 런타임 정책 변경 | semodule, setsebool | apparmor_parser -r | /sys/fs/smackfs/ | 불가 (비가역적) |
| 코드 규모 (LoC) | ~30,000+ | ~15,000 | ~5,000 | ~3,000 |
보안 모듈 선택 가이드
- 엔터프라이즈 서버 (RHEL 계열) — SELinux: 가장 포괄적인 MAC, 정책 생태계 성숙, Common Criteria 인증
- 데스크톱/클라우드 (Ubuntu 계열) — AppArmor: 상대적으로 쉬운 프로파일 관리, 충분한 보안 수준
- IoT/임베디드 — SMACK: 최소 오버헤드, 단순한 레이블 모델, 제한된 리소스에 적합
- 애플리케이션 샌드박싱 — Landlock + Seccomp: 비특권 사용자도 적용 가능, 컨테이너 내부에서도 사용
- 동적 보안 정책 — BPF LSM: 재부팅 없이 정책 적용/제거, 관측성(observability)과 결합
실전 조합 전략
현대 Linux 시스템은 여러 보안 메커니즘을 조합하여 사용합니다. 예를 들어, Docker 컨테이너는 다음과 같은 다층 보안을 적용합니다:
# Docker 컨테이너의 보안 스택 예시
# 1. User Namespace — UID 매핑으로 격리
# 2. Seccomp — 약 50개 위험 시스콜 차단 (기본 프로파일)
# 3. AppArmor/SELinux — MAC 프로파일 적용
# 4. Capabilities — 불필요한 capability 제거
# 5. Landlock — 추가적 파일시스템 샌드박싱 (애플리케이션 레벨)
# Docker 보안 옵션 확인
docker inspect --format='{{.HostConfig.SecurityOpt}}' mycontainer
# [seccomp=default apparmor=docker-default]
# 컨테이너 capability 확인
docker inspect --format='{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' mycontainer
# 컨테이너 기본 capability (제한된 세트)
# CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW,
# SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE,
# SYS_CHROOT, KILL, AUDIT_WRITE
# 모든 capability 제거 후 필요한 것만 추가
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
보안 모듈 성능 영향
| 메커니즘 | 오버헤드 | 주요 비용 원인 |
|---|---|---|
| DAC (기본 퍼미션) | < 1% | uid/gid 비교 (매우 가벼움) |
| Capabilities | < 1% | 비트마스크 연산 |
| SELinux | 1~5% | AVC 캐시 미스 시 정책 엔진 질의 |
| AppArmor | 1~3% | 경로 해석, 프로파일 매칭 |
| SMACK | < 1% | 레이블 비교 (매우 가벼움) |
| Landlock | < 1% | 규칙 세트 크기에 비례 |
| Seccomp-BPF | < 1% | 시스콜 진입 시 BPF 프로그램 실행 |
| BPF LSM | 1~3% | BPF 프로그램 복잡도에 비례 |
avcstat 명령으로 캐시 히트율을 모니터링하세요. 히트율이 낮으면 정책 규칙이 과도하게 세분화된 것일 수 있습니다.
LSM 훅 체인
LSM 프레임워크의 핵심은 훅 체인(hook chain) 메커니즘입니다. 시스템 콜이 보안 관련 리소스에 접근할 때, 커널은 해당 작업에 대응하는 LSM 훅 함수를 호출합니다. 여러 보안 모듈이 동시에 활성화된 환경(스택킹)에서는 각 모듈의 콜백이 등록 순서대로 연쇄 호출됩니다.
훅 체인 실행 메커니즘
security_hook_heads 구조체는 각 훅 포인트별 hlist_head를 보유합니다. 보안 모듈이 초기화될 때 security_add_hooks()를 통해 자신의 콜백을 등록하면, 해당 리스트에 추가됩니다. 훅 호출 시 call_int_hook() / call_void_hook() 매크로가 리스트를 순회하며 각 콜백을 실행합니다.
/* 개념 예시: security_file_open() 호출 경로 (security/security.c) */
int security_file_open(struct file *file)
{
int ret;
/* call_int_hook 매크로: 등록된 모든 LSM 콜백을 순회 */
ret = call_int_hook(file_open, 0, file);
if (ret)
return ret; /* 하나라도 거부하면 즉시 반환 */
return fsnotify_perm(file, MAY_OPEN);
}
/* call_int_hook 매크로 확장 (간소화) */
#define call_int_hook(FUNC, IRC, ...) ({ \
int RC = IRC; \
struct security_hook_list *P; \
hlist_for_each_entry(P, \
&security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != 0) \
break; /* 거부 시 중단 */ \
} \
RC; \
})
call_int_hook()는 체인 내 하나의 모듈이라도 0이 아닌 값(보통 -EACCES 또는 -EPERM)을 반환하면 즉시 반복을 중단하고 해당 에러를 반환합니다. 이는 "가장 제한적인 정책이 우선"하는 설계입니다. 즉, SELinux가 허용해도 AppArmor가 거부하면 접근이 차단됩니다.
다중 LSM 스택킹
리눅스 커널 6.x에서는 LSM 스택킹이 안정화되어 여러 Major LSM을 동시에 활성화할 수 있습니다. 스택킹 순서는 부트 파라미터 lsm=으로 결정됩니다.
# LSM 스택 순서 확인
cat /sys/kernel/security/lsm
# lockdown,capability,landlock,yama,apparmor,selinux,bpf
# 부트 파라미터로 순서 지정
# GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,selinux,apparmor,bpf"
# 활성화된 LSM 모듈 상세 정보
cat /sys/kernel/security/lsm
ls /sys/kernel/security/
| LSM 분류 | 스택킹 동작 | 예시 모듈 |
|---|---|---|
| Minor LSM | 항상 스택 가능 (독립 동작) | Lockdown, Yama, LoadPin, SafeSetID |
| Major LSM (커널 6.x+) | 동시 활성화 가능 (스택킹 지원) | SELinux, AppArmor, SMACK |
| 특수 LSM | 독립 슬롯, 항상 활성 | Capability, Landlock |
| BPF LSM | 동적 attach, 다른 LSM과 공존 | bpf |
훅 체인 내부 구조
/* 개념 예시: security_hook_heads — 모든 훅 포인트의 리스트 헤드 */
struct security_hook_heads {
struct hlist_head binder_set_context_mgr;
struct hlist_head binder_transaction;
/* ... */
struct hlist_head file_permission;
struct hlist_head file_open; /* ← security_file_open()이 참조 */
struct hlist_head file_alloc_security;
/* ... 약 230개 훅 포인트 */
struct hlist_head socket_create;
struct hlist_head socket_connect;
} __randomize_layout;
/* 개념 예시: security_hook_list — 개별 콜백 등록 항목 */
struct security_hook_list {
struct hlist_node list; /* 체인 연결용 */
struct hlist_head *head; /* 소속 훅 포인트 */
union security_list_options hook; /* 실제 콜백 함수 포인터 */
const struct lsm_id *lsmid; /* 소속 LSM 식별자 */
};
call_void_hook()은 반환값이 없는 알림 훅(예: file_free_security)에 사용됩니다. 이 경우 모든 모듈의 콜백이 무조건 호출되며 중단 없이 리스트를 끝까지 순회합니다.
SELinux vs AppArmor vs Landlock 심화 비교
세 보안 모듈은 동일한 LSM 훅을 통해 동작하지만, 정책 모델과 식별 방식이 근본적으로 다릅니다. 각 모듈의 보안 결정 경로를 비교하면 아키텍처적 트레이드오프를 명확히 이해할 수 있습니다.
정책 모델 비교
| 관점 | SELinux | AppArmor | Landlock |
|---|---|---|---|
| 객체 식별 | inode 레이블 (security.selinux xattr) |
파일시스템 경로 (pathname) | 파일 디스크립터 (fd → inode) |
| 정책 언어 | Type Enforcement + RBAC + MLS/MCS | 프로파일 규칙 (경로 + 퍼미션) | C API (ruleset + rule) |
| 정책 저장소 | 바이너리 정책 DB (/etc/selinux/) |
텍스트 프로파일 (/etc/apparmor.d/) |
프로세스 메모리 (런타임 전용) |
| 하드링크/마운트 | 레이블 불변 → 안전 | 경로 변경 시 정책 우회 가능 | fd 기반 → 경로 무관 |
| 네임스페이스 인식 | MCS로 컨테이너 격리 | 스택 프로파일로 격리 | 자동 (비특권 설계) |
| 정책 변경 | semodule -i, setsebool |
apparmor_parser -r |
불가 (비가역적, 추가만 가능) |
| 학습 모드 | Permissive 모드 + audit2allow |
Complain 모드 + aa-logprof |
없음 (설계에 불필요) |
SELinux Type Enforcement 결정 경로
/* 개념 예시: SELinux 접근 결정 흐름 */
/* 1. 접근 요청 발생: 프로세스(subject) → 파일(object) */
/* subject: httpd_t (웹 서버 프로세스 타입) */
/* object: httpd_sys_content_t (웹 콘텐츠 파일 타입) */
/* action: file:read */
/* 2. AVC 캐시 조회 (빠른 경로) */
int avc_has_perm(struct selinux_state *state,
u32 ssid, /* source SID: httpd_t */
u32 tsid, /* target SID: httpd_sys_content_t */
u16 tclass, /* target class: file */
u32 requested) /* 요청 권한: read */
{
struct av_decision avd;
int rc;
/* 3. 캐시 히트 → 즉시 결정 (일반적인 경우) */
rc = avc_has_perm_noaudit(state, ssid, tsid,
tclass, requested, 0, &avd);
/* 4. 캐시 미스 → 정책 DB 질의 (느린 경로) */
/* security_compute_av() → TE 규칙 탐색 */
/* 5. 감사 로그 기록 */
avc_audit(state, ssid, tsid, tclass,
requested, &avd, rc, ...);
return rc;
}
# SELinux 정책 규칙 예시: httpd가 웹 콘텐츠를 읽도록 허용
# allow source_type target_type : target_class permissions;
allow httpd_t httpd_sys_content_t : file { read open getattr };
allow httpd_t httpd_sys_content_t : dir { search getattr };
# 현재 프로세스의 SELinux 컨텍스트 확인
ps auxZ | grep httpd
# system_u:system_r:httpd_t:s0 /usr/sbin/httpd
# 파일의 SELinux 레이블 확인
ls -Z /var/www/html/index.html
# system_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
AppArmor 경로 기반 결정 경로
# AppArmor 프로파일 예시: /usr/sbin/nginx
# /etc/apparmor.d/usr.sbin.nginx
profile nginx /usr/sbin/nginx {
# 기본 실행 파일 접근
/usr/sbin/nginx mr,
# 웹 콘텐츠 읽기
/var/www/html/** r,
/var/www/html/ r,
# 로그 쓰기
/var/log/nginx/*.log w,
# PID 파일
/run/nginx.pid rw,
# 네트워크 접근
network inet stream,
network inet6 stream,
# 명시적으로 나열되지 않은 경로는 모두 거부
}
# 프로파일 로드/리로드
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
# 프로파일 상태 확인
aa-status
# 1 profiles are in enforce mode.
# /usr/sbin/nginx
Landlock 파일 디스크립터 기반 모델
/* 실습 예제: Landlock 자발적 샌드박싱 */
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <fcntl.h>
int sandbox_readonly(const char *path)
{
struct landlock_ruleset_attr attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE | /* 처리 대상에 포함 */
LANDLOCK_ACCESS_FS_REMOVE_FILE,
};
/* 1. Ruleset 생성 */
int ruleset_fd = syscall(SYS_landlock_create_ruleset,
&attr, sizeof(attr), 0);
/* 2. 읽기 전용 규칙 추가 (쓰기/삭제는 포함하지 않음) */
int path_fd = open(path, O_PATH | O_CLOEXEC);
struct landlock_path_beneath_attr rule = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.parent_fd = path_fd,
};
syscall(SYS_landlock_add_rule, ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
/* 3. 비가역적 적용 — 이후 권한 확대 불가 */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
syscall(SYS_landlock_restrict_self, ruleset_fd, 0);
close(path_fd);
close(ruleset_fd);
return 0;
/* 이 시점 이후 path 아래만 읽기 가능, 쓰기/삭제 차단 */
}
bind mount나 hardlink를 통한 경로 변경에 취약할 수 있습니다. SELinux의 inode 레이블은 파일이 이동/링크되어도 유지되지만, 새 파일 생성 시 올바른 레이블 부여(restorecon)가 필요합니다. Landlock은 fd 기반이라 두 문제 모두 회피합니다.
BPF LSM 심화
앞 절에서 BPF LSM의 기본 사용법을 다뤘습니다. 이 절에서는 Cilium Tetragon 등 실제 프로덕션 도구의 BPF LSM 활용, 동적 정책 적용의 아키텍처, 그리고 다중 BPF 프로그램 관리를 심화 분석합니다.
BPF LSM 내부 아키텍처
BPF LSM은 BPF trampoline을 활용하여 기존 LSM 훅에 동적으로 attach됩니다. 커널 JIT 컴파일러가 트램폴린 코드를 생성하여 BPF 프로그램을 호출하므로, 오버헤드는 간접 함수 호출 수준으로 최소화됩니다.
/* 개념 예시: BPF LSM attach 내부 흐름 */
/* 1. 사용자 공간: bpf(BPF_LINK_CREATE) 시스템 콜 */
/* → attach_type = BPF_LSM_MAC */
/* → target: security_file_open */
/* 2. 커널: bpf_trampoline_link_prog() */
/* → 기존 security_file_open() 진입점에 트램폴린 삽입 */
/* → JIT가 네이티브 코드 생성 (x86_64 call 명령어) */
/* 3. 호출 시점: VFS → security_file_open() */
/* → 트램폴린 → BPF 프로그램 실행 */
/* → 반환값이 0이 아니면 접근 거부 */
/* BPF LSM 프로그램 예시: 특정 UID의 파일 실행 차단 */
SEC("lsm/bprm_check_security")
int BPF_PROG(deny_exec_by_uid,
struct linux_binprm *bprm)
{
__u32 uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
/* UID 1000 사용자의 /tmp 아래 실행 차단 */
if (uid == 1000) {
char path[256];
bpf_d_path(&bprm->file->f_path, path, sizeof(path));
/* /tmp 접두사 확인 */
if (path[0] == '/' && path[1] == 't' &&
path[2] == 'm' && path[3] == 'p' &&
path[4] == '/')
return -EPERM;
}
return 0; /* 허용 */
}
char LICENSE[] SEC("license") = "GPL";
Cilium Tetragon과 BPF LSM
Cilium Tetragon은 eBPF 기반 보안 관측성(observability) 및 런타임 강제(enforcement) 도구입니다. BPF LSM을 활용하여 Kubernetes 환경에서 동적으로 보안 정책을 적용합니다.
# Tetragon TracingPolicy 예시: 민감 파일 접근 모니터링 + 차단
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: block-sensitive-files
spec:
kprobes:
- call: "security_file_open"
syscall: false
args:
- index: 0
type: "file"
selectors:
- matchArgs:
- index: 0
operator: "Prefix"
values:
- "/etc/shadow"
- "/etc/gshadow"
matchActions:
- action: Sigkill # 접근 시 프로세스 강제 종료
- action: Post # 이벤트 기록
# Tetragon 설치 (Kubernetes)
helm install tetragon cilium/tetragon -n kube-system
# 정책 적용
kubectl apply -f block-sensitive-files.yaml
# 실시간 이벤트 관찰
kubectl exec -n kube-system ds/tetragon -c tetragon \
-- tetra getevents -o compact
# 출력 예시:
# 🔴 exit default/nginx-pod /bin/cat /etc/shadow SIGKILL
# process blocked by tracing policy "block-sensitive-files"
다중 BPF LSM 프로그램 관리
동일한 훅 포인트에 여러 BPF LSM 프로그램을 attach할 수 있습니다. 이 경우 모든 프로그램이 순서대로 실행되며, 하나라도 거부하면 최종 결정은 거부입니다.
# 현재 attach된 BPF LSM 프로그램 목록
bpftool prog list | grep -A3 lsm
# 42: lsm name deny_unlink tag abc123 gpl
# 43: lsm name audit_open tag def456 gpl
# 특정 프로그램의 attach 정보
bpftool link list
# 7: lsm prog 42 attach_type lsm_mac
# target: security_inode_unlink
# BPF LSM 프로그램 detach (링크 ID로 제거)
bpftool link detach id 7
# BPF 맵을 통한 동적 설정 변경 (프로그램 재로드 없이)
bpftool map update name config_map \
key 0x00 0x00 0x00 0x00 \
value 0x01 0x00 0x00 0x00 # enforcement 활성화
CAP_BPF + CAP_SYS_ADMIN이 필요하며, BPF verifier의 안전성 검증을 통과해야 합니다. 루프 횟수 제한, 메모리 접근 범위 검증, 스택 크기 제한(512바이트) 등의 제약이 있습니다. 또한 sleepable BPF 프로그램(BPF_F_SLEEPABLE)을 사용하면 일부 helper 함수 호출이 제한됩니다.
LSM 훅 추적과 디버깅
LSM 정책 위반 디버깅은 보안 운영의 핵심입니다. ftrace, bpftrace, 그리고 감사 로그(auditd)를 활용하면 어떤 LSM 모듈이 어떤 요청을 거부했는지 실시간으로 추적할 수 있습니다.
ftrace로 LSM 훅 추적
# 1. 사용 가능한 LSM 관련 함수 목록 확인
cat /sys/kernel/tracing/available_filter_functions | grep ^security_
# security_file_open
# security_inode_permission
# security_inode_unlink
# security_bprm_check
# security_socket_connect
# ... (약 230개)
# 2. security_file_open 호출 추적 설정
echo 0 > /sys/kernel/tracing/tracing_on
echo function_graph > /sys/kernel/tracing/current_tracer
echo security_file_open > /sys/kernel/tracing/set_graph_function
echo 3 > /sys/kernel/tracing/max_graph_depth
# 3. 추적 시작
echo 1 > /sys/kernel/tracing/tracing_on
# 4. 다른 터미널에서 파일 열기 시도
cat /etc/passwd
# 5. 추적 결과 확인
cat /sys/kernel/tracing/trace
# 0) | security_file_open() {
# 0) 0.234 us | selinux_file_open();
# 0) 0.156 us | apparmor_file_open();
# 0) 0.891 us | }
# 6. 추적 중지 및 정리
echo 0 > /sys/kernel/tracing/tracing_on
echo nop > /sys/kernel/tracing/current_tracer
bpftrace로 정책 위반 분석
# bpftrace: security_file_open 거부 추적
# 반환값이 0이 아닌 경우 (거부)만 출력
bpftrace -e '
kretprobe:security_file_open
/retval != 0/
{
printf("DENIED: pid=%d comm=%s retval=%d\n",
pid, comm, retval);
printf(" stack:\n%s\n", kstack);
}'
# 출력 예시:
# DENIED: pid=1234 comm=cat retval=-13
# stack:
# security_file_open+0x45
# do_filp_open+0x78
# do_sys_openat2+0x9c
# bpftrace: SELinux AVC 거부 빈도 분석
bpftrace -e '
kprobe:avc_denied
{
@deny_count[comm] = count();
}
interval:s:10
{
printf("\n--- SELinux AVC 거부 통계 (10초) ---\n");
print(@deny_count);
clear(@deny_count);
}'
# 출력 예시:
# --- SELinux AVC 거부 통계 (10초) ---
# @deny_count[httpd]: 42
# @deny_count[postfix]: 3
감사 로그 분석
# SELinux AVC 거부 로그 분석
ausearch -m avc -ts recent
# type=AVC msg=audit(1709913600.123:456): avc: denied { read }
# for pid=1234 comm="httpd" name="secret.conf"
# scontext=system_u:system_r:httpd_t:s0
# tcontext=system_u:object_r:admin_home_t:s0
# tclass=file permissive=0
# 거부 로그에서 허용 정책 자동 생성
ausearch -m avc -ts recent | audit2allow -M my_httpd_fix
# *** Generated policy module: my_httpd_fix ***
# 내용 확인 후 적용:
semodule -i my_httpd_fix.pp
# AppArmor 거부 로그 분석
journalctl -k | grep apparmor.*DENIED
# apparmor="DENIED" operation="open"
# profile="/usr/sbin/nginx"
# name="/etc/shadow" pid=5678
# comm="nginx" requested_mask="r"
# denied_mask="r" fsuid=0 ouid=0
# AppArmor 거부 로그에서 프로파일 업데이트
aa-logprof
# 대화형으로 거부된 접근에 대해 허용/거부 결정
LSM 정책 디버깅 워크플로
| 단계 | 도구 | 목적 |
|---|---|---|
| 1. 증상 확인 | dmesg, journalctl | 거부 메시지 발견 |
| 2. 감사 분석 | ausearch, aa-status | 어떤 모듈의 어떤 규칙이 거부했는지 식별 |
| 3. 실시간 추적 | ftrace, bpftrace | 거부 빈도, 호출 경로, 스택 트레이스 수집 |
| 4. 정책 수정 | audit2allow, aa-logprof | 최소 권한 원칙에 맞는 정책 생성 |
| 5. 검증 | Permissive/Complain 모드 | 거부 없이 감사 로그만 수집하여 정책 검증 |
| 6. 적용 | semodule, apparmor_parser | 검증된 정책을 Enforcing 모드로 적용 |
setenforce 0)는 시스템 전체에 적용됩니다. 특정 도메인만 Permissive로 전환하려면 semanage permissive -a httpd_t를 사용하세요. 프로덕션 환경에서는 도메인 단위 Permissive가 더 안전합니다.
LSM 성능 분석
LSM 보안 모듈은 시스템 콜 경로에 추가 검사를 삽입하므로 불가피한 오버헤드가 발생합니다. 그러나 각 모듈의 구현 방식에 따라 오버헤드 특성이 크게 다릅니다. 이 절에서는 정량적 비교와 벤치마크 방법론을 다룹니다.
훅 실행 비용 분석
| 보안 모듈 | 훅당 평균 지연 | 주요 비용 원인 | 캐시 메커니즘 | 워크로드 영향 |
|---|---|---|---|---|
| DAC | 50~100 ns | uid/gid 비교, ACL 조회 | dentry 캐시 | 무시 가능 (< 0.5%) |
| Capabilities | 30~80 ns | 비트마스크 AND 연산 | cred 구조체 직접 참조 | 무시 가능 (< 0.3%) |
| SELinux | 200~800 ns | AVC 캐시 미스 시 정책 DB 질의 | AVC (512 슬롯 기본) | I/O 집약: 1~3%, CPU 집약: 3~5% |
| AppArmor | 150~500 ns | 경로 문자열 생성 + DFA 매칭 | dentry 기반 (경로 재생성 필요) | 파일 집약: 1~3%, 네트워크: < 1% |
| SMACK | 50~150 ns | 레이블 문자열 비교 | 레이블 해시 테이블 | 무시 가능 (< 1%) |
| Landlock | 100~300 ns | ruleset 트리 순회 | 없음 (규칙 수에 비례) | 파일 접근: < 1% |
| Seccomp-BPF | 30~200 ns | cBPF 프로그램 실행 | 비트맵 필터 (커널 5.0+) | 시스콜 빈도에 비례 (< 1%) |
| BPF LSM | 100~2000 ns | eBPF JIT 코드 실행, 맵 조회 | BPF 맵 (사용자 정의) | 프로그램 복잡도에 비례 (1~5%) |
벤치마크 방법론
# 1. 기준선 측정: LSM 비활성화 상태
# 부트 파라미터: lsm=lockdown,capability (최소 LSM만 활성)
# 2. 파일 I/O 벤치마크 (fio)
fio --name=lsm-bench --ioengine=sync --rw=randread \
--bs=4k --numjobs=4 --size=1G \
--runtime=60 --time_based --group_reporting
# 결과: IOPS, latency (p50/p99), bandwidth
# 3. 시스콜 오버헤드 측정 (syscall-specific)
# perf로 security_file_open 함수의 실행 시간 측정
perf stat -e 'duration_time' -a -- \
bpftrace -e '
kprobe:security_file_open { @start[tid] = nsecs; }
kretprobe:security_file_open /@start[tid]/ {
@latency = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:30 { exit(); }
'
# 4. SELinux AVC 캐시 효율 측정
cat /sys/fs/selinux/avc/cache_stats
# lookups hits misses allocations reclaims frees
# 1234567 1234000 567 567 0 0
# 히트율: 1234000/1234567 = 99.95% (양호)
# 5. 네트워크 워크로드 벤치마크
# iperf3으로 TCP 처리량 비교
iperf3 -c server -t 60 -P 8
# LSM 활성/비활성 시 처리량 비교
성능 최적화 전략
| 최적화 | 적용 대상 | 방법 | 기대 효과 |
|---|---|---|---|
| AVC 캐시 확장 | SELinux | avc_cache_threshold=1024 부트 파라미터 |
캐시 미스 감소 → 정책 DB 질의 회피 |
| 정책 간소화 | SELinux | 불필요한 dontaudit 규칙 제거, 타입 통합 | 정책 DB 크기 감소 → 질의 속도 향상 |
| 프로파일 최적화 | AppArmor | 와일드카드 규칙 최소화, 정확한 경로 지정 | DFA 상태 수 감소 → 매칭 속도 향상 |
| 규칙 수 제한 | Landlock | 계층적 규칙 활용, 개별 파일 규칙 회피 | ruleset 순회 비용 감소 |
| BPF 맵 최적화 | BPF LSM | 해시 맵 대신 per-CPU 배열, 맵 크기 최소화 | 캐시 라인 경합 감소, 조회 속도 향상 |
| Seccomp 비트맵 | Seccomp-BPF | 커널 5.0+ 비트맵 필터 자동 적용 | 빈번한 시스콜의 빠른 허용 판단 |
perf top으로 security_* 함수의 CPU 점유율을 먼저 확인하세요.
관련 문서
LSM/Seccomp와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.