LSM / Seccomp 심화

Linux Security Modules 프레임워크, SELinux, AppArmor, SMACK, Landlock, Seccomp-BPF, Capabilities, BPF LSM, 커스텀 LSM 모듈 작성까지 포괄하는 심화 문서입니다.

관련 표준: ISO/IEC 15408 (Common Criteria), POSIX.1-2017 (Capabilities, DAC), Bell-LaPadula / Biba 모델 (MLS/MCS) — LSM 프레임워크가 참조하는 보안 모델 및 평가 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

1. 리눅스 보안 모델 개요

Linux 커널의 보안은 단일 메커니즘이 아닌 다층 방어(Defense in Depth) 전략으로 구성됩니다. 각 계층이 독립적으로 동작하며, 하나의 계층이 우회되더라도 나머지 계층이 보호를 제공합니다.

DAC vs MAC

특성DAC (Discretionary)MAC (Mandatory)
정책 결정 주체자원 소유자 (사용자)시스템 관리자 (정책)
핵심 메커니즘uid/gid, rwx 퍼미션, ACL보안 레이블, 정책 규칙
root 우회root는 모든 제한 우회 가능root도 정책에 종속
유연성높음 (사용자 재량)낮음 (정책 고정)
보안 수준중간 — 사용자 실수에 취약높음 — 정책 기반 강제
대표 구현UNIX 퍼미션, POSIX ACLSELinux, AppArmor, SMACK

보안 계층 구조

Linux 보안 다층 방어 모델 사용자 공간: 프로세스 → 시스템 콜 진입 Seccomp-BPF 필터 (시스템 콜 허용/거부) DAC: uid/gid 퍼미션 + POSIX ACL Capabilities: CAP_* 비트마스크 (root 권한 세분화) LSM (MAC): SELinux / AppArmor / SMACK / Landlock security_hook_heads → 모든 LSM 훅 AND 로직

시스템 콜이 커널에 진입하면 다음 순서로 보안 검사를 통과해야 합니다:

  1. Seccomp-BPF — 시스템 콜 자체의 허용/거부 (가장 먼저 평가)
  2. DAC — 전통적 UNIX 퍼미션 검사 (uid/gid/mode)
  3. Capabilities — 세분화된 권한 비트 검사
  4. LSM 훅 — MAC 정책 기반 최종 접근 제어
Defense in Depth: 각 보안 계층은 독립적입니다. Seccomp이 시스템 콜을 허용하더라도 DAC에서 거부될 수 있고, DAC를 통과하더라도 LSM 정책이 차단할 수 있습니다. 이 다층 구조가 Linux 커널 보안의 핵심입니다.

2. 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;                                                 \
})
AND 로직: call_int_hook은 등록된 모든 LSM 훅을 순차 호출합니다. 하나라도 비-0(거부)을 반환하면 즉시 중단하고 거부를 반환합니다. 모든 LSM이 0(허용)을 반환해야 최종 허용됩니다.

LSM 스태킹

커널 6.x부터 여러 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_securityexecve() 시 실행 파일 검증
태스크 (task)task_alloc, task_kill, task_setscheduler프로세스 생성, 시그널, 스케줄링
파일 (file)file_permission, file_open, mmap_file파일 접근, mmap 시
inodeinode_permission, inode_create, inode_unlinkinode 생성/삭제/접근
소켓 (socket)socket_create, socket_connect, socket_bind네트워크 소켓 연산
키 (key)key_alloc, key_permission키링 접근
IPCmsg_queue_msgsnd, shm_shmatSysV IPC 접근
마운트sb_mount, sb_umount, sb_kern_mount파일시스템 마운트

3. SELinux

SELinux(Security-Enhanced Linux)는 NSA가 개발한 MAC 시스템으로, Type Enforcement(TE), RBAC(Role-Based Access Control), MLS/MCS(Multi-Level/Multi-Category Security) 모델을 결합한 가장 포괄적인 리눅스 보안 프레임워크입니다. RHEL, Fedora, CentOS에서 기본 활성화됩니다.

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 정책 섹션을 참고하라.

4. AppArmor

AppArmor는 Ubuntu, SUSE에서 기본 활성화되는 MAC 시스템으로, 경로 기반(path-based) 접근 제어를 사용합니다. 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 레이블이므로 우회 어려움
기본 배포판Ubuntu, SUSERHEL, Fedora, CentOS

5. 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);
}

6. Landlock

Landlock는 커널 5.13+에 도입된 비특권 샌드박싱 메커니즘입니다. root 권한 없이도 프로세스가 자발적으로 자신의 접근 권한을 제한할 수 있어, 애플리케이션 수준의 최소 권한 원칙을 구현합니다. 파일시스템(5.13+)과 네트워크(6.4+) 접근 제한을 지원합니다.

Landlock API

#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.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 v1(5.13): 파일시스템 기본, v2(5.19): REFER, v3(6.2): TRUNCATE */
/* ABI v4(6.4): 네트워크, v5(6.7): IOCTL_DEV */

/* 커널이 지원하지 않는 플래그 제거 */
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 */
}

7. 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_inheritableexecve() 시 자식에게 전달 가능한 세트
Boundingcap_bndexecve() 시 permitted에 추가 가능한 상한선
Ambientcap_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

8. 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>

/* strict mode 활성화 — read/write/_exit/sigreturn만 허용 */
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
/* 또는 */
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>

/* 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);
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];          /* 시스템 콜 인자 */
};

9. Seccomp-BPF 실전

실무에서는 raw BPF 명령어 대신 libseccomp 라이브러리를 사용하여 가독성 높은 필터를 작성합니다.

libseccomp 사용

#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>

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);
}

10. 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_opensecurity_file_open파일 열기 제어
lsm/inode_permissionsecurity_inode_permissioninode 접근 제어
lsm/inode_unlinksecurity_inode_unlink파일 삭제 제어
lsm/bprm_check_securitysecurity_bprm_check프로그램 실행 제어
lsm/socket_connectsecurity_socket_connect네트워크 연결 제어
lsm/task_allocsecurity_task_alloc프로세스 생성 제어
BPF LSM vs Landlock: BPF LSM은 CAP_BPF + CAP_SYS_ADMIN이 필요한 관리자용 도구입니다. Landlock는 비특권 사용자도 사용할 수 있는 자발적 샌드박싱입니다. 둘은 상호 보완적이며 동시 사용이 가능합니다.

11. 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 vs cred: real_cred는 태스크가 "대상"으로서 평가될 때(예: 시그널 수신 시) 사용되고, cred는 태스크가 "주체"로서 행동할 때(예: 파일 열기) 사용됩니다. 대부분의 경우 두 포인터는 같은 struct cred를 가리킵니다.

12. 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
주의: LSM은 커널에 정적으로 빌드되어야 합니다. 로드 가능 커널 모듈(LKM)로는 LSM을 등록할 수 없습니다. 이는 보안상의 이유로 의도된 설계입니다 — 런타임에 보안 정책 모듈을 교체하는 것은 보안 우회 벡터가 될 수 있기 때문입니다.

13. 보안 모듈 비교

SELinux vs AppArmor vs SMACK vs Landlock

특성SELinuxAppArmorSMACKLandlock
접근 제어 모델 TE + RBAC + MLS 경로 기반 프로파일 레이블 기반 규칙 비특권 샌드박싱
식별 방식 inode 레이블 (xattr) 파일시스템 경로 객체 레이블 (xattr) 파일 디스크립터 기반
정책 복잡도 매우 높음 중간 낮음 낮음
root 권한 필요 관리에 필요 관리에 필요 관리에 필요 불필요 (비특권)
기본 배포판 RHEL, Fedora Ubuntu, SUSE Tizen, AGL 커널 5.13+ 범용
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%비트마스크 연산
SELinux1~5%AVC 캐시 미스 시 정책 엔진 질의
AppArmor1~3%경로 해석, 프로파일 매칭
SMACK< 1%레이블 비교 (매우 가벼움)
Landlock< 1%규칙 세트 크기에 비례
Seccomp-BPF< 1%시스콜 진입 시 BPF 프로그램 실행
BPF LSM1~3%BPF 프로그램 복잡도에 비례
성능 팁: SELinux의 AVC(Access Vector Cache)는 접근 결정을 캐싱하여 반복 질의 시 오버헤드를 최소화합니다. avcstat 명령으로 캐시 히트율을 모니터링하세요. 히트율이 낮으면 정책 규칙이 과도하게 세분화된 것일 수 있습니다.