네임스페이스 (Namespaces)
Linux 커널 네임스페이스: PID, mount, network, user 네임스페이스 격리 기술.
네임스페이스 개요
네임스페이스는 커널 리소스를 격리하여 프로세스 그룹이 독립적인 시스템 뷰를 갖게 하는 메커니즘입니다. 컨테이너(Docker, LXC)의 핵심 기반 기술입니다.
네임스페이스 격리 원리
네임스페이스의 핵심 원리는 "같은 커널 위에서 서로 다른 시스템처럼 보이게 만든다"는 것입니다. 운영체제 수준의 가상화가 아니라, 커널 자원에 대한 뷰(view)를 분리하는 것입니다.
커널 구현: nsproxy 구조체
모든 프로세스(task_struct)는 nsproxy 포인터를 통해 자신이 속한 네임스페이스 집합을 참조합니다. 같은 네임스페이스를 공유하는 프로세스들은 동일한 nsproxy를 가리킵니다:
/* include/linux/nsproxy.h */
struct nsproxy {
refcount_t count; /* 참조 카운트 */
struct uts_namespace *uts_ns; /* 호스트명 */
struct ipc_namespace *ipc_ns; /* IPC 객체 */
struct mnt_namespace *mnt_ns; /* 마운트 포인트 */
struct pid_namespace *pid_ns_for_children; /* 자식의 PID NS */
struct net *net_ns; /* 네트워크 스택 */
struct time_namespace *time_ns; /* 시간 오프셋 */
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns; /* cgroup 뷰 */
};
/* task_struct에서 네임스페이스 접근 */
struct task_struct {
/* ... */
struct nsproxy *nsproxy; /* 네임스페이스 프록시 */
/* user_ns는 cred에서 별도 관리 */
};
네임스페이스 격리의 동작 방식은 다음과 같습니다:
- 자원 조회 시 네임스페이스 참조: 커널의 모든 자원 조회 경로(PID 조회, 네트워크 소켓 생성, 마운트 탐색 등)에서 현재 프로세스의
nsproxy를 참조하여 해당 네임스페이스 범위 내에서만 자원을 검색합니다. - Copy-on-Write 생성:
clone()이나unshare()시 지정된 네임스페이스만 새로 복사하고, 나머지는 부모와 공유합니다. 이는 생성 비용을 최소화합니다. - 참조 카운팅으로 수명 관리: 네임스페이스는 참조 카운트가 0이 되면(마지막 프로세스 종료 + bind mount 없음) 자동으로 해제됩니다.
PID 번역 원리
PID 네임스페이스는 계층적 번역(hierarchical translation) 모델을 사용합니다. 하나의 프로세스가 여러 PID 네임스페이스에서 서로 다른 PID를 가질 수 있습니다:
/* 커널 내부: 프로세스는 레벨별로 PID를 가짐 */
struct pid {
refcount_t count;
unsigned int level; /* PID NS 깊이 */
struct upid numbers[]; /* 각 레벨별 PID 번호 */
};
struct upid {
int nr; /* 해당 NS에서의 PID 값 */
struct pid_namespace *ns; /* 소속 NS */
};
/* 예: 컨테이너 프로세스의 PID 매핑
* Host NS (level 0): PID 1234
* Container NS (level 1): PID 1
* → numbers[0] = {nr=1234, ns=host_ns}
* → numbers[1] = {nr=1, ns=container_ns}
*
* 자식 NS에서는 부모 NS의 PID를 볼 수 없지만,
* 부모 NS에서는 자식의 모든 프로세스를 볼 수 있음
*/
User 네임스페이스의 특수성: User NS는 nsproxy가 아닌 cred(자격 증명) 구조체에서 관리됩니다. 이는 UID/GID 매핑이 프로세스의 보안 컨텍스트에 직접 영향을 미치기 때문입니다. User NS는 다른 모든 네임스페이스의 "소유자(owner)"를 결정하며, 비특권 사용자도 User NS 내에서 다른 네임스페이스를 생성할 수 있게 합니다.
네임스페이스 유형
| 네임스페이스 | 격리 대상 | 시스템 콜 플래그 |
|---|---|---|
PID | 프로세스 ID | CLONE_NEWPID |
Mount | 마운트 포인트 | CLONE_NEWNS |
Network | 네트워크 스택 | CLONE_NEWNET |
User | UID/GID 매핑 | CLONE_NEWUSER |
UTS | 호스트명, 도메인명 | CLONE_NEWUTS |
IPC | System V IPC, POSIX MQ | CLONE_NEWIPC |
Cgroup | Cgroup 루트 디렉터리 | CLONE_NEWCGROUP |
Time | CLOCK_MONOTONIC 등 | CLONE_NEWTIME |
네임스페이스 API
#include <sched.h>
/* 새 네임스페이스로 프로세스 생성 */
clone(child_fn, stack, CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, arg);
/* 현재 프로세스를 새 네임스페이스로 이동 */
unshare(CLONE_NEWNS | CLONE_NEWPID);
/* 기존 네임스페이스에 진입 */
int fd = open("/proc/<pid>/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);
PID Namespace
PID 네임스페이스는 프로세스 ID를 격리합니다. 새 PID NS 내의 첫 프로세스는 PID 1이 되며, 부모 NS에서는 다른 PID로 보입니다.
Network Namespace
네트워크 네임스페이스는 네트워크 인터페이스, 라우팅 테이블, iptables 규칙, 소켓 등을 완전히 격리합니다. veth 페어로 네임스페이스 간 통신을 구성합니다.
# 네트워크 네임스페이스 생성
ip netns add myns
# veth 페어 생성 및 연결
ip link add veth0 type veth peer name veth1
ip link set veth1 netns myns
# 네임스페이스 내에서 명령 실행
ip netns exec myns ip addr add 10.0.0.2/24 dev veth1
ip netns exec myns ip link set veth1 up
lsns 명령으로 시스템의 모든 네임스페이스를 확인할 수 있습니다. /proc/[pid]/ns/ 디렉터리에서 프로세스가 속한 네임스페이스의 파일 디스크립터를 얻을 수 있습니다.
Mount Namespace
Mount namespace는 프로세스별로 독립적인 마운트 포인트 테이블을 제공합니다. 컨테이너가 호스트와 다른 파일시스템 뷰를 가질 수 있게 합니다. 내부적으로 각 Mount NS는 자체 struct mnt_namespace를 가지며, 이 안에 마운트 포인트의 트리 구조가 저장됩니다.
Mount Propagation 원리
Mount namespace 간의 마운트 이벤트 전파는 peer group 메커니즘으로 제어됩니다. 새 Mount NS를 생성하면 기본적으로 부모와 peer group을 형성하여 마운트 이벤트가 양방향으로 전파됩니다. propagation 유형에 따라 이 동작을 제어합니다:
/* 새 mount namespace에서 프로세스 생성 */
int flags = CLONE_NEWNS;
unshare(flags);
/* pivot_root: 컨테이너의 루트 파일시스템 변경 */
pivot_root("./newroot", "./newroot/oldroot");
umount2("/oldroot", MNT_DETACH);
# mount namespace 생성 및 확인
unshare --mount --propagation private /bin/bash
mount --bind /tmp/myroot /mnt
# 호스트에서는 /mnt가 변경되지 않음
# mount propagation 유형
mount --make-shared /mnt # 양방향 전파
mount --make-slave /mnt # 단방향 (호스트→컨테이너)
mount --make-private /mnt # 전파 없음
mount --make-unbindable /mnt # bind mount도 불가
User Namespace
User namespace는 UID/GID 매핑을 격리합니다. 컨테이너 내에서 root(UID 0)로 보이지만 호스트에서는 일반 사용자입니다.
/* user namespace 생성 */
unshare(CLONE_NEWUSER);
/* UID 매핑 설정: /proc/PID/uid_map */
/* 형식: container_uid host_uid range */
/* 예: "0 1000 1" → 컨테이너 UID 0 = 호스트 UID 1000 */
# unprivileged 컨테이너 생성
unshare --user --map-root-user /bin/bash
id # uid=0(root) gid=0(root) (컨테이너 내부에서)
# UID 매핑 확인
cat /proc/self/uid_map
# 0 1000 1
Cgroup Namespace
Cgroup namespace는 프로세스의 cgroup 뷰를 격리합니다. 컨테이너 내에서 /proc/self/cgroup이 루트(/)로 보입니다.
UTS와 IPC Namespace
# UTS namespace: 호스트네임 격리
unshare --uts /bin/bash
hostname container-host
hostname # container-host (호스트에는 영향 없음)
# IPC namespace: System V IPC, POSIX 메시지 큐 격리
unshare --ipc /bin/bash
ipcs # 빈 IPC 테이블 (호스트의 IPC 객체 안보임)
네임스페이스 생명주기와 관리
/* 네임스페이스 생성 방법 3가지 */
/* 1. clone() 시 플래그로 생성 */
clone(child_fn, stack, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, arg);
/* 2. unshare()로 현재 프로세스의 namespace 분리 */
unshare(CLONE_NEWNS | CLONE_NEWPID);
/* 3. setns()로 기존 namespace에 진입 */
int fd = open("/proc/PID/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);
close(fd);
# 프로세스의 네임스페이스 확인
ls -la /proc/self/ns/
# cgroup -> cgroup:[4026531835]
# ipc -> ipc:[4026531839]
# mnt -> mnt:[4026531841]
# net -> net:[4026531840]
# pid -> pid:[4026531836]
# user -> user:[4026531837]
# uts -> uts:[4026531838]
# 네임스페이스를 파일로 유지 (bind mount)
touch /run/netns/my_netns
mount --bind /proc/self/ns/net /run/netns/my_netns
# ip netns는 이 방식 사용
# nsenter: 기존 네임스페이스에 진입
nsenter --target $PID --net --pid --mount /bin/bash
컨테이너 런타임과의 관계
Docker, Podman 등의 컨테이너 런타임은 namespace를 조합하여 격리된 환경을 구성합니다:
| Namespace | 격리 대상 | 커널 버전 | clone 플래그 |
|---|---|---|---|
| Mount | 파일시스템 마운트 포인트 | 2.4.19 | CLONE_NEWNS |
| UTS | 호스트네임, 도메인네임 | 2.6.19 | CLONE_NEWUTS |
| IPC | System V IPC, POSIX MQ | 2.6.19 | CLONE_NEWIPC |
| PID | 프로세스 ID | 2.6.24 | CLONE_NEWPID |
| Network | 네트워크 스택 | 2.6.29 | CLONE_NEWNET |
| User | UID/GID 매핑 | 3.8 | CLONE_NEWUSER |
| Cgroup | cgroup 루트 디렉토리 | 4.6 | CLONE_NEWCGROUP |
| Time | 부트 시간, 모노토닉 시계 | 5.6 | CLONE_NEWTIME |
참고 자료: LWN: Namespaces in operation 시리즈, man 7 namespaces, man 7 pid_namespaces, man 7 network_namespaces
네임스페이스 관련 주요 취약점
User namespace는 비특권 사용자에게 네임스페이스 내부의 CAP_SYS_ADMIN을 부여하므로, 커널의 다양한 공격 면적을 노출합니다. 많은 커널 취약점이 user namespace를 전제 조건으로 요구하며, 이는 namespace 자체의 보안 설계와 밀접하게 연관됩니다.
fsconfig() 시스템 콜의 legacy_parse_param()에서 파라미터 길이 검증 부족으로 정수 언더플로우가 발생합니다. User namespace 내의 CAP_SYS_ADMIN만으로 트리거 가능하며, 힙 오버플로우를 통해 init namespace로 탈출할 수 있습니다. Linux 5.1~5.16에 영향을 미칩니다.
Ubuntu 커널에 적용된 OverlayFS 패치에서, user namespace 내에서 OverlayFS를 마운트할 때 trusted.overlayfs.metacopy xattr 설정이 권한 검사를 우회합니다. 이를 통해 비특권 사용자가 임의 파일에 setuid/setcap 속성을 설정하여 root 권한을 획득할 수 있습니다.
/* User Namespace가 공격 전제 조건인 주요 CVE 목록 */
/*
* CVE-2022-0185: fsconfig() 힙 오버플로우
* → user NS의 CAP_SYS_ADMIN으로 fsconfig() 호출 가능
*
* CVE-2022-0492: cgroup v1 release_agent
* → user NS + cgroup NS로 cgroup 마운트 가능
*
* CVE-2022-1015: nf_tables 스택 버퍼 오버플로우
* → user NS의 CAP_NET_ADMIN으로 nf_tables 규칙 생성
*
* CVE-2023-0386: OverlayFS + FUSE setuid 우회
* → user NS에서 FUSE + OverlayFS 마운트 가능
*
* CVE-2023-32233: nf_tables 익명 set UAF
* → user NS의 CAP_NET_ADMIN으로 nf_tables 조작
*
* CVE-2024-1086: nf_tables verdict UAF
* → user NS의 CAP_NET_ADMIN으로 트리거
*/
/* User Namespace 공격 면적 제한 방법 */
# 방법 1: User NS 완전 비활성화 (가장 강력)
sysctl -w user.max_user_namespaces=0
# 방법 2: 비특권 User NS 비활성화 (Debian/Ubuntu)
sysctl -w kernel.unprivileged_userns_clone=0
# 방법 3: BPF와 perf 제한 (user NS 내 CAP_SYS_ADMIN 영향 축소)
sysctl -w kernel.unprivileged_bpf_disabled=2
sysctl -w kernel.perf_event_paranoid=3
# 방법 4: AppArmor로 user NS 내 특정 작업 제한 (Ubuntu 23.10+)
# /etc/apparmor.d/unprivileged_userns 프로파일 활용
PID 재사용 공격: PID namespace 내에서 프로세스가 종료되고 동일 PID가 재할당될 때, kill()이나 ptrace() 등의 시스템 콜이 의도하지 않은 프로세스에 작용할 수 있습니다. pidfd_open()(5.3+)을 사용하여 PID 대신 파일 디스크립터로 프로세스를 참조하면 이 문제를 방지할 수 있습니다.
프로세스 정보 누출: /proc 파일시스템의 hidepid=2 마운트 옵션을 사용하여 다른 사용자의 프로세스 정보를 숨길 수 있습니다.